Merge pull request 'feat: allow tags passing when creating/editing an issue' (#31) from feat/issue-27/labels_for_issue_creation_and_edition into main

Reviewed-on: https://codeberg.org/romaintb/fgj/pulls/31
This commit is contained in:
Romain Bertrand 2026-02-20 17:53:36 +01:00
commit 527f6bf15f
3 changed files with 327 additions and 7 deletions

View file

@ -84,6 +84,7 @@ func init() {
issueCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") issueCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
issueCreateCmd.Flags().StringP("title", "t", "", "Title for the issue") issueCreateCmd.Flags().StringP("title", "t", "", "Title for the issue")
issueCreateCmd.Flags().StringP("body", "b", "", "Body 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("repo", "R", "", "Repository in owner/name format")
issueCommentCmd.Flags().StringP("body", "b", "", "Comment body") 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("title", "t", "", "New title for the issue")
issueEditCmd.Flags().StringP("body", "b", "", "New body 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().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 { 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") repo, _ := cmd.Flags().GetString("repo")
title, _ := cmd.Flags().GetString("title") title, _ := cmd.Flags().GetString("title")
body, _ := cmd.Flags().GetString("body") body, _ := cmd.Flags().GetString("body")
labelNames, _ := cmd.Flags().GetStringSlice("label")
owner, name, err := parseRepo(repo) owner, name, err := parseRepo(repo)
if err != nil { if err != nil {
@ -253,9 +257,18 @@ func runIssueCreate(cmd *cobra.Command, args []string) error {
return err 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{ issue, _, err := client.CreateIssue(owner, name, gitea.CreateIssueOption{
Title: title, Title: title,
Body: body, Body: body,
Labels: labelIDs,
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to create issue: %w", err) 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") title, _ := cmd.Flags().GetString("title")
body, _ := cmd.Flags().GetString("body") body, _ := cmd.Flags().GetString("body")
stateStr, _ := cmd.Flags().GetString("state") 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) issueNumber, err := strconv.ParseInt(args[0], 10, 64)
if err != nil { if err != nil {
@ -368,8 +383,8 @@ func runIssueEdit(cmd *cobra.Command, args []string) error {
return err return err
} }
if title == "" && body == "" && stateStr == "" { if title == "" && body == "" && stateStr == "" && len(addLabelNames) == 0 && len(removeLabelNames) == 0 {
return fmt.Errorf("at least one of --title, --body, or --state must be provided") return fmt.Errorf("at least one of --title, --body, --state, --add-label, or --remove-label must be provided")
} }
cfg, err := config.Load() cfg, err := config.Load()
@ -405,12 +420,73 @@ func runIssueEdit(cmd *cobra.Command, args []string) error {
} }
} }
if title != "" || body != "" || stateStr != "" {
_, _, err = client.EditIssue(owner, name, issueNumber, editOpt) _, _, err = client.EditIssue(owner, name, issueNumber, editOpt)
if err != nil { if err != nil {
return fmt.Errorf("failed to edit issue: %w", err) 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) fmt.Printf("Issue #%d updated\n", issueNumber)
return nil 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
}

View file

@ -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 // GetBinaryPath returns the path to the built fgj binary
func (env *TestEnv) GetBinaryPath() string { func (env *TestEnv) GetBinaryPath() string {
binaryPath := os.Getenv("FGJ_BINARY_PATH") binaryPath := os.Getenv("FGJ_BINARY_PATH")

View file

@ -103,6 +103,182 @@ func TestCreateAndListIssues(t *testing.T) {
t.Logf("Successfully created and listed issue #%d", issueNum) 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: #<number>")
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 // TestGetIssue verifies we can get issue details
func TestGetIssue(t *testing.T) { func TestGetIssue(t *testing.T) {
env := NewTestEnv(t) env := NewTestEnv(t)