mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
- **switch tea to the Cobra command tree:** `internal/app` wires the packages from parts 1–4 into explicit noun/verb commands with `--repo`, `--json` / `--jq`, `--sudo` and `--debug`; `main.go` points at it. Build, release and docs tooling (`tools/cli-docs`, Makefile, goreleaser, snap, CI) and README / CONTRIBUTING are updated. - **remove the urfave/cli command tree:** deletes `cmd/`, `modules/`, the old integration tests, contrib completion scripts and the generated `docs/CLI.md`, and drops their dependencies. This commit only deletes. ## Breaking changes This is a breaking release. It requires Gitea 28 or newer; configuration moves to separate `config.yml` and `hosts.yml` files and requires fresh authentication, and command names, aliases, flags and output formats change. `docs/migration-1.0.md` maps every old command, flag and environment variable to its replacement. ## Issues fixes #1118 fixes #1116 fixes #1115 fixes #1094 fixes #1091 fixes #1073 fixes #1052 fixes #1032 fixes #1023 fixes #966 fixes #908 fixes #833 fixes #831 fixes #817 fixes #804 fixes #779 fixes #770 fixes #737 fixes #697 fixes #726 fixes #677 fixes #672 fixes #633 fixes #632 fixes #560 fixes #556 fixes #523 fixes #516 fixes #510 fixes #504 fixes #500 fixes #482 fixes #406 fixes #403 fixes #373 fixes #351 fixes #344 fixes #326 fixes #325 fixes #323 fixes #246 fixes #240 fixes #232 fixes #197 fixes #172 fixes #141 fixes #69 fixes #1110 fixes #569 fixes #480 fixes #666 fixes #670 fixes #457 fixes #643 fixes #498 fixes #490 fixes #1069 fixes #834 fixes #602 fixes #328 fixes #417 fixes #571 fixes #1029 fixes #489 fixes #1050 fixes #162 fixes #430 fixes #1087 fixes #683 Reviewed-on: https://gitea.com/gitea/tea/pulls/1126 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
361 lines
12 KiB
Go
361 lines
12 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.dev/tea/internal/clierr"
|
|
"gitea.dev/tea/internal/services"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func (a *application) labelCommand() *cobra.Command {
|
|
group := &cobra.Command{Use: "label", Short: "Manage repository or organization labels"}
|
|
var org string
|
|
group.PersistentFlags().StringVar(&org, "org", "", "Manage organization labels instead of repository labels")
|
|
service := func(cmd *cobra.Command) (services.Labels, error) {
|
|
if org != "" && a.options.repo != "" {
|
|
return services.Labels{}, clierr.Wrap(clierr.Usage, errors.New("--org and --repo are mutually exclusive"))
|
|
}
|
|
t, err := a.resolve(cmd.Context(), org == "")
|
|
return services.Labels{Client: t.client, Repository: t.Repo, Organization: org}, err
|
|
}
|
|
group.AddCommand(a.labelExportCommand(service))
|
|
var limit, page int
|
|
list := &cobra.Command{Use: "list", Short: "List labels in the selected scope", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
|
s, err := service(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items, err := s.List(cmd.Context(), limit, page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeList(a, cmd, items, labelRow)
|
|
}}
|
|
pageFlags(list, &limit, &page)
|
|
group.AddCommand(list)
|
|
for _, verb := range []string{"create", "edit"} {
|
|
var name, color, description, file string
|
|
var exclusive, archived bool
|
|
command := &cobra.Command{Use: verb, Short: titleCase(verb) + " a label"}
|
|
if verb == "edit" {
|
|
command.Use += " <id>"
|
|
command.Args = argsExact(1)
|
|
} else {
|
|
command.Args = argsExact(0)
|
|
}
|
|
command.Flags().StringVar(&name, "name", "", "Label name")
|
|
command.Flags().StringVar(&color, "color", "", "Six-digit RGB color")
|
|
command.Flags().StringVar(&description, "description", "", "Label description")
|
|
command.Flags().BoolVar(&exclusive, "exclusive", false, "Only one label from this exclusive group may be applied")
|
|
command.Flags().BoolVar(&archived, "archived", false, "Archive the label so it cannot be newly applied")
|
|
if verb == "create" {
|
|
command.Flags().StringVar(&file, "file", "", "Import color/name rows with optional ;description")
|
|
}
|
|
command.RunE = func(cmd *cobra.Command, args []string) error {
|
|
opts := services.LabelOptions{}
|
|
var imported []services.LabelOptions
|
|
var id int64
|
|
if verb == "edit" {
|
|
var err error
|
|
id, err = number(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if file != "" {
|
|
if cmd.Flags().Changed("name") || cmd.Flags().Changed("color") || cmd.Flags().Changed("description") || cmd.Flags().Changed("exclusive") || cmd.Flags().Changed("archived") {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--file cannot be combined with label fields"))
|
|
}
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if imported, err = parseLabelFile(f); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if verb == "create" {
|
|
if _, err := a.require(cmd, &name, "name"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := a.require(cmd, &color, "color"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if verb == "create" || cmd.Flags().Changed("name") {
|
|
if name == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("label name cannot be empty"))
|
|
}
|
|
opts.Name = &name
|
|
}
|
|
if verb == "create" || cmd.Flags().Changed("color") {
|
|
color = strings.TrimPrefix(color, "#")
|
|
if !labelColor.MatchString(color) {
|
|
return clierr.Wrap(clierr.Usage, errors.New("--color requires six hexadecimal digits"))
|
|
}
|
|
opts.Color = &color
|
|
}
|
|
if verb == "create" || cmd.Flags().Changed("description") {
|
|
opts.Description = &description
|
|
}
|
|
if verb == "create" || cmd.Flags().Changed("exclusive") {
|
|
opts.Exclusive = &exclusive
|
|
}
|
|
if cmd.Flags().Changed("archived") {
|
|
opts.Archived = &archived
|
|
}
|
|
if opts.Name == nil && opts.Color == nil && opts.Description == nil && opts.Exclusive == nil && opts.Archived == nil {
|
|
return clierr.Wrap(clierr.Usage, errors.New("supply label fields to edit"))
|
|
}
|
|
}
|
|
s, err := service(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if file != "" {
|
|
results := []services.Label{}
|
|
var failures []error
|
|
for _, row := range imported {
|
|
item, err := s.Create(cmd.Context(), row)
|
|
if err != nil {
|
|
failures = append(failures, fmt.Errorf("label %s: %w", *row.Name, err))
|
|
continue
|
|
}
|
|
results = append(results, item)
|
|
}
|
|
return errors.Join(append(failures, writeList(a, cmd, results, labelRow))...)
|
|
}
|
|
var item services.Label
|
|
if verb == "create" {
|
|
item, err = s.Create(cmd.Context(), opts)
|
|
} else {
|
|
item, err = s.Edit(cmd.Context(), id, opts)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeItem(a, cmd, item, labelRow)
|
|
}
|
|
group.AddCommand(command)
|
|
}
|
|
group.AddCommand(&cobra.Command{Use: "delete <id>", Short: "Delete a label", Annotations: textOutput("label deletion does not return a resource"), Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
|
id, err := number(args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s, err := service(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := a.confirm(cmd, "Delete label "+args[0]); err != nil {
|
|
return err
|
|
}
|
|
return s.Delete(cmd.Context(), id)
|
|
}})
|
|
return group
|
|
}
|
|
|
|
func (a *application) milestoneCommand() *cobra.Command {
|
|
group := &cobra.Command{Use: "milestone", Short: "Manage repository milestones"}
|
|
service := func(cmd *cobra.Command) (services.Milestones, error) {
|
|
t, err := a.resolve(cmd.Context(), true)
|
|
return services.Milestones{Client: t.client, Repository: t.Repo}, err
|
|
}
|
|
var limit, page int
|
|
var state string
|
|
list := &cobra.Command{Use: "list", Short: "List milestones", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) error {
|
|
if err := oneOf("state", state, "open", "closed", "all"); err != nil {
|
|
return err
|
|
}
|
|
s, err := service(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items, err := s.List(cmd.Context(), state, limit, page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeList(a, cmd, items, milestoneRow)
|
|
}}
|
|
pageFlags(list, &limit, &page)
|
|
list.Flags().StringVar(&state, "state", "open", "open, closed, all")
|
|
group.AddCommand(list)
|
|
for _, verb := range []string{"view", "create", "edit", "delete"} {
|
|
var title, description, descriptionFile, deadline, createState string
|
|
var byName, useEditor bool
|
|
command := &cobra.Command{Use: verb, Short: titleCase(verb) + " a milestone"}
|
|
if verb == "create" {
|
|
command.Args = argsExact(0)
|
|
} else {
|
|
command.Use += " <id-or-name>"
|
|
command.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone title instead of ID")
|
|
command.Args = argsExact(1)
|
|
}
|
|
if verb == "create" || verb == "edit" {
|
|
command.Flags().StringVar(&title, "title", "", "Milestone title")
|
|
command.Flags().StringVar(&description, "description", "", "Milestone description")
|
|
command.Flags().StringVar(&descriptionFile, "description-file", "", "Read description from file, or - for stdin")
|
|
command.Flags().BoolVar(&useEditor, "editor", false, "Edit the milestone description in the configured editor")
|
|
command.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp; empty clears")
|
|
}
|
|
if verb == "create" {
|
|
command.Flags().StringVar(&createState, "state", "open", "Milestone state: open or closed")
|
|
}
|
|
command.RunE = func(cmd *cobra.Command, args []string) (runErr error) {
|
|
var editorDraft terminal.Draft
|
|
defer finishDraft(&editorDraft, &runErr)
|
|
if verb == "create" || verb == "edit" {
|
|
if err := validateTextSources(cmd, "description", "description-file"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
opts := services.MilestoneOptions{}
|
|
var id int64
|
|
if verb != "create" {
|
|
var err error
|
|
if id, err = milestoneSelector(args[0], byName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var interactiveDeadline *time.Time
|
|
descriptionChanged := false
|
|
if verb == "create" {
|
|
if err := oneOf("state", createState, "open", "closed"); err != nil {
|
|
return err
|
|
}
|
|
opts.State = &createState
|
|
snapshot, err := a.deps.Settings.Load()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
policy := a.policy(snapshot)
|
|
if !localFlagsChanged(cmd) && policy.CanPrompt(a.deps.Streams) && a.deps.MilestoneForm != nil {
|
|
values, err := a.deps.MilestoneForm.CreateMilestone(cmd.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
title, description = values.Title, values.Description
|
|
interactiveDeadline = values.Deadline
|
|
descriptionChanged = true
|
|
} else if _, err := a.require(cmd, &title, "title"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if (verb == "create" || verb == "edit") && !useEditor {
|
|
var err error
|
|
description, err = a.readTextSource(cmd, description, descriptionFile, "description", "description-file")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cmd.Flags().Changed("description") || cmd.Flags().Changed("description-file") {
|
|
descriptionChanged = true
|
|
}
|
|
}
|
|
if cmd.Flags().Changed("title") && strings.TrimSpace(title) == "" {
|
|
return clierr.Wrap(clierr.Usage, errors.New("milestone title cannot be empty"))
|
|
}
|
|
if verb == "create" || cmd.Flags().Changed("title") {
|
|
opts.Title = &title
|
|
}
|
|
if cmd.Flags().Changed("deadline") || interactiveDeadline != nil {
|
|
date := time.Time{}
|
|
if interactiveDeadline != nil {
|
|
date = *interactiveDeadline
|
|
} else if deadline != "" {
|
|
parsed, _, err := parseDeadline(deadline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
date = *parsed
|
|
}
|
|
normalized := date.Format(time.RFC3339)
|
|
opts.Deadline = &normalized
|
|
}
|
|
|
|
if verb == "edit" && opts.Title == nil && !descriptionChanged && !useEditor && opts.Deadline == nil {
|
|
return clierr.Wrap(clierr.Usage, errors.New("supply milestone fields to edit"))
|
|
}
|
|
s, err := service(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if byName {
|
|
id, err = s.ResolveName(cmd.Context(), args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if verb == "delete" {
|
|
if err := a.confirm(cmd, "Delete milestone "+args[0]); err != nil {
|
|
return err
|
|
}
|
|
if a.options.json != "" {
|
|
item, err := s.View(cmd.Context(), id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.Delete(cmd.Context(), id); err != nil {
|
|
return err
|
|
}
|
|
return writeItem(a, cmd, item, milestoneRow)
|
|
}
|
|
return s.Delete(cmd.Context(), id)
|
|
}
|
|
if useEditor {
|
|
initial := ""
|
|
if verb == "edit" {
|
|
current, viewErr := s.View(cmd.Context(), id)
|
|
if viewErr != nil {
|
|
return viewErr
|
|
}
|
|
initial = current.Description
|
|
}
|
|
editorDraft, err = a.editDraft(cmd, initial)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
description = editorDraft.Body
|
|
descriptionChanged = true
|
|
}
|
|
if verb == "create" || descriptionChanged {
|
|
opts.Description = &description
|
|
}
|
|
var item services.Milestone
|
|
if verb == "view" {
|
|
item, err = s.View(cmd.Context(), id)
|
|
} else if verb == "create" {
|
|
item, err = s.Create(cmd.Context(), opts)
|
|
} else {
|
|
item, err = s.Edit(cmd.Context(), id, opts)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeItem(a, cmd, item, milestoneRow)
|
|
}
|
|
group.AddCommand(command)
|
|
}
|
|
group.AddCommand(a.milestoneStateCommand("close", service), a.milestoneStateCommand("reopen", service))
|
|
group.AddCommand(a.milestoneIssueCommand())
|
|
return group
|
|
}
|
|
|
|
func labelRow(item services.Label) []terminal.Cell {
|
|
return []terminal.Cell{{Text: strconv.FormatInt(item.ID, 10)}, {Text: item.Name, Color: item.Color}, {Text: item.Color, Color: item.Color}, {Text: item.Description}}
|
|
}
|
|
|
|
func milestoneRow(item services.Milestone) []terminal.Cell {
|
|
return []terminal.Cell{{Text: strconv.FormatInt(item.ID, 10)}, {Text: item.Title}, {Text: item.State, Color: stateColors[item.State]}}
|
|
}
|