mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
fix(milestone): address milestones by title or ID everywhere
milestone view v2.0 failed with "invalid resource number" although its usage promised <id-or-name>, and issue create --milestone v1.0 failed with a strconv error. Every milestone argument (view, edit, delete, close, reopen, milestone issue add|remove|list) and issue create|edit -m/--milestone now accept a title or an ID: a number is an ID first, then a title; --by-name matches numeric titles only. --milestone-name stays as a hidden name of --milestone, and issue edit gains --remove-milestone (as does --milestone 0). Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
12 files changed
+213
-140
No files matched your search
@@ -151,8 +151,8 @@ func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/v1/repos/alice/project/milestones/3":
|
case "/api/v1/repos/alice/project/milestones":
|
||||||
_, _ = w.Write([]byte(`{"id":3,"title":"v1"}`))
|
writeMilestones(w, r, `[{"id":3,"title":"v1"}]`)
|
||||||
case "/api/v1/repos/alice/project/issues":
|
case "/api/v1/repos/alice/project/issues":
|
||||||
_, _ = w.Write([]byte("[" + issue(1) + "," + issue(2) + "]"))
|
_, _ = w.Write([]byte("[" + issue(1) + "," + issue(2) + "]"))
|
||||||
case "/api/v1/repos/alice/project/issues/1", "/api/v1/repos/alice/project/issues/2":
|
case "/api/v1/repos/alice/project/issues/1", "/api/v1/repos/alice/project/issues/2":
|
||||||
|
|||||||
+14
-12
@@ -20,8 +20,8 @@ func (a *application) issueEditCommand() *cobra.Command {
|
|||||||
var useEditor bool
|
var useEditor bool
|
||||||
var deadline string
|
var deadline string
|
||||||
var ref string
|
var ref string
|
||||||
var milestone int64
|
var milestone string
|
||||||
var milestoneName string
|
var removeMilestone bool
|
||||||
var metadata metadataFlags
|
var metadata metadataFlags
|
||||||
cmd := &cobra.Command{Use: "edit <number>...", Short: "Edit issue content, assignees, and milestone", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
|
cmd := &cobra.Command{Use: "edit <number>...", Short: "Edit issue content, assignees, and milestone", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) (runErr error) {
|
||||||
var draft terminal.Draft
|
var draft terminal.Draft
|
||||||
@@ -46,10 +46,14 @@ func (a *application) issueEditCommand() *cobra.Command {
|
|||||||
if useEditor && len(numbers) != 1 {
|
if useEditor && len(numbers) != 1 {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("--editor requires one issue"))
|
return clierr.Wrap(clierr.Usage, errors.New("--editor requires one issue"))
|
||||||
}
|
}
|
||||||
if err := validateMilestoneFlags(cmd, milestoneName, milestone); err != nil {
|
milestone, removeMilestone, err := milestoneFlag(cmd, milestone)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts := services.IssueEdit{}
|
opts := services.IssueEdit{}
|
||||||
|
if removeMilestone {
|
||||||
|
opts.Milestone = new(int64)
|
||||||
|
}
|
||||||
if cmd.Flags().Changed("ref") {
|
if cmd.Flags().Changed("ref") {
|
||||||
opts.Ref = &ref
|
opts.Ref = &ref
|
||||||
}
|
}
|
||||||
@@ -72,25 +76,22 @@ func (a *application) issueEditCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
opts.Body = &body
|
opts.Body = &body
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("milestone") {
|
|
||||||
opts.Milestone = &milestone
|
|
||||||
}
|
|
||||||
if err := metadata.validate(cmd); err != nil {
|
if err := metadata.validate(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts == (services.IssueEdit{}) && !useEditor && !metadata.changed(cmd) && !cmd.Flags().Changed("milestone-name") {
|
if opts == (services.IssueEdit{}) && !useEditor && !metadata.changed(cmd) && milestone == "" {
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("supply an edit flag; see tea issue edit --help"))
|
return clierr.Wrap(clierr.Usage, errors.New("supply an edit flag; see tea issue edit --help"))
|
||||||
}
|
}
|
||||||
target, err := a.resolve(cmd.Context(), true)
|
target, err := a.resolve(cmd.Context(), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("milestone-name") {
|
if milestone != "" {
|
||||||
milestone, err = (services.Milestones{Client: target.client, Repository: target.Repo}).ResolveName(cmd.Context(), milestoneName)
|
id, err := milestoneID(cmd, target, milestone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.Milestone = &milestone
|
opts.Milestone = &id
|
||||||
}
|
}
|
||||||
if opts.Labels, opts.Assignees, opts.Changes, err = metadata.resolve(cmd, target); err != nil {
|
if opts.Labels, opts.Assignees, opts.Changes, err = metadata.resolve(cmd, target); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -117,8 +118,9 @@ func (a *application) issueEditCommand() *cobra.Command {
|
|||||||
cmd.Flags().StringVar(&body, "body", "", "Issue body (empty clears)")
|
cmd.Flags().StringVar(&body, "body", "", "Issue body (empty clears)")
|
||||||
cmd.Flags().StringVar(&file, "body-file", "", "Read body from file, or - for stdin")
|
cmd.Flags().StringVar(&file, "body-file", "", "Read body from file, or - for stdin")
|
||||||
cmd.Flags().BoolVar(&useEditor, "editor", false, "Edit the current issue body in the configured editor")
|
cmd.Flags().BoolVar(&useEditor, "editor", false, "Edit the current issue body in the configured editor")
|
||||||
cmd.Flags().StringVar(&milestoneName, "milestone-name", "", "Milestone by exact title; conflicts with --milestone")
|
cmd.Flags().StringVarP(&milestone, "milestone", "m", "", "Move the issue to a milestone by `title` or ID")
|
||||||
cmd.Flags().Int64Var(&milestone, "milestone", 0, "Milestone ID; 0 clears")
|
cmd.Flags().BoolVar(&removeMilestone, "remove-milestone", false, "Remove the issue from its milestone")
|
||||||
|
flagAliases(cmd, map[string]string{"milestone-name": "milestone"})
|
||||||
metadata.register(cmd)
|
metadata.register(cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import (
|
|||||||
func TestIssueCreationMetadataAndReferenceClearing(t *testing.T) {
|
func TestIssueCreationMetadataAndReferenceClearing(t *testing.T) {
|
||||||
calls := 0
|
calls := 0
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/api/v1/repos/owner/repo/milestones" {
|
||||||
|
writeMilestones(w, r, `[{"id":4,"title":"Release"}]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
calls++
|
calls++
|
||||||
if calls == 1 {
|
if calls == 1 {
|
||||||
require.Equal(t, http.MethodPost, r.Method)
|
require.Equal(t, http.MethodPost, r.Method)
|
||||||
|
|||||||
+34
-10
@@ -80,7 +80,31 @@ func issueTemplate(ctx context.Context, service services.Issues, name string) (s
|
|||||||
return services.IssueTemplate{}, fmt.Errorf("issue template %q does not exist; available: %s", name, strings.Join(names, ", "))
|
return services.IssueTemplate{}, fmt.Errorf("issue template %q does not exist; available: %s", name, strings.Join(names, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateMilestoneFlags checks --milestone and --milestone-name before any request.
|
// milestoneFlag reads -m/--milestone, a milestone title or ID, before any request: value is
|
||||||
|
// "" when the flag is absent, and remove reports 0 or --remove-milestone, which clear it.
|
||||||
|
func milestoneFlag(cmd *cobra.Command, value string) (selected string, remove bool, err error) {
|
||||||
|
remove, _ = cmd.Flags().GetBool("remove-milestone")
|
||||||
|
switch {
|
||||||
|
case !cmd.Flags().Changed("milestone"):
|
||||||
|
return "", remove, nil
|
||||||
|
case remove:
|
||||||
|
return "", false, clierr.Wrap(clierr.Usage, errors.New("--milestone and --remove-milestone cannot be combined"))
|
||||||
|
case strings.TrimSpace(value) == "":
|
||||||
|
return "", false, clierr.Wrap(clierr.Usage, errors.New("--milestone needs a title or ID; use --remove-milestone to clear it"))
|
||||||
|
case strings.TrimSpace(value) == "0":
|
||||||
|
return "", true, nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(value), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// milestoneID resolves a milestone title or ID in the target repository.
|
||||||
|
func milestoneID(cmd *cobra.Command, t target, raw string) (int64, error) {
|
||||||
|
milestone, err := (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, false)
|
||||||
|
return milestone.ID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateMilestoneFlags checks the --milestone ID and --milestone-name flags of pr create and pr
|
||||||
|
// edit before any request.
|
||||||
func validateMilestoneFlags(cmd *cobra.Command, name string, id int64) error {
|
func validateMilestoneFlags(cmd *cobra.Command, name string, id int64) error {
|
||||||
if cmd.Flags().Changed("milestone-name") {
|
if cmd.Flags().Changed("milestone-name") {
|
||||||
if cmd.Flags().Changed("milestone") {
|
if cmd.Flags().Changed("milestone") {
|
||||||
@@ -301,15 +325,15 @@ func (a *application) issueCommand() *cobra.Command {
|
|||||||
var useEditor bool
|
var useEditor bool
|
||||||
var ref string
|
var ref string
|
||||||
var assignees, labelNames, attachmentPaths []string
|
var assignees, labelNames, attachmentPaths []string
|
||||||
var milestone int64
|
var milestone string
|
||||||
var milestoneName string
|
|
||||||
create := &cobra.Command{Use: "create", Short: "Create a repository issue", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) {
|
create := &cobra.Command{Use: "create", Short: "Create a repository issue", Args: argsExact(0), RunE: func(cmd *cobra.Command, _ []string) (runErr error) {
|
||||||
var draft terminal.Draft
|
var draft terminal.Draft
|
||||||
defer finishDraft(&draft, &runErr)
|
defer finishDraft(&draft, &runErr)
|
||||||
if err := validateBodySources(cmd); err != nil {
|
if err := validateBodySources(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := validateMilestoneFlags(cmd, milestoneName, milestone); err != nil {
|
milestone, _, err := milestoneFlag(cmd, milestone)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := requireNames("label", "", labelNames); err != nil {
|
if err := requireNames("label", "", labelNames); err != nil {
|
||||||
@@ -381,9 +405,9 @@ func (a *application) issueCommand() *cobra.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cmd.Flags().Changed("milestone-name") {
|
var milestoneValue int64
|
||||||
milestone, err = (services.Milestones{Client: target.client, Repository: target.Repo}).ResolveName(cmd.Context(), milestoneName)
|
if milestone != "" {
|
||||||
if err != nil {
|
if milestoneValue, err = milestoneID(cmd, target, milestone); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +421,7 @@ func (a *application) issueCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
service := services.Issues{Client: target.client, Repository: target.Repo}
|
service := services.Issues{Client: target.client, Repository: target.Repo}
|
||||||
item, err := service.Create(cmd.Context(), services.IssueOptions{Title: title, Body: body, Deadline: due, Ref: ref, Assignees: assignees, Milestone: milestone, Labels: labelIDs})
|
item, err := service.Create(cmd.Context(), services.IssueOptions{Title: title, Body: body, Deadline: due, Ref: ref, Assignees: assignees, Milestone: milestoneValue, Labels: labelIDs})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -416,8 +440,8 @@ func (a *application) issueCommand() *cobra.Command {
|
|||||||
create.Flags().StringVar(&ref, "ref", "", "Referenced branch or tag")
|
create.Flags().StringVar(&ref, "ref", "", "Referenced branch or tag")
|
||||||
create.Flags().StringSliceVar(&labelNames, "label", nil, "Apply labels by exact name; use repo: or org: to disambiguate")
|
create.Flags().StringSliceVar(&labelNames, "label", nil, "Apply labels by exact name; use repo: or org: to disambiguate")
|
||||||
create.Flags().StringSliceVar(&assignees, "assignee", nil, "Assign users by username")
|
create.Flags().StringSliceVar(&assignees, "assignee", nil, "Assign users by username")
|
||||||
create.Flags().StringVar(&milestoneName, "milestone-name", "", "Milestone by exact title; conflicts with --milestone")
|
create.Flags().StringVarP(&milestone, "milestone", "m", "", "Add the issue to a milestone by `title` or ID")
|
||||||
create.Flags().Int64Var(&milestone, "milestone", 0, "Milestone ID")
|
flagAliases(create, map[string]string{"milestone-name": "milestone"})
|
||||||
create.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp")
|
create.Flags().StringVar(&deadline, "deadline", "", "Due date: YYYY-MM-DD, RFC3339, or Unix timestamp")
|
||||||
create.Flags().StringVar(&body, "body", "", "Issue body")
|
create.Flags().StringVar(&body, "body", "", "Issue body")
|
||||||
create.Flags().StringVar(&bodyFile, "body-file", "", "Read body from a file, or - for stdin")
|
create.Flags().StringVar(&bodyFile, "body-file", "", "Read body from a file, or - for stdin")
|
||||||
|
|||||||
+13
-28
@@ -205,8 +205,8 @@ func (a *application) milestoneCommand() *cobra.Command {
|
|||||||
if verb == "create" {
|
if verb == "create" {
|
||||||
command.Args = argsExact(0)
|
command.Args = argsExact(0)
|
||||||
} else {
|
} else {
|
||||||
command.Use += " <id-or-name>"
|
command.Use += " <milestone>"
|
||||||
command.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone title instead of ID")
|
command.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
|
||||||
command.Args = argsExact(1)
|
command.Args = argsExact(1)
|
||||||
}
|
}
|
||||||
if verb == "create" || verb == "edit" {
|
if verb == "create" || verb == "edit" {
|
||||||
@@ -228,12 +228,8 @@ func (a *application) milestoneCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
opts := services.MilestoneOptions{}
|
opts := services.MilestoneOptions{}
|
||||||
var id int64
|
if verb != "create" && strings.TrimSpace(args[0]) == "" {
|
||||||
if verb != "create" {
|
return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
|
||||||
var err error
|
|
||||||
if id, err = milestoneSelector(args[0], byName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var interactiveDeadline *time.Time
|
var interactiveDeadline *time.Time
|
||||||
descriptionChanged := false
|
descriptionChanged := false
|
||||||
@@ -297,38 +293,27 @@ func (a *application) milestoneCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if byName {
|
var current services.Milestone
|
||||||
id, err = s.ResolveName(cmd.Context(), args[0])
|
if verb != "create" {
|
||||||
if err != nil {
|
if current, err = s.FindOne(cmd.Context(), args[0], byName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
id := current.ID
|
||||||
if verb == "delete" {
|
if verb == "delete" {
|
||||||
if err := a.confirm(cmd, "Delete milestone "+args[0]); err != nil {
|
if err := a.confirm(cmd, "Delete milestone "+args[0]); err != nil {
|
||||||
return err
|
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 {
|
if err := s.Delete(cmd.Context(), id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeItem(a, cmd, item, milestoneRow)
|
if a.options.json != "" {
|
||||||
|
return writeItem(a, cmd, current, milestoneRow)
|
||||||
}
|
}
|
||||||
return s.Delete(cmd.Context(), id)
|
return nil
|
||||||
}
|
}
|
||||||
if useEditor {
|
if useEditor {
|
||||||
initial := ""
|
editorDraft, err = a.editDraft(cmd, current.Description)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +325,7 @@ func (a *application) milestoneCommand() *cobra.Command {
|
|||||||
}
|
}
|
||||||
var item services.Milestone
|
var item services.Milestone
|
||||||
if verb == "view" {
|
if verb == "view" {
|
||||||
item, err = s.View(cmd.Context(), id)
|
item = current
|
||||||
} else if verb == "create" {
|
} else if verb == "create" {
|
||||||
item, err = s.Create(cmd.Context(), opts)
|
item, err = s.Create(cmd.Context(), opts)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -17,9 +17,13 @@ import (
|
|||||||
func TestBulkMilestoneStateContinuesAfterFailureAndKeepsJSONClean(t *testing.T) {
|
func TestBulkMilestoneStateContinuesAfterFailureAndKeepsJSONClean(t *testing.T) {
|
||||||
mutations := []string{}
|
mutations := []string{}
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/api/v1/repos/alice/project/milestones" {
|
||||||
|
writeMilestones(w, r, `[{"id":9,"title":"v9"},{"id":10,"title":"v10"},{"id":11,"title":"v11"}]`)
|
||||||
|
return
|
||||||
|
}
|
||||||
id := strings.TrimPrefix(r.URL.Path, "/api/v1/repos/alice/project/milestones/")
|
id := strings.TrimPrefix(r.URL.Path, "/api/v1/repos/alice/project/milestones/")
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
_, _ = fmt.Fprintf(w, `{"id":%s,"title":"Milestone %s"}`, id, id)
|
_, _ = fmt.Fprintf(w, `{"id":%s,"title":"v%s"}`, id, id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mutations = append(mutations, id)
|
mutations = append(mutations, id)
|
||||||
@@ -32,7 +36,7 @@ func TestBulkMilestoneStateContinuesAfterFailureAndKeepsJSONClean(t *testing.T)
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
deps, out, diagnostics := serverDeps(t, server.URL)
|
deps, out, diagnostics := serverDeps(t, server.URL)
|
||||||
err := Execute(context.Background(), deps, []string{"milestone", "close", "9", "10", "11", "-R", "alice/project", "--json=id,state"})
|
err := Execute(context.Background(), deps, []string{"milestone", "close", "9", "10", "11", "-R", "alice/project", "--json=id,state"})
|
||||||
require.ErrorContains(t, err, "milestone 10")
|
require.ErrorContains(t, err, "milestone v10")
|
||||||
require.Equal(t, clierr.Failure, clierr.ExitCode(err))
|
require.Equal(t, clierr.Failure, clierr.ExitCode(err))
|
||||||
require.Equal(t, []string{"9", "10", "11"}, mutations)
|
require.Equal(t, []string{"9", "10", "11"}, mutations)
|
||||||
require.JSONEq(t, `[{"id":9,"state":"closed"},{"id":11,"state":"closed"}]`, out.String())
|
require.JSONEq(t, `[{"id":9,"state":"closed"},{"id":11,"state":"closed"}]`, out.String())
|
||||||
@@ -40,7 +44,7 @@ func TestBulkMilestoneStateContinuesAfterFailureAndKeepsJSONClean(t *testing.T)
|
|||||||
mutations = nil
|
mutations = nil
|
||||||
out.Reset()
|
out.Reset()
|
||||||
err = Execute(context.Background(), deps, []string{"milestone", "close", "9", "bad", "-R", "alice/project"})
|
err = Execute(context.Background(), deps, []string{"milestone", "close", "9", "bad", "-R", "alice/project"})
|
||||||
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
|
require.ErrorContains(t, err, `milestone "bad" not found in alice/project`)
|
||||||
require.Empty(t, mutations)
|
require.Empty(t, mutations)
|
||||||
require.Empty(t, out.String())
|
require.Empty(t, out.String())
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,8 @@ func TestMilestoneCreateDescriptionFileAndEditor(t *testing.T) {
|
|||||||
func TestMilestoneEditEditorSeedsDescription(t *testing.T) {
|
func TestMilestoneEditEditorSeedsDescription(t *testing.T) {
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch {
|
switch {
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project/milestones":
|
||||||
|
writeMilestones(w, r, `[{"id":4,"title":"Ship","description":"current notes","state":"open"}]`)
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project/milestones/4":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/repos/alice/project/milestones/4":
|
||||||
_, _ = w.Write([]byte(`{"id":4,"title":"Ship","description":"current notes","state":"open"}`))
|
_, _ = w.Write([]byte(`{"id":4,"title":"Ship","description":"current notes","state":"open"}`))
|
||||||
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/repos/alice/project/milestones/4":
|
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/repos/alice/project/milestones/4":
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,35 +13,19 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// milestoneSelector validates a milestone ID or, with byName, a title before any request.
|
// findMilestone resolves a milestone argument, a title or an ID, in the target repository.
|
||||||
func milestoneSelector(selector string, byName bool) (int64, error) {
|
func findMilestone(cmd *cobra.Command, t target, raw string, byName bool) (services.Milestone, error) {
|
||||||
if byName {
|
if strings.TrimSpace(raw) == "" {
|
||||||
if strings.TrimSpace(selector) == "" {
|
return services.Milestone{}, clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
|
||||||
return 0, clierr.Wrap(clierr.Usage, errors.New("milestone name cannot be empty"))
|
|
||||||
}
|
}
|
||||||
return 0, nil
|
return (services.Milestones{Client: t.client, Repository: t.Repo}).FindOne(cmd.Context(), raw, byName)
|
||||||
}
|
|
||||||
return number(selector)
|
|
||||||
}
|
|
||||||
|
|
||||||
// existingMilestone resolves a title, or confirms that an ID exists.
|
|
||||||
func existingMilestone(ctx context.Context, milestones services.Milestones, selector string, id int64, byName bool) (int64, error) {
|
|
||||||
if byName {
|
|
||||||
return milestones.ResolveName(ctx, selector)
|
|
||||||
}
|
|
||||||
_, err := milestones.View(ctx, id)
|
|
||||||
return id, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *application) milestoneIssueCommand() *cobra.Command {
|
func (a *application) milestoneIssueCommand() *cobra.Command {
|
||||||
group := &cobra.Command{Use: "issue", Short: "Manage milestone issues and pull requests"}
|
group := &cobra.Command{Use: "issue", Short: "Manage milestone issues and pull requests"}
|
||||||
opts := services.ListOptions{}
|
opts := services.ListOptions{}
|
||||||
var byName bool
|
var byName bool
|
||||||
list := &cobra.Command{Use: "list <id-or-name>", Short: "List issues and pull requests in a milestone", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
list := &cobra.Command{Use: "list <milestone>", Short: "List issues and pull requests in a milestone", Args: argsExact(1), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
id, err := milestoneSelector(args[0], byName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := oneOf("state", opts.State, "open", "closed", "all"); err != nil {
|
if err := oneOf("state", opts.State, "open", "closed", "all"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -53,11 +36,11 @@ func (a *application) milestoneIssueCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
id, err = existingMilestone(cmd.Context(), services.Milestones{Client: target.client, Repository: target.Repo}, args[0], id, byName)
|
milestone, err := findMilestone(cmd, target, args[0], byName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
opts.Milestones = strconv.FormatInt(id, 10)
|
opts.Milestones = strconv.FormatInt(milestone.ID, 10)
|
||||||
items, more, err := (services.Issues{Client: target.client, Repository: target.Repo}).List(cmd.Context(), opts)
|
items, more, err := (services.Issues{Client: target.client, Repository: target.Repo}).List(cmd.Context(), opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -70,15 +53,11 @@ func (a *application) milestoneIssueCommand() *cobra.Command {
|
|||||||
pageFlags(list, &opts.Limit, &opts.Page)
|
pageFlags(list, &opts.Limit, &opts.Page)
|
||||||
list.Flags().StringVar(&opts.State, "state", "open", "open, closed, or all")
|
list.Flags().StringVar(&opts.State, "state", "open", "open, closed, or all")
|
||||||
list.Flags().StringVar(&opts.Kind, "kind", "all", "all, issues, or pulls")
|
list.Flags().StringVar(&opts.Kind, "kind", "all", "all, issues, or pulls")
|
||||||
list.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone title instead of ID")
|
list.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
|
||||||
group.AddCommand(list)
|
group.AddCommand(list)
|
||||||
for _, verb := range []string{"add", "remove"} {
|
for _, verb := range []string{"add", "remove"} {
|
||||||
var byName bool
|
var byName bool
|
||||||
command := &cobra.Command{Use: verb + " <id-or-name> <number>...", Short: titleCase(verb) + " issues or pull requests in a milestone", Args: usageArgs(cobra.MinimumNArgs(2)), RunE: func(cmd *cobra.Command, args []string) error {
|
command := &cobra.Command{Use: verb + " <milestone> <number>...", Short: titleCase(verb) + " issues or pull requests in a milestone", Args: usageArgs(cobra.MinimumNArgs(2)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
id, err := milestoneSelector(args[0], byName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
numbers, err := resourceNumbers(args[1:])
|
numbers, err := resourceNumbers(args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -87,10 +66,11 @@ func (a *application) milestoneIssueCommand() *cobra.Command {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
id, err = existingMilestone(cmd.Context(), services.Milestones{Client: target.client, Repository: target.Repo}, args[0], id, byName)
|
milestone, err := findMilestone(cmd, target, args[0], byName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
id := milestone.ID
|
||||||
service := services.Issues{Client: target.client, Repository: target.Repo}
|
service := services.Issues{Client: target.client, Repository: target.Repo}
|
||||||
return batch(a, cmd, numbers, issueLabel, func(n int64) (services.Issue, error) {
|
return batch(a, cmd, numbers, issueLabel, func(n int64) (services.Issue, error) {
|
||||||
if verb == "remove" {
|
if verb == "remove" {
|
||||||
@@ -99,7 +79,7 @@ func (a *application) milestoneIssueCommand() *cobra.Command {
|
|||||||
return service.Edit(cmd.Context(), n, services.IssueEdit{Milestone: &id})
|
return service.Edit(cmd.Context(), n, services.IssueEdit{Milestone: &id})
|
||||||
}, issueRow)
|
}, issueRow)
|
||||||
}}
|
}}
|
||||||
command.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone title instead of ID")
|
command.Flags().BoolVar(&byName, "by-name", false, "Match a numeric argument as a milestone title rather than an ID")
|
||||||
group.AddCommand(command)
|
group.AddCommand(command)
|
||||||
}
|
}
|
||||||
return group
|
return group
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -58,12 +59,13 @@ func TestMilestoneDeleteReturnsJSONSnapshotOnlyAfterSuccess(t *testing.T) {
|
|||||||
calls := []string{}
|
calls := []string{}
|
||||||
fail := false
|
fail := false
|
||||||
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.Equal(t, "/api/v1/repos/alice/project/milestones/9", r.URL.Path)
|
|
||||||
calls = append(calls, r.Method)
|
calls = append(calls, r.Method)
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
_, _ = w.Write([]byte(`{"id":9,"title":"Release one","state":"open"}`))
|
require.Equal(t, "/api/v1/repos/alice/project/milestones", r.URL.Path)
|
||||||
|
writeMilestones(w, r, `[{"id":9,"title":"Release one","state":"open"}]`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
require.Equal(t, "/api/v1/repos/alice/project/milestones/9", r.URL.Path)
|
||||||
require.Equal(t, http.MethodDelete, r.Method)
|
require.Equal(t, http.MethodDelete, r.Method)
|
||||||
if fail {
|
if fail {
|
||||||
w.WriteHeader(403)
|
w.WriteHeader(403)
|
||||||
@@ -76,7 +78,7 @@ func TestMilestoneDeleteReturnsJSONSnapshotOnlyAfterSuccess(t *testing.T) {
|
|||||||
args := []string{"milestone", "delete", "9", "-R", "alice/project", "--yes", "--json=id,title"}
|
args := []string{"milestone", "delete", "9", "-R", "alice/project", "--yes", "--json=id,title"}
|
||||||
require.NoError(t, Execute(context.Background(), deps, args))
|
require.NoError(t, Execute(context.Background(), deps, args))
|
||||||
require.JSONEq(t, `{"id":9,"title":"Release one"}`, out.String())
|
require.JSONEq(t, `{"id":9,"title":"Release one"}`, out.String())
|
||||||
require.Equal(t, []string{"GET", "DELETE"}, calls)
|
require.Equal(t, []string{"GET", "GET", "DELETE"}, calls)
|
||||||
fail = true
|
fail = true
|
||||||
out.Reset()
|
out.Reset()
|
||||||
require.Error(t, Execute(context.Background(), deps, args))
|
require.Error(t, Execute(context.Background(), deps, args))
|
||||||
@@ -113,5 +115,77 @@ func TestMilestoneIssueCommandsSelectByName(t *testing.T) {
|
|||||||
require.NoError(t, Execute(context.Background(), deps, []string{"milestone", "issue", "add", "Release one", "3", "--by-name", "-R", "alice/project", "--json=number"}))
|
require.NoError(t, Execute(context.Background(), deps, []string{"milestone", "issue", "add", "Release one", "3", "--by-name", "-R", "alice/project", "--json=number"}))
|
||||||
require.NotContains(t, calls, "GET /api/v1/repos/alice/project/milestones/9")
|
require.NotContains(t, calls, "GET /api/v1/repos/alice/project/milestones/9")
|
||||||
err := Execute(context.Background(), deps, []string{"milestone", "issue", "remove", " ", "3", "--by-name", "-R", "alice/project"})
|
err := Execute(context.Background(), deps, []string{"milestone", "issue", "remove", " ", "3", "--by-name", "-R", "alice/project"})
|
||||||
require.ErrorContains(t, err, "milestone name cannot be empty")
|
require.ErrorContains(t, err, "milestone title or ID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMilestones answers a milestone listing with page, a JSON array, as its first and only page.
|
||||||
|
func writeMilestones(w http.ResponseWriter, r *http.Request, page string) {
|
||||||
|
if r.URL.Query().Get("page") != "1" {
|
||||||
|
page = `[]`
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
// payloadField returns the JSON request body's field name as text.
|
||||||
|
func payloadField(t *testing.T, r *http.Request, name string) string {
|
||||||
|
t.Helper()
|
||||||
|
var payload map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))
|
||||||
|
return fmt.Sprint(payload[name])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMilestonesAcceptTitlesOrIDs(t *testing.T) {
|
||||||
|
var calls []string
|
||||||
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method + " " + r.URL.Path {
|
||||||
|
case "GET /api/v1/repos/alice/project/milestones":
|
||||||
|
writeMilestones(w, r, `[{"id":3,"title":"v1.0","state":"open"},{"id":4,"title":"2026","state":"closed"},{"id":5,"title":"v2.0"}]`)
|
||||||
|
case "GET /api/v1/repos/alice/project/milestones/3", "GET /api/v1/repos/alice/project/milestones/4":
|
||||||
|
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0","state":"open"}`))
|
||||||
|
case "PATCH /api/v1/repos/alice/project/milestones/3", "PATCH /api/v1/repos/alice/project/milestones/4":
|
||||||
|
calls = append(calls, r.Method+" "+r.URL.Path)
|
||||||
|
_, _ = w.Write([]byte(`{"id":3,"title":"v1.0","state":"closed"}`))
|
||||||
|
case "POST /api/v1/repos/alice/project/issues":
|
||||||
|
requirePayload(t, `{"title":"x","milestone":3}`, r.Body)
|
||||||
|
calls = append(calls, r.Method+" "+r.URL.Path)
|
||||||
|
_, _ = w.Write([]byte(`{"number":8}`))
|
||||||
|
case "PATCH /api/v1/repos/alice/project/issues/8":
|
||||||
|
calls = append(calls, r.Method+" "+r.URL.Path+" milestone="+payloadField(t, r, "milestone"))
|
||||||
|
_, _ = w.Write([]byte(`{"number":8}`))
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
deps, out, _ := serverDeps(t, server.URL)
|
||||||
|
run := func(args ...string) error {
|
||||||
|
return Execute(context.Background(), deps, append(args, "-R", "alice/project", "--json", "--yes"))
|
||||||
|
}
|
||||||
|
require.NoError(t, run("milestone", "view", "v1.0"))
|
||||||
|
require.Contains(t, out.String(), `"id":3`)
|
||||||
|
require.NoError(t, run("milestone", "close", "V1.0", "2026"))
|
||||||
|
require.NoError(t, run("issue", "create", "--title", "x", "-m", "v1.0"))
|
||||||
|
require.NoError(t, run("issue", "edit", "8", "--milestone-name", "3"))
|
||||||
|
require.NoError(t, run("issue", "edit", "8", "--remove-milestone"))
|
||||||
|
require.NoError(t, run("issue", "edit", "8", "--milestone", "0"))
|
||||||
|
require.Equal(t, []string{
|
||||||
|
"PATCH /api/v1/repos/alice/project/milestones/3",
|
||||||
|
"PATCH /api/v1/repos/alice/project/milestones/4",
|
||||||
|
"POST /api/v1/repos/alice/project/issues",
|
||||||
|
"PATCH /api/v1/repos/alice/project/issues/8 milestone=3",
|
||||||
|
"PATCH /api/v1/repos/alice/project/issues/8 milestone=0",
|
||||||
|
"PATCH /api/v1/repos/alice/project/issues/8 milestone=0",
|
||||||
|
}, calls)
|
||||||
|
for _, test := range []struct {
|
||||||
|
args []string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{[]string{"milestone", "view", "v9"}, `milestone "v9" not found in alice/project`},
|
||||||
|
{[]string{"milestone", "close", "v1.0", "3"}, "milestone 3 is selected more than once"},
|
||||||
|
{[]string{"issue", "create", "--title", "x", "--milestone", "nope"}, `milestone "nope" not found in alice/project`},
|
||||||
|
} {
|
||||||
|
require.ErrorContains(t, run(test.args...), test.want)
|
||||||
|
}
|
||||||
|
require.Len(t, calls, 6)
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,8 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/tea/internal/clierr"
|
"gitea.dev/tea/internal/clierr"
|
||||||
"gitea.dev/tea/internal/services"
|
"gitea.dev/tea/internal/services"
|
||||||
@@ -13,54 +15,31 @@ import (
|
|||||||
|
|
||||||
func (a *application) milestoneStateCommand(verb string, service func(*cobra.Command) (services.Milestones, error)) *cobra.Command {
|
func (a *application) milestoneStateCommand(verb string, service func(*cobra.Command) (services.Milestones, error)) *cobra.Command {
|
||||||
var byName bool
|
var byName bool
|
||||||
cmd := &cobra.Command{Use: verb + " <id-or-name>...", Short: titleCase(verb) + " one or more milestones", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
cmd := &cobra.Command{Use: verb + " <milestone>...", Short: titleCase(verb) + " one or more milestones", Args: usageArgs(cobra.MinimumNArgs(1)), RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
ids := make(map[string]int64, len(args))
|
if slices.ContainsFunc(args, func(arg string) bool { return strings.TrimSpace(arg) == "" }) {
|
||||||
seenIDs := map[int64]bool{}
|
return clierr.Wrap(clierr.Usage, errors.New("milestone title or ID cannot be empty"))
|
||||||
selectID := func(argument string, id int64) error {
|
|
||||||
if seenIDs[id] {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("selectors resolve to the same milestone"))
|
|
||||||
}
|
|
||||||
seenIDs[id], ids[argument] = true, id
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, argument := range args {
|
|
||||||
if _, seen := ids[argument]; seen {
|
|
||||||
return clierr.Wrap(clierr.Usage, errors.New("milestone selectors must be unique"))
|
|
||||||
}
|
|
||||||
id, err := milestoneSelector(argument, byName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ids[argument] = 0
|
|
||||||
if !byName {
|
|
||||||
if err := selectID(argument, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
milestones, err := service(cmd)
|
milestones, err := service(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if byName {
|
selected, err := milestones.Find(cmd.Context(), args, byName)
|
||||||
for _, argument := range args {
|
|
||||||
id, err := milestones.ResolveName(cmd.Context(), argument)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := selectID(argument, id); err != nil {
|
for i, milestone := range selected {
|
||||||
return err
|
if slices.ContainsFunc(selected[:i], func(other services.Milestone) bool { return other.ID == milestone.ID }) {
|
||||||
}
|
return clierr.Wrap(clierr.Usage, errors.New("milestone "+args[i]+" is selected more than once"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state := "closed"
|
state := "closed"
|
||||||
if verb == "reopen" {
|
if verb == "reopen" {
|
||||||
state = "open"
|
state = "open"
|
||||||
}
|
}
|
||||||
return batch(a, cmd, args, func(argument string) string { return "milestone " + argument }, func(argument string) (services.Milestone, error) {
|
return batch(a, cmd, selected, func(milestone services.Milestone) string { return "milestone " + milestone.Title }, func(milestone services.Milestone) (services.Milestone, error) {
|
||||||
return milestones.Edit(cmd.Context(), ids[argument], services.MilestoneOptions{State: &state})
|
return milestones.Edit(cmd.Context(), milestone.ID, services.MilestoneOptions{State: &state})
|
||||||
}, milestoneRow)
|
}, milestoneRow)
|
||||||
}}
|
}}
|
||||||
cmd.Flags().BoolVar(&byName, "by-name", false, "Select by exact milestone titles instead of IDs")
|
cmd.Flags().BoolVar(&byName, "by-name", false, "Match numeric arguments as milestone titles rather than IDs")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@@ -51,12 +51,12 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
|
|||||||
{[]string{"issue", "create", "--no-input"}, "missing required input: supply --title"},
|
{[]string{"issue", "create", "--no-input"}, "missing required input: supply --title"},
|
||||||
{[]string{"issue", "create", "--title", "safe", "--json=unknown"}, "unknown JSON field \"unknown\""},
|
{[]string{"issue", "create", "--title", "safe", "--json=unknown"}, "unknown JSON field \"unknown\""},
|
||||||
{[]string{"issue", "create", "--title", "safe", "--json", "--jq", "["}, "invalid --jq expression"},
|
{[]string{"issue", "create", "--title", "safe", "--json", "--jq", "["}, "invalid --jq expression"},
|
||||||
{[]string{"issue", "create", "--milestone=-1"}, "--milestone must be a positive ID, or 0 for none"},
|
{[]string{"issue", "create", "--milestone="}, "--milestone needs a title or ID; use --remove-milestone to clear it"},
|
||||||
{[]string{"issue", "create", "--assignee", "alice, "}, "assignee names cannot be empty"},
|
{[]string{"issue", "create", "--assignee", "alice, "}, "assignee names cannot be empty"},
|
||||||
{[]string{"issue", "create", "--template="}, "--template cannot be empty"},
|
{[]string{"issue", "create", "--template="}, "--template cannot be empty"},
|
||||||
{[]string{"issue", "create", "--title", "x", "--attachment", "."}, ". is not a regular file"},
|
{[]string{"issue", "create", "--title", "x", "--attachment", "."}, ". is not a regular file"},
|
||||||
{[]string{"issue", "create", "--milestone-name="}, "--milestone-name cannot be empty"},
|
{[]string{"issue", "create", "--milestone-name="}, "--milestone needs a title or ID"},
|
||||||
{[]string{"issue", "create", "--milestone-name=Release", "--milestone=1"}, "--milestone and --milestone-name are mutually exclusive"},
|
{[]string{"issue", "edit", "1", "--milestone", "v1", "--remove-milestone"}, "--milestone and --remove-milestone cannot be combined"},
|
||||||
{[]string{"issue", "edit", "1"}, "supply an edit flag"},
|
{[]string{"issue", "edit", "1"}, "supply an edit flag"},
|
||||||
{[]string{"issue", "edit", "1", "--editor=false"}, "supply an edit flag"},
|
{[]string{"issue", "edit", "1", "--editor=false"}, "supply an edit flag"},
|
||||||
{[]string{"issue", "edit", "1", "--add-labels="}, "supply an edit flag"},
|
{[]string{"issue", "edit", "1", "--add-labels="}, "supply an edit flag"},
|
||||||
@@ -73,9 +73,7 @@ func TestUsageErrorsPrecedeRequests(t *testing.T) {
|
|||||||
{[]string{"label", "create", "--name", "bug", "--color", "wrong"}, "--color requires six hexadecimal digits"},
|
{[]string{"label", "create", "--name", "bug", "--color", "wrong"}, "--color requires six hexadecimal digits"},
|
||||||
{[]string{"label", "edit", "1"}, "supply label fields to edit"},
|
{[]string{"label", "edit", "1"}, "supply label fields to edit"},
|
||||||
{[]string{"label", "list", "--org", "team", "-R", "a/b"}, "--org and --repo are mutually exclusive"},
|
{[]string{"label", "list", "--org", "team", "-R", "a/b"}, "--org and --repo are mutually exclusive"},
|
||||||
{[]string{"milestone", "close", "9", "09"}, "selectors resolve to the same milestone"},
|
{[]string{"milestone", "close", " ", "--by-name"}, "milestone title or ID cannot be empty"},
|
||||||
{[]string{"milestone", "close", " ", "--by-name"}, "milestone name cannot be empty"},
|
|
||||||
{[]string{"milestone", "reopen", "v1", "v1", "--by-name"}, "milestone selectors must be unique"},
|
|
||||||
{[]string{"milestone", "create", "--title", "v1", "--deadline", "invalid"}, "invalid --deadline"},
|
{[]string{"milestone", "create", "--title", "v1", "--deadline", "invalid"}, "invalid --deadline"},
|
||||||
{[]string{"milestone", "create", "--title", "x", "--state", "merged", "--no-input"}, "--state must be open or closed"},
|
{[]string{"milestone", "create", "--title", "x", "--state", "merged", "--no-input"}, "--state must be open or closed"},
|
||||||
{[]string{"milestone", "edit", "1"}, "supply milestone fields to edit"},
|
{[]string{"milestone", "edit", "1"}, "supply milestone fields to edit"},
|
||||||
|
|||||||
@@ -40,22 +40,43 @@ func (s Milestones) All(ctx context.Context) ([]Milestone, error) {
|
|||||||
return listAll(func(page int) ([]Milestone, error) { return s.List(ctx, "all", 100, page) })
|
return listAll(func(page int) ([]Milestone, error) { return s.List(ctx, "all", 100, page) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterIDs resolves the milestones that filter an issue listing, each an ID or a title, to their
|
// Find returns the milestones that raws address among the open and closed milestones, each by ID
|
||||||
// IDs. Gitea drops filter values that name no milestone and lists every issue when none is left,
|
// or title (D10); byName matches numeric values as titles only.
|
||||||
// so an unknown milestone is an error here.
|
func (s Milestones) Find(ctx context.Context, raws []string, byName bool) ([]Milestone, error) {
|
||||||
func (s Milestones) FilterIDs(ctx context.Context, values []string) ([]string, error) {
|
|
||||||
items, err := s.All(ctx)
|
items, err := s.All(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
finder := NameOrID[Milestone]{Kind: "milestone", Scope: s.Repository, ID: func(m Milestone) int64 { return m.ID }, Name: func(m Milestone) string { return m.Title }}
|
finder := NameOrID[Milestone]{Kind: "milestone", Scope: s.Repository, ID: func(m Milestone) int64 { return m.ID }, Name: func(m Milestone) string { return m.Title }, ByName: byName}
|
||||||
ids := make([]string, len(values))
|
result := make([]Milestone, len(raws))
|
||||||
for i, value := range values {
|
for i, raw := range raws {
|
||||||
milestone, err := finder.Find(items, value)
|
if result[i], err = finder.Find(items, raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindOne returns the milestone that raw addresses like Find.
|
||||||
|
func (s Milestones) FindOne(ctx context.Context, raw string, byName bool) (Milestone, error) {
|
||||||
|
items, err := s.Find(ctx, []string{raw}, byName)
|
||||||
|
if err != nil {
|
||||||
|
return Milestone{}, err
|
||||||
|
}
|
||||||
|
return items[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterIDs resolves the milestones that filter an issue listing, each an ID or a title, to their
|
||||||
|
// IDs. Gitea drops filter values that name no milestone and lists every issue when none is left,
|
||||||
|
// so an unknown milestone is an error here.
|
||||||
|
func (s Milestones) FilterIDs(ctx context.Context, values []string) ([]string, error) {
|
||||||
|
items, err := s.Find(ctx, values, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ids[i] = strconv.FormatInt(milestone.ID, 10)
|
ids := make([]string, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
ids[i] = strconv.FormatInt(item.ID, 10)
|
||||||
}
|
}
|
||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user