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-18 11:48:08 +01:00
|
|
|
"github.com/spf13/cobra"
|
2025-12-08 09:49:07 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var prCmd = &cobra.Command{
|
|
|
|
|
Use: "pr",
|
|
|
|
|
Aliases: []string{"pull-request"},
|
|
|
|
|
Short: "Manage pull requests",
|
|
|
|
|
Long: "Create, view, list, and manage pull requests.",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prListCmd = &cobra.Command{
|
|
|
|
|
Use: "list [flags]",
|
|
|
|
|
Short: "List pull requests",
|
|
|
|
|
Long: "List pull requests in a repository.",
|
|
|
|
|
RunE: runPRList,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prViewCmd = &cobra.Command{
|
|
|
|
|
Use: "view <number>",
|
|
|
|
|
Short: "View a pull request",
|
|
|
|
|
Long: "Display detailed information about a pull request.",
|
|
|
|
|
Args: cobra.ExactArgs(1),
|
|
|
|
|
RunE: runPRView,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prCreateCmd = &cobra.Command{
|
|
|
|
|
Use: "create",
|
|
|
|
|
Short: "Create a pull request",
|
|
|
|
|
Long: "Create a new pull request.",
|
|
|
|
|
RunE: runPRCreate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var prMergeCmd = &cobra.Command{
|
|
|
|
|
Use: "merge <number>",
|
|
|
|
|
Short: "Merge a pull request",
|
|
|
|
|
Long: "Merge a pull request.",
|
|
|
|
|
Args: cobra.ExactArgs(1),
|
|
|
|
|
RunE: runPRMerge,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
rootCmd.AddCommand(prCmd)
|
|
|
|
|
prCmd.AddCommand(prListCmd)
|
|
|
|
|
prCmd.AddCommand(prViewCmd)
|
|
|
|
|
prCmd.AddCommand(prCreateCmd)
|
|
|
|
|
prCmd.AddCommand(prMergeCmd)
|
|
|
|
|
|
|
|
|
|
prListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
|
|
|
prListCmd.Flags().StringP("state", "s", "open", "Filter by state: open, closed, all")
|
2026-01-18 11:48:08 +01:00
|
|
|
prListCmd.Flags().Bool("json", false, "Output pull requests as JSON")
|
2025-12-08 09:49:07 +01:00
|
|
|
|
|
|
|
|
prViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
2026-01-18 11:48:08 +01:00
|
|
|
prViewCmd.Flags().Bool("json", false, "Output pull request as JSON")
|
2025-12-08 09:49:07 +01:00
|
|
|
|
|
|
|
|
prCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
|
|
|
prCreateCmd.Flags().StringP("title", "t", "", "Title for the pull request")
|
|
|
|
|
prCreateCmd.Flags().StringP("body", "b", "", "Body for the pull request")
|
|
|
|
|
prCreateCmd.Flags().StringP("head", "H", "", "Head branch")
|
|
|
|
|
prCreateCmd.Flags().StringP("base", "B", "", "Base branch (default: main)")
|
2025-12-24 10:15:50 +01:00
|
|
|
prCreateCmd.Flags().StringSliceP("assignee", "a", []string{}, "Assign people by their login. Use \"@me\" to self-assign.")
|
2025-12-08 09:49:07 +01:00
|
|
|
|
|
|
|
|
prMergeCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
|
|
|
prMergeCmd.Flags().String("merge-method", "merge", "Merge method: merge, rebase, squash")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runPRList(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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prs, _, err := client.ListRepoPullRequests(owner, name, gitea.ListPullRequestsOptions{
|
|
|
|
|
State: stateType,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to list pull requests: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 11:48:08 +01:00
|
|
|
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
|
|
|
|
return writeJSON(prs)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 09:49:07 +01:00
|
|
|
if len(prs) == 0 {
|
|
|
|
|
fmt.Printf("No %s pull requests 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\tBRANCH\tSTATE\n")
|
2025-12-08 09:49:07 +01:00
|
|
|
for _, pr := range prs {
|
2025-12-08 10:00:50 +01:00
|
|
|
_, _ = fmt.Fprintf(w, "#%d\t%s\t%s\t%s\n", pr.Index, pr.Title, pr.Head.Ref, pr.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 runPRView(cmd *cobra.Command, args []string) error {
|
|
|
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
|
|
|
prNumber, err := strconv.ParseInt(args[0], 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("invalid pull request 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pr, _, err := client.GetPullRequest(owner, name, prNumber)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to get pull request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 11:48:08 +01:00
|
|
|
if jsonOutput, _ := cmd.Flags().GetBool("json"); jsonOutput {
|
|
|
|
|
return writeJSON(pr)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 09:49:07 +01:00
|
|
|
fmt.Printf("Pull Request #%d\n", pr.Index)
|
|
|
|
|
fmt.Printf("Title: %s\n", pr.Title)
|
|
|
|
|
fmt.Printf("State: %s\n", pr.State)
|
|
|
|
|
fmt.Printf("Author: %s\n", pr.Poster.UserName)
|
|
|
|
|
fmt.Printf("Branch: %s -> %s\n", pr.Head.Ref, pr.Base.Ref)
|
|
|
|
|
fmt.Printf("Created: %s\n", pr.Created.Format("2006-01-02 15:04:05"))
|
|
|
|
|
fmt.Printf("Updated: %s\n", pr.Updated.Format("2006-01-02 15:04:05"))
|
|
|
|
|
if pr.Body != "" {
|
|
|
|
|
fmt.Printf("\n%s\n", pr.Body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runPRCreate(cmd *cobra.Command, args []string) error {
|
|
|
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
|
|
|
title, _ := cmd.Flags().GetString("title")
|
|
|
|
|
body, _ := cmd.Flags().GetString("body")
|
|
|
|
|
head, _ := cmd.Flags().GetString("head")
|
|
|
|
|
base, _ := cmd.Flags().GetString("base")
|
2025-12-24 10:15:50 +01:00
|
|
|
assignees, _ := cmd.Flags().GetStringSlice("assignee")
|
2025-12-08 09:49:07 +01:00
|
|
|
|
|
|
|
|
if base == "" {
|
|
|
|
|
base = "main"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
owner, name, err := parseRepo(repo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if title == "" {
|
|
|
|
|
return fmt.Errorf("title is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if head == "" {
|
|
|
|
|
return fmt.Errorf("head branch 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 10:15:50 +01:00
|
|
|
// Resolve @me in assignees
|
|
|
|
|
resolvedAssignees := make([]string, 0, len(assignees))
|
|
|
|
|
for _, assignee := range assignees {
|
|
|
|
|
if assignee == "@me" {
|
|
|
|
|
user, _, err := client.GetMyUserInfo()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to get current user info: %w", err)
|
|
|
|
|
}
|
|
|
|
|
resolvedAssignees = append(resolvedAssignees, user.UserName)
|
|
|
|
|
} else {
|
|
|
|
|
resolvedAssignees = append(resolvedAssignees, assignee)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 09:49:07 +01:00
|
|
|
pr, _, err := client.CreatePullRequest(owner, name, gitea.CreatePullRequestOption{
|
2025-12-24 10:15:50 +01:00
|
|
|
Title: title,
|
|
|
|
|
Body: body,
|
|
|
|
|
Head: head,
|
|
|
|
|
Base: base,
|
|
|
|
|
Assignees: resolvedAssignees,
|
2025-12-08 09:49:07 +01:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create pull request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Pull request created: #%d\n", pr.Index)
|
|
|
|
|
fmt.Printf("View at: %s\n", pr.HTMLURL)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func runPRMerge(cmd *cobra.Command, args []string) error {
|
|
|
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
|
|
|
mergeMethod, _ := cmd.Flags().GetString("merge-method")
|
|
|
|
|
prNumber, err := strconv.ParseInt(args[0], 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("invalid pull request 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var method gitea.MergeStyle
|
|
|
|
|
switch strings.ToLower(mergeMethod) {
|
|
|
|
|
case "merge":
|
|
|
|
|
method = gitea.MergeStyleMerge
|
|
|
|
|
case "rebase":
|
|
|
|
|
method = gitea.MergeStyleRebase
|
|
|
|
|
case "squash":
|
|
|
|
|
method = gitea.MergeStyleSquash
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Errorf("invalid merge method: %s", mergeMethod)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, _, err = client.MergePullRequest(owner, name, prNumber, gitea.MergePullRequestOption{
|
|
|
|
|
Style: method,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to merge pull request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Pull request #%d merged successfully\n", prNumber)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|