diff --git a/cmd/issue.go b/cmd/issue.go index 1644143..f7dfd7c 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -8,9 +8,9 @@ import ( "text/tabwriter" "code.gitea.io/sdk/gitea" - "github.com/spf13/cobra" "codeberg.org/romaintb/fgj/internal/api" "codeberg.org/romaintb/fgj/internal/config" + "github.com/spf13/cobra" ) var issueCmd = &cobra.Command{ @@ -57,6 +57,14 @@ var issueCloseCmd = &cobra.Command{ RunE: runIssueClose, } +var issueEditCmd = &cobra.Command{ + Use: "edit ", + Short: "Edit an issue", + Long: "Edit an existing issue's title, body, or state.", + Args: cobra.ExactArgs(1), + RunE: runIssueEdit, +} + func init() { rootCmd.AddCommand(issueCmd) issueCmd.AddCommand(issueListCmd) @@ -64,6 +72,7 @@ func init() { issueCmd.AddCommand(issueCreateCmd) issueCmd.AddCommand(issueCommentCmd) issueCmd.AddCommand(issueCloseCmd) + issueCmd.AddCommand(issueEditCmd) issueListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") issueListCmd.Flags().StringP("state", "s", "open", "Filter by state: open, closed, all") @@ -78,6 +87,11 @@ func init() { issueCommentCmd.Flags().StringP("body", "b", "", "Comment body") issueCloseCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") + + issueEditCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format") + 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)") } func runIssueList(cmd *cobra.Command, args []string) error { @@ -299,3 +313,66 @@ func runIssueClose(cmd *cobra.Command, args []string) error { return nil } + +func runIssueEdit(cmd *cobra.Command, args []string) error { + repo, _ := cmd.Flags().GetString("repo") + title, _ := cmd.Flags().GetString("title") + body, _ := cmd.Flags().GetString("body") + stateStr, _ := cmd.Flags().GetString("state") + + issueNumber, err := strconv.ParseInt(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid issue number: %w", err) + } + + owner, name, err := parseRepo(repo) + if err != nil { + return err + } + + if title == "" && body == "" && stateStr == "" { + return fmt.Errorf("at least one of --title, --body, or --state must be provided") + } + + cfg, err := config.Load() + if err != nil { + return err + } + + client, err := api.NewClientFromConfig(cfg, "") + if err != nil { + return err + } + + editOpt := gitea.EditIssueOption{} + + if title != "" { + editOpt.Title = title + } + + if body != "" { + editOpt.Body = &body + } + + if stateStr != "" { + switch strings.ToLower(stateStr) { + case "open": + stateOpen := gitea.StateOpen + editOpt.State = &stateOpen + case "closed": + stateClosed := gitea.StateClosed + editOpt.State = &stateClosed + default: + return fmt.Errorf("invalid state: %s (must be 'open' or 'closed')", stateStr) + } + } + + _, _, err = client.EditIssue(owner, name, issueNumber, editOpt) + if err != nil { + return fmt.Errorf("failed to edit issue: %w", err) + } + + fmt.Printf("Issue #%d updated\n", issueNumber) + + return nil +} diff --git a/tests/functional/functional_test.go b/tests/functional/functional_test.go index 73f098c..5d662dd 100644 --- a/tests/functional/functional_test.go +++ b/tests/functional/functional_test.go @@ -302,6 +302,184 @@ func TestCLIIssueClose(t *testing.T) { t.Logf("Successfully tested issue close via API for CLI #%d", issueNum) } +// TestEditIssueTitle verifies we can edit an issue's title +func TestEditIssueTitle(t *testing.T) { + env := NewTestEnv(t) + + // Create a test issue + issueNum := env.CreateTestIssue( + "[FGJ E2E Test] Original Title", + "This issue's title will be edited", + ) + + defer env.CleanupIssue(issueNum) + + // Edit the title + newTitle := "[FGJ E2E Test] Updated Title" + _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ + Title: newTitle, + }) + if err != nil { + t.Fatalf("failed to edit issue title: %v", err) + } + + // Verify the title was updated + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.Title != newTitle { + t.Fatalf("expected title '%s', got '%s'", newTitle, issue.Title) + } + + t.Logf("Successfully edited issue #%d title", issueNum) +} + +// TestEditIssueBody verifies we can edit an issue's body +func TestEditIssueBody(t *testing.T) { + env := NewTestEnv(t) + + // Create a test issue + issueNum := env.CreateTestIssue( + "[FGJ E2E Test] Edit Body Test", + "Original body content", + ) + + defer env.CleanupIssue(issueNum) + + // Edit the body + newBody := "Updated body content from functional test" + _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ + Body: &newBody, + }) + if err != nil { + t.Fatalf("failed to edit issue body: %v", err) + } + + // Verify the body was updated + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.Body != newBody { + t.Fatalf("expected body '%s', got '%s'", newBody, issue.Body) + } + + t.Logf("Successfully edited issue #%d body", issueNum) +} + +// TestEditIssueState verifies we can edit an issue's state +func TestEditIssueState(t *testing.T) { + env := NewTestEnv(t) + + // Create a test issue (starts as open) + issueNum := env.CreateTestIssue( + "[FGJ E2E Test] Edit State Test", + "This issue's state will be changed", + ) + + defer env.CleanupIssue(issueNum) + + // Verify it starts as open + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.State != "open" { + t.Fatalf("expected initial state 'open', got '%s'", issue.State) + } + + // Edit state to closed + closedState := gitea.StateClosed + _, _, err = env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ + State: &closedState, + }) + if err != nil { + t.Fatalf("failed to edit issue state: %v", err) + } + + // Verify state changed to closed + issue, _, err = env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.State != "closed" { + t.Fatalf("expected state 'closed' after edit, got '%s'", issue.State) + } + + // Edit state back to open + openState := gitea.StateOpen + _, _, err = env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ + State: &openState, + }) + if err != nil { + t.Fatalf("failed to reopen issue: %v", err) + } + + // Verify state is now open + issue, _, err = env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.State != "open" { + t.Fatalf("expected state 'open' after reopen, got '%s'", issue.State) + } + + t.Logf("Successfully edited issue #%d state", issueNum) +} + +// TestEditIssueMultipleFields verifies we can edit multiple issue fields at once +func TestEditIssueMultipleFields(t *testing.T) { + env := NewTestEnv(t) + + // Create a test issue + issueNum := env.CreateTestIssue( + "[FGJ E2E Test] Original Multi-Edit", + "Original body for multi-field edit", + ) + + defer env.CleanupIssue(issueNum) + + // Edit title, body, and state at once + newTitle := "[FGJ E2E Test] Updated Multi-Edit" + newBody := "Updated body from multi-field edit test" + closedState := gitea.StateClosed + + _, _, err := env.Client.EditIssue(env.Owner, env.RepoName, issueNum, gitea.EditIssueOption{ + Title: newTitle, + Body: &newBody, + State: &closedState, + }) + if err != nil { + t.Fatalf("failed to edit multiple issue fields: %v", err) + } + + // Verify all fields were updated + issue, _, err := env.Client.GetIssue(env.Owner, env.RepoName, issueNum) + if err != nil { + t.Fatalf("failed to get issue: %v", err) + } + + if issue.Title != newTitle { + t.Fatalf("expected title '%s', got '%s'", newTitle, issue.Title) + } + + if issue.Body != newBody { + t.Fatalf("expected body '%s', got '%s'", newBody, issue.Body) + } + + if issue.State != "closed" { + t.Fatalf("expected state 'closed', got '%s'", issue.State) + } + + t.Logf("Successfully edited multiple fields on issue #%d", issueNum) +} + // TestCLIPRList verifies the `fgj pr list` command works func TestCLIPRList(t *testing.T) { env := NewTestEnv(t)