From 0b8f67e43899d4c5b3d0432d84f3620318d10e79 Mon Sep 17 00:00:00 2001 From: Romain Bertrand Date: Fri, 20 Feb 2026 17:41:45 +0100 Subject: [PATCH 1/2] feat: allow tags passing when creating/editing an issue resolves #27 --- cmd/issue.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/cmd/issue.go b/cmd/issue.go index d582d01..f008f83 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -84,6 +84,7 @@ func init() { issueCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") issueCreateCmd.Flags().StringP("title", "t", "", "Title for the issue") issueCreateCmd.Flags().StringP("body", "b", "", "Body for the issue") + issueCreateCmd.Flags().StringSliceP("label", "l", nil, "Labels to add (can be specified multiple times)") issueCommentCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") issueCommentCmd.Flags().StringP("body", "b", "", "Comment body") @@ -95,6 +96,8 @@ func init() { issueEditCmd.Flags().StringP("title", "t", "", "New title for the issue") issueEditCmd.Flags().StringP("body", "b", "", "New body for the issue") issueEditCmd.Flags().StringP("state", "s", "", "New state for the issue (open or closed)") + issueEditCmd.Flags().StringSlice("add-label", nil, "Labels to add (can be specified multiple times)") + issueEditCmd.Flags().StringSlice("remove-label", nil, "Labels to remove (can be specified multiple times)") } func runIssueList(cmd *cobra.Command, args []string) error { @@ -233,6 +236,7 @@ func runIssueCreate(cmd *cobra.Command, args []string) error { repo, _ := cmd.Flags().GetString("repo") title, _ := cmd.Flags().GetString("title") body, _ := cmd.Flags().GetString("body") + labelNames, _ := cmd.Flags().GetStringSlice("label") owner, name, err := parseRepo(repo) if err != nil { @@ -253,9 +257,18 @@ func runIssueCreate(cmd *cobra.Command, args []string) error { return err } + var labelIDs []int64 + if len(labelNames) > 0 { + labelIDs, err = resolveLabelIDs(client, owner, name, labelNames) + if err != nil { + return err + } + } + issue, _, err := client.CreateIssue(owner, name, gitea.CreateIssueOption{ - Title: title, - Body: body, + Title: title, + Body: body, + Labels: labelIDs, }) if err != nil { return fmt.Errorf("failed to create issue: %w", err) @@ -357,6 +370,8 @@ func runIssueEdit(cmd *cobra.Command, args []string) error { title, _ := cmd.Flags().GetString("title") body, _ := cmd.Flags().GetString("body") stateStr, _ := cmd.Flags().GetString("state") + addLabelNames, _ := cmd.Flags().GetStringSlice("add-label") + removeLabelNames, _ := cmd.Flags().GetStringSlice("remove-label") issueNumber, err := strconv.ParseInt(args[0], 10, 64) if err != nil { @@ -368,8 +383,8 @@ func runIssueEdit(cmd *cobra.Command, args []string) error { return err } - if title == "" && body == "" && stateStr == "" { - return fmt.Errorf("at least one of --title, --body, or --state must be provided") + if title == "" && body == "" && stateStr == "" && len(addLabelNames) == 0 && len(removeLabelNames) == 0 { + return fmt.Errorf("at least one of --title, --body, --state, --add-label, or --remove-label must be provided") } cfg, err := config.Load() @@ -405,12 +420,73 @@ func runIssueEdit(cmd *cobra.Command, args []string) error { } } - _, _, err = client.EditIssue(owner, name, issueNumber, editOpt) - if err != nil { - return fmt.Errorf("failed to edit issue: %w", err) + if title != "" || body != "" || stateStr != "" { + _, _, err = client.EditIssue(owner, name, issueNumber, editOpt) + if err != nil { + return fmt.Errorf("failed to edit issue: %w", err) + } + } + + if len(addLabelNames) > 0 { + labelIDs, err := resolveLabelIDs(client, owner, name, addLabelNames) + if err != nil { + return err + } + _, _, err = client.AddIssueLabels(owner, name, issueNumber, gitea.IssueLabelsOption{ + Labels: labelIDs, + }) + if err != nil { + return fmt.Errorf("failed to add labels: %w", err) + } + } + + if len(removeLabelNames) > 0 { + labelIDs, err := resolveLabelIDs(client, owner, name, removeLabelNames) + if err != nil { + return err + } + for _, labelID := range labelIDs { + _, err = client.DeleteIssueLabel(owner, name, issueNumber, labelID) + if err != nil { + return fmt.Errorf("failed to remove label %d: %w", labelID, err) + } + } } fmt.Printf("Issue #%d updated\n", issueNumber) return nil } + +func resolveLabelIDs(client *api.Client, owner, name string, labelNames []string) ([]int64, error) { + if len(labelNames) == 0 { + return nil, nil + } + + labels, _, err := client.ListRepoLabels(owner, name, gitea.ListLabelsOptions{}) + if err != nil { + return nil, fmt.Errorf("failed to list labels: %w", err) + } + + labelNameToID := make(map[string]int64) + for _, label := range labels { + labelNameToID[label.Name] = label.ID + } + + var labelIDs []int64 + var missingLabels []string + for _, labelName := range labelNames { + labelID, exists := labelNameToID[labelName] + if !exists { + missingLabels = append(missingLabels, labelName) + continue + } + labelIDs = append(labelIDs, labelID) + } + + if len(missingLabels) > 0 { + return nil, fmt.Errorf("labels not found: %s", strings.Join(missingLabels, ", ")) + } + + return labelIDs, nil +} From 2a20a4e0b80f290a2336cf69c94529f9b5076595 Mon Sep 17 00:00:00 2001 From: Romain Bertrand Date: Fri, 20 Feb 2026 17:48:56 +0100 Subject: [PATCH 2/2] tests: add functional tests for issues labels --- tests/functional/fixtures.go | 68 +++++++++++ tests/functional/functional_test.go | 176 ++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) diff --git a/tests/functional/fixtures.go b/tests/functional/fixtures.go index 80621ab..e510a92 100644 --- a/tests/functional/fixtures.go +++ b/tests/functional/fixtures.go @@ -151,6 +151,74 @@ func (env *TestEnv) VerifyAPIConnection() { } } +// EnsureTestLabels creates test labels if they don't exist +func (env *TestEnv) EnsureTestLabels() { + labelNames := []string{"bug", "enhancement", "help-wanted"} + + for _, name := range labelNames { + labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{}) + if err != nil { + env.T.Fatalf("failed to list labels: %v", err) + } + + exists := false + for _, label := range labels { + if label.Name == name { + exists = true + break + } + } + + if !exists { + color := "#00aabb" + if name == "bug" { + color = "#ff0000" + } else if name == "enhancement" { + color = "#00ff00" + } + _, _, err = env.Client.CreateLabel(env.Owner, env.RepoName, gitea.CreateLabelOption{ + Name: name, + Color: color, + }) + if err != nil { + env.T.Logf("warning: failed to create label '%s': %v", name, err) + } else { + env.T.Logf("Created test label: %s", name) + } + } + } +} + +// GetIssueLabels gets the labels on an issue +func (env *TestEnv) GetIssueLabels(issueNumber int64) ([]*gitea.Label, error) { + labels, _, err := env.Client.GetIssueLabels(env.Owner, env.RepoName, issueNumber, gitea.ListLabelsOptions{}) + return labels, err +} + +// GetLabelIDs converts label names to label IDs +func (env *TestEnv) GetLabelIDs(labelNames []string) []int64 { + labels, _, err := env.Client.ListRepoLabels(env.Owner, env.RepoName, gitea.ListLabelsOptions{}) + if err != nil { + env.T.Fatalf("failed to list labels: %v", err) + } + + nameToID := make(map[string]int64) + for _, label := range labels { + nameToID[label.Name] = label.ID + } + + var ids []int64 + for _, name := range labelNames { + id, exists := nameToID[name] + if !exists { + env.T.Fatalf("label '%s' not found", name) + } + ids = append(ids, id) + } + + return ids +} + // GetBinaryPath returns the path to the built fgj binary func (env *TestEnv) GetBinaryPath() string { binaryPath := os.Getenv("FGJ_BINARY_PATH") diff --git a/tests/functional/functional_test.go b/tests/functional/functional_test.go index 6dee28b..1764ba6 100644 --- a/tests/functional/functional_test.go +++ b/tests/functional/functional_test.go @@ -103,6 +103,182 @@ func TestCreateAndListIssues(t *testing.T) { t.Logf("Successfully created and listed issue #%d", issueNum) } +// TestIssueCreateWithLabels verifies we can create issues with labels via CLI +func TestIssueCreateWithLabels(t *testing.T) { + env := NewTestEnv(t) + + // Ensure test labels exist + env.EnsureTestLabels() + + // Create an issue with labels via CLI + result := env.RunCLI( + "--hostname", env.Hostname, + "issue", "create", + "-t", "[FGJ E2E Test] Issue with Labels", + "-b", "This issue was created with labels", + "-l", "bug", + "-l", "enhancement", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue create with labels failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + // Extract issue number from output (format: "Issue created: #") + var issueNum int64 + _, err := fmt.Sscanf(result.Stdout, "Issue created: #%d", &issueNum) + if err != nil { + t.Fatalf("failed to parse issue number from output: %v", err) + } + + defer env.CleanupIssue(issueNum) + + // Verify labels were applied + labels, err := env.GetIssueLabels(issueNum) + if err != nil { + t.Fatalf("failed to get issue labels: %v", err) + } + + if len(labels) != 2 { + t.Fatalf("expected 2 labels, got %d", len(labels)) + } + + labelNames := make(map[string]bool) + for _, label := range labels { + labelNames[label.Name] = true + } + + if !labelNames["bug"] || !labelNames["enhancement"] { + t.Fatalf("expected labels 'bug' and 'enhancement', got %v", labelNames) + } + + t.Logf("Successfully created issue #%d with labels: bug, enhancement", issueNum) +} + +// TestIssueEditAddLabels verifies we can add labels to an issue via CLI +func TestIssueEditAddLabels(t *testing.T) { + env := NewTestEnv(t) + + // Ensure test labels exist + env.EnsureTestLabels() + + // Create an issue without labels + issueNum := env.CreateTestIssue( + "[FGJ E2E Test] Add Labels Test", + "This issue will have labels added", + ) + defer env.CleanupIssue(issueNum) + + // Verify no labels initially + labels, err := env.GetIssueLabels(issueNum) + if err != nil { + t.Fatalf("failed to get initial labels: %v", err) + } + + if len(labels) != 0 { + t.Fatalf("expected 0 labels initially, got %d", len(labels)) + } + + // Add labels via CLI + result := env.RunCLI( + "--hostname", env.Hostname, + "issue", "edit", + fmt.Sprintf("%d", issueNum), + "--add-label", "bug", + "--add-label", "help-wanted", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue edit add-label failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + // Verify labels were added + labels, err = env.GetIssueLabels(issueNum) + if err != nil { + t.Fatalf("failed to get labels after edit: %v", err) + } + + if len(labels) != 2 { + t.Fatalf("expected 2 labels after edit, got %d", len(labels)) + } + + labelNames := make(map[string]bool) + for _, label := range labels { + labelNames[label.Name] = true + } + + if !labelNames["bug"] || !labelNames["help-wanted"] { + t.Fatalf("expected labels 'bug' and 'help-wanted', got %v", labelNames) + } + + t.Logf("Successfully added labels to issue #%d", issueNum) +} + +// TestIssueEditRemoveLabels verifies we can remove labels from an issue via CLI +func TestIssueEditRemoveLabels(t *testing.T) { + env := NewTestEnv(t) + + // Ensure test labels exist + env.EnsureTestLabels() + + // Create an issue with labels + issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, gitea.CreateIssueOption{ + Title: "[FGJ E2E Test] Remove Labels Test", + Body: "This issue will have labels removed", + }) + if err != nil { + t.Fatalf("failed to create issue: %v", err) + } + defer env.CleanupIssue(issue.Index) + + // Add labels via API + labelIDs := env.GetLabelIDs([]string{"bug", "enhancement"}) + _, _, err = env.Client.AddIssueLabels(env.Owner, env.RepoName, issue.Index, gitea.IssueLabelsOption{ + Labels: labelIDs, + }) + if err != nil { + t.Fatalf("failed to add initial labels: %v", err) + } + + // Verify initial labels + labels, err := env.GetIssueLabels(issue.Index) + if err != nil { + t.Fatalf("failed to get initial labels: %v", err) + } + + if len(labels) != 2 { + t.Fatalf("expected 2 labels initially, got %d", len(labels)) + } + + // Remove one label via CLI + result := env.RunCLI( + "--hostname", env.Hostname, + "issue", "edit", + fmt.Sprintf("%d", issue.Index), + "--remove-label", "bug", + ) + + if result.ExitCode != 0 { + t.Fatalf("issue edit remove-label failed with exit code %d: %s", result.ExitCode, result.Stderr) + } + + // Verify label was removed + labels, err = env.GetIssueLabels(issue.Index) + if err != nil { + t.Fatalf("failed to get labels after edit: %v", err) + } + + if len(labels) != 1 { + t.Fatalf("expected 1 label after removal, got %d", len(labels)) + } + + if labels[0].Name != "enhancement" { + t.Fatalf("expected remaining label 'enhancement', got '%s'", labels[0].Name) + } + + t.Logf("Successfully removed label from issue #%d", issue.Index) +} + // TestGetIssue verifies we can get issue details func TestGetIssue(t *testing.T) { env := NewTestEnv(t)