fj/cmd/issue.go

379 lines
8.9 KiB
Go
Raw Normal View History

2025-12-08 09:49:07 +01:00
package cmd
import (
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"code.gitea.io/sdk/gitea"
"codeberg.org/romaintb/fgj/internal/api"
"codeberg.org/romaintb/fgj/internal/config"
2026-01-03 11:49:34 +01:00
"github.com/spf13/cobra"
2025-12-08 09:49:07 +01:00
)
var issueCmd = &cobra.Command{
Use: "issue",
Short: "Manage issues",
Long: "Create, view, list, and manage issues.",
}
var issueListCmd = &cobra.Command{
Use: "list [flags]",
Short: "List issues",
Long: "List issues in a repository.",
RunE: runIssueList,
}
var issueViewCmd = &cobra.Command{
Use: "view <number>",
Short: "View an issue",
Long: "Display detailed information about an issue.",
Args: cobra.ExactArgs(1),
RunE: runIssueView,
}
var issueCreateCmd = &cobra.Command{
Use: "create",
Short: "Create an issue",
Long: "Create a new issue.",
RunE: runIssueCreate,
}
var issueCommentCmd = &cobra.Command{
Use: "comment <number>",
Short: "Add a comment to an issue",
Long: "Add a comment to an existing issue.",
Args: cobra.ExactArgs(1),
RunE: runIssueComment,
}
var issueCloseCmd = &cobra.Command{
Use: "close <number>",
Short: "Close an issue",
Long: "Close an existing issue.",
Args: cobra.ExactArgs(1),
RunE: runIssueClose,
}
2026-01-03 11:49:34 +01:00
var issueEditCmd = &cobra.Command{
Use: "edit <number>",
Short: "Edit an issue",
Long: "Edit an existing issue's title, body, or state.",
Args: cobra.ExactArgs(1),
RunE: runIssueEdit,
}
2025-12-08 09:49:07 +01:00
func init() {
rootCmd.AddCommand(issueCmd)
issueCmd.AddCommand(issueListCmd)
issueCmd.AddCommand(issueViewCmd)
issueCmd.AddCommand(issueCreateCmd)
issueCmd.AddCommand(issueCommentCmd)
issueCmd.AddCommand(issueCloseCmd)
2026-01-03 11:49:34 +01:00
issueCmd.AddCommand(issueEditCmd)
2025-12-08 09:49:07 +01:00
issueListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
issueListCmd.Flags().StringP("state", "s", "open", "Filter by state: open, closed, all")
issueViewCmd.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("body", "b", "", "Body for the issue")
issueCommentCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
issueCommentCmd.Flags().StringP("body", "b", "", "Comment body")
issueCloseCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
2026-01-03 11:49:34 +01:00
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)")
2025-12-08 09:49:07 +01:00
}
func runIssueList(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")
state, _ := cmd.Flags().GetString("state")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
cfg, err := config.Load()
if err != nil {
return err
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2025-12-08 09:49:07 +01:00
if err != nil {
return err
}
var stateType gitea.StateType
switch strings.ToLower(state) {
case "open":
stateType = gitea.StateOpen
case "closed":
stateType = gitea.StateClosed
case "all":
stateType = gitea.StateAll
default:
return fmt.Errorf("invalid state: %s", state)
}
issues, _, err := client.ListRepoIssues(owner, name, gitea.ListIssueOption{
State: stateType,
})
if err != nil {
return fmt.Errorf("failed to list issues: %w", err)
}
if len(issues) == 0 {
fmt.Printf("No %s issues in %s/%s\n", state, owner, name)
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
2025-12-08 10:00:50 +01:00
_, _ = fmt.Fprintf(w, "NUMBER\tTITLE\tSTATE\n")
2025-12-08 09:49:07 +01:00
for _, issue := range issues {
if issue.PullRequest == nil {
2025-12-08 10:00:50 +01:00
_, _ = fmt.Fprintf(w, "#%d\t%s\t%s\n", issue.Index, issue.Title, issue.State)
2025-12-08 09:49:07 +01:00
}
}
2025-12-08 10:00:50 +01:00
_ = w.Flush()
2025-12-08 09:49:07 +01:00
return nil
}
func runIssueView(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")
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
}
cfg, err := config.Load()
if err != nil {
return err
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2025-12-08 09:49:07 +01:00
if err != nil {
return err
}
issue, _, err := client.GetIssue(owner, name, issueNumber)
if err != nil {
return fmt.Errorf("failed to get issue: %w", err)
}
fmt.Printf("Issue #%d\n", issue.Index)
fmt.Printf("Title: %s\n", issue.Title)
fmt.Printf("State: %s\n", issue.State)
fmt.Printf("Author: %s\n", issue.Poster.UserName)
fmt.Printf("Created: %s\n", issue.Created.Format("2006-01-02 15:04:05"))
fmt.Printf("Updated: %s\n", issue.Updated.Format("2006-01-02 15:04:05"))
if issue.Body != "" {
fmt.Printf("\n%s\n", issue.Body)
}
comments, _, err := client.ListIssueComments(owner, name, issueNumber, gitea.ListIssueCommentOptions{})
if err == nil && len(comments) > 0 {
fmt.Printf("\nComments (%d):\n", len(comments))
for _, comment := range comments {
fmt.Printf("\n---\n%s (@%s) - %s\n%s\n",
comment.Poster.FullName,
comment.Poster.UserName,
comment.Created.Format("2006-01-02 15:04:05"),
comment.Body)
}
}
return nil
}
func runIssueCreate(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")
title, _ := cmd.Flags().GetString("title")
body, _ := cmd.Flags().GetString("body")
owner, name, err := parseRepo(repo)
if err != nil {
return err
}
if title == "" {
return fmt.Errorf("title is required")
}
cfg, err := config.Load()
if err != nil {
return err
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2025-12-08 09:49:07 +01:00
if err != nil {
return err
}
issue, _, err := client.CreateIssue(owner, name, gitea.CreateIssueOption{
Title: title,
Body: body,
})
if err != nil {
return fmt.Errorf("failed to create issue: %w", err)
}
fmt.Printf("Issue created: #%d\n", issue.Index)
fmt.Printf("View at: %s\n", issue.HTMLURL)
return nil
}
func runIssueComment(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")
body, _ := cmd.Flags().GetString("body")
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 body == "" {
return fmt.Errorf("comment body is required")
}
cfg, err := config.Load()
if err != nil {
return err
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2025-12-08 09:49:07 +01:00
if err != nil {
return err
}
comment, _, err := client.CreateIssueComment(owner, name, issueNumber, gitea.CreateIssueCommentOption{
Body: body,
})
if err != nil {
return fmt.Errorf("failed to create comment: %w", err)
}
fmt.Printf("Comment added to issue #%d\n", issueNumber)
fmt.Printf("View at: %s\n", comment.HTMLURL)
return nil
}
func runIssueClose(cmd *cobra.Command, args []string) error {
repo, _ := cmd.Flags().GetString("repo")
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
}
cfg, err := config.Load()
if err != nil {
return err
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2025-12-08 09:49:07 +01:00
if err != nil {
return err
}
stateClosed := gitea.StateClosed
_, _, err = client.EditIssue(owner, name, issueNumber, gitea.EditIssueOption{
State: &stateClosed,
})
if err != nil {
return fmt.Errorf("failed to close issue: %w", err)
}
fmt.Printf("Issue #%d closed\n", issueNumber)
return nil
}
2026-01-03 11:49:34 +01:00
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
}
2026-01-05 12:47:28 +01:00
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
2026-01-03 11:49:34 +01:00
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
}