feat: v0.3.0c — add labels, milestones, wiki, issue dependencies

New commands:
- fgj label list/create/edit/delete
- fgj milestone list/view/create/edit/delete
- fgj wiki list/view/create/edit/delete

Enhanced:
- fgj issue edit --add-dependency/--remove-dependency
This commit is contained in:
sid 2026-03-21 21:50:24 -06:00
parent 7ee5a61910
commit 95da06c003
7 changed files with 1206 additions and 3 deletions

278
cmd/label.go Normal file
View file

@ -0,0 +1,278 @@
package cmd
import (
"fmt"
"os"
"strings"
"text/tabwriter"
"code.gitea.io/sdk/gitea"
"forgejo.zerova.net/sid/fgj-sid/internal/api"
"forgejo.zerova.net/sid/fgj-sid/internal/config"
"github.com/spf13/cobra"
)
var labelCmd = &cobra.Command{
Use: "label",
Short: "Manage labels",
Long: "List, create, edit, and delete repository labels.",
}
var labelListCmd = &cobra.Command{
Use: "list",
Short: "List labels for a repository",
Long: "List all labels defined in a repository.",
Example: ` # List labels for the current repository
fgj label list
# List labels for a specific repository
fgj label list -R owner/repo
# Output as JSON
fgj label list --json`,
RunE: runLabelList,
}
var labelCreateCmd = &cobra.Command{
Use: "create <name>",
Short: "Create a label",
Long: "Create a new label in a repository.",
Example: ` # Create a label with a color
fgj label create bug -c ff0000
# Create a label with color and description
fgj label create feature -c 00ff00 -d "New feature request"
# Create a label in a specific repository
fgj label create urgent -c ff0000 -R owner/repo`,
Args: cobra.ExactArgs(1),
RunE: runLabelCreate,
}
var labelEditCmd = &cobra.Command{
Use: "edit <name>",
Short: "Edit a label",
Long: "Edit an existing label in a repository.",
Example: ` # Rename a label
fgj label edit bug --name bugfix
# Change the color of a label
fgj label edit bug -c 00ff00
# Update description
fgj label edit bug -d "Something is broken"`,
Args: cobra.ExactArgs(1),
RunE: runLabelEdit,
}
var labelDeleteCmd = &cobra.Command{
Use: "delete <name>",
Short: "Delete a label",
Long: "Delete a label from a repository.",
Example: ` # Delete a label
fgj label delete bug
# Delete a label from a specific repository
fgj label delete bug -R owner/repo`,
Args: cobra.ExactArgs(1),
RunE: runLabelDelete,
}
func init() {
rootCmd.AddCommand(labelCmd)
labelCmd.AddCommand(labelListCmd)
labelCmd.AddCommand(labelCreateCmd)
labelCmd.AddCommand(labelEditCmd)
labelCmd.AddCommand(labelDeleteCmd)
labelListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
labelListCmd.Flags().Bool("json", false, "Output as JSON")
labelCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
labelCreateCmd.Flags().StringP("color", "c", "", "Label color (hex, e.g. 00ff00)")
labelCreateCmd.Flags().StringP("description", "d", "", "Label description")
labelCreateCmd.Flags().Bool("json", false, "Output as JSON")
labelEditCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
labelEditCmd.Flags().String("name", "", "New name for the label")
labelEditCmd.Flags().StringP("color", "c", "", "New color (hex, e.g. 00ff00)")
labelEditCmd.Flags().StringP("description", "d", "", "New description")
labelEditCmd.Flags().Bool("json", false, "Output as JSON")
labelDeleteCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
}
func newLabelClient(cmd *cobra.Command) (*api.Client, string, string, error) {
repo, _ := cmd.Flags().GetString("repo")
owner, name, err := parseRepo(repo)
if err != nil {
return nil, "", "", err
}
cfg, err := config.Load()
if err != nil {
return nil, "", "", err
}
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
if err != nil {
return nil, "", "", err
}
return client, owner, name, nil
}
// findLabelByName lists all repo labels and returns the one matching the given name.
func findLabelByName(client *api.Client, owner, repo, labelName string) (*gitea.Label, error) {
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{})
if err != nil {
return nil, fmt.Errorf("failed to list labels: %w", err)
}
for _, l := range labels {
if strings.EqualFold(l.Name, labelName) {
return l, nil
}
}
return nil, fmt.Errorf("label not found: %s", labelName)
}
func runLabelList(cmd *cobra.Command, args []string) error {
client, owner, name, err := newLabelClient(cmd)
if err != nil {
return err
}
labels, _, err := client.ListRepoLabels(owner, name, gitea.ListLabelsOptions{})
if err != nil {
return fmt.Errorf("failed to list labels: %w", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(labels)
}
if len(labels) == 0 {
fmt.Println("No labels found")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "NAME\tCOLOR\tDESCRIPTION\n")
for _, l := range labels {
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", l.Name, l.Color, l.Description)
}
_ = w.Flush()
return nil
}
func runLabelCreate(cmd *cobra.Command, args []string) error {
labelName := args[0]
color, _ := cmd.Flags().GetString("color")
description, _ := cmd.Flags().GetString("description")
client, owner, name, err := newLabelClient(cmd)
if err != nil {
return err
}
label, _, err := client.CreateLabel(owner, name, gitea.CreateLabelOption{
Name: labelName,
Color: color,
Description: description,
})
if err != nil {
return fmt.Errorf("failed to create label: %w", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(label)
}
fmt.Printf("Label created: %s\n", label.Name)
return nil
}
func runLabelEdit(cmd *cobra.Command, args []string) error {
labelName := args[0]
client, owner, name, err := newLabelClient(cmd)
if err != nil {
return err
}
existing, err := findLabelByName(client, owner, name, labelName)
if err != nil {
return err
}
opt := gitea.EditLabelOption{}
changed := false
if cmd.Flags().Changed("name") {
n, _ := cmd.Flags().GetString("name")
opt.Name = &n
changed = true
}
if cmd.Flags().Changed("color") {
c, _ := cmd.Flags().GetString("color")
opt.Color = &c
changed = true
}
if cmd.Flags().Changed("description") {
d, _ := cmd.Flags().GetString("description")
opt.Description = &d
changed = true
}
if !changed {
return fmt.Errorf("no changes specified; use flags like --name, --color, or --description")
}
label, _, err := client.EditLabel(owner, name, existing.ID, opt)
if err != nil {
return fmt.Errorf("failed to edit label: %w", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
return writeJSON(label)
}
fmt.Printf("Label updated: %s\n", label.Name)
return nil
}
func runLabelDelete(cmd *cobra.Command, args []string) error {
labelName := args[0]
client, owner, name, err := newLabelClient(cmd)
if err != nil {
return err
}
existing, err := findLabelByName(client, owner, name, labelName)
if err != nil {
return err
}
fmt.Printf("Are you sure you want to delete label %q? (y/N): ", labelName)
var confirm string
_, _ = fmt.Scanln(&confirm)
if strings.ToLower(confirm) != "y" {
fmt.Println("Aborted")
return nil
}
_, err = client.DeleteLabel(owner, name, existing.ID)
if err != nil {
return fmt.Errorf("failed to delete label: %w", err)
}
fmt.Printf("Label deleted: %s\n", labelName)
return nil
}