From 2a8c1daa675201ecbda40db5b5bff6d0b59c9fd5 Mon Sep 17 00:00:00 2001 From: Norwin Date: Tue, 25 Oct 2022 08:40:00 +0800 Subject: [PATCH] Add `tea issue edit` (#506) fixes #229 fixes #502 interactive mode will be in a follow up Co-authored-by: Norwin Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/506 Reviewed-by: 6543 <6543@obermui.de> Reviewed-by: Lunny Xiao Co-authored-by: Norwin Co-committed-by: Norwin --- cmd/flags/issue_pr.go | 104 +++++++++++++++++++++++----- cmd/issues.go | 1 + cmd/issues/create.go | 4 +- cmd/issues/edit.go | 63 +++++++++++++++++ cmd/pulls/create.go | 4 +- modules/task/issue_edit.go | 135 +++++++++++++++++++++++++++++++++++++ modules/task/labels.go | 2 +- 7 files changed, 293 insertions(+), 20 deletions(-) create mode 100644 cmd/issues/edit.go create mode 100644 modules/task/issue_edit.go diff --git a/cmd/flags/issue_pr.go b/cmd/flags/issue_pr.go index 7f4a514..c16ccd9 100644 --- a/cmd/flags/issue_pr.go +++ b/cmd/flags/issue_pr.go @@ -7,6 +7,7 @@ package flags import ( "fmt" "strings" + "time" "code.gitea.io/sdk/gitea" "code.gitea.io/tea/modules/context" @@ -84,8 +85,8 @@ var IssueListingFlags = append([]cli.Flag{ &PaginationLimitFlag, }, AllDefaultFlags...) -// IssuePREditFlags defines flags for properties of issues and PRs -var IssuePREditFlags = append([]cli.Flag{ +// issuePRFlags defines shared flags between flags IssuePRCreateFlags and IssuePREditFlags +var issuePRFlags = append([]cli.Flag{ &cli.StringFlag{ Name: "title", Aliases: []string{"t"}, @@ -94,6 +95,25 @@ var IssuePREditFlags = append([]cli.Flag{ Name: "description", Aliases: []string{"d"}, }, + &cli.StringFlag{ + Name: "referenced-version", + Aliases: []string{"v"}, + Usage: "commit-hash or tag name to assign", + }, + &cli.StringFlag{ + Name: "milestone", + Aliases: []string{"m"}, + Usage: "Milestone to assign", + }, + &cli.StringFlag{ + Name: "deadline", + Aliases: []string{"D"}, + Usage: "Deadline timestamp to assign", + }, +}, LoginRepoFlags...) + +// IssuePRCreateFlags defines flags for creation of issues and PRs +var IssuePRCreateFlags = append([]cli.Flag{ &cli.StringFlag{ Name: "assignees", Aliases: []string{"a"}, @@ -104,20 +124,10 @@ var IssuePREditFlags = append([]cli.Flag{ Aliases: []string{"L"}, Usage: "Comma-separated list of labels to assign", }, - &cli.StringFlag{ - Name: "deadline", - Aliases: []string{"D"}, - Usage: "Deadline timestamp to assign", - }, - &cli.StringFlag{ - Name: "milestone", - Aliases: []string{"m"}, - Usage: "Milestone to assign", - }, -}, LoginRepoFlags...) +}, issuePRFlags...) -// GetIssuePREditFlags parses all IssuePREditFlags -func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) { +// GetIssuePRCreateFlags parses all IssuePREditFlags +func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, error) { opts := gitea.CreateIssueOption{ Title: ctx.String("title"), Body: ctx.String("description"), @@ -159,3 +169,67 @@ func GetIssuePREditFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, err return &opts, nil } + +// IssuePREditFlags defines flags for editing properties of issues and PRs +var IssuePREditFlags = append([]cli.Flag{ + &cli.StringFlag{ + Name: "add-assignees", + Aliases: []string{"a"}, + Usage: "Comma-separated list of usernames to assign", + }, + &cli.StringFlag{ + Name: "add-labels", + Aliases: []string{"L"}, + Usage: "Comma-separated list of labels to assign. Takes precedence over --remove-labels", + }, + &cli.StringFlag{ + Name: "remove-labels", + Usage: "Comma-separated list of labels to remove", + }, +}, issuePRFlags...) + +// GetIssuePREditFlags parses all IssuePREditFlags +func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error) { + opts := task.EditIssueOption{} + if ctx.IsSet("title") { + val := ctx.String("title") + opts.Title = &val + } + if ctx.IsSet("description") { + val := ctx.String("description") + opts.Body = &val + } + if ctx.IsSet("referenced-version") { + val := ctx.String("referenced-version") + opts.Ref = &val + } + if ctx.IsSet("milestone") { + val := ctx.String("milestone") + opts.Milestone = &val + } + if ctx.IsSet("deadline") { + date := ctx.String("deadline") + if date == "" { + opts.Deadline = &time.Time{} + } else { + t, err := dateparse.ParseAny(date) + if err != nil { + return nil, err + } + opts.Deadline = &t + } + } + if ctx.IsSet("add-assignees") { + val := ctx.String("add-assignees") + opts.AddAssignees = strings.Split(val, ",") + } + if ctx.IsSet("add-labels") { + val := ctx.String("add-labels") + opts.AddLabels = strings.Split(val, ",") + } + if ctx.IsSet("remove-labels") { + val := ctx.String("remove-labels") + opts.RemoveLabels = strings.Split(val, ",") + } + return &opts, nil +} diff --git a/cmd/issues.go b/cmd/issues.go index 3648e4f..c2856b4 100644 --- a/cmd/issues.go +++ b/cmd/issues.go @@ -28,6 +28,7 @@ var CmdIssues = cli.Command{ Subcommands: []*cli.Command{ &issues.CmdIssuesList, &issues.CmdIssuesCreate, + &issues.CmdIssuesEdit, &issues.CmdIssuesReopen, &issues.CmdIssuesClose, }, diff --git a/cmd/issues/create.go b/cmd/issues/create.go index 90d3964..b5e0a28 100644 --- a/cmd/issues/create.go +++ b/cmd/issues/create.go @@ -21,7 +21,7 @@ var CmdIssuesCreate = cli.Command{ Description: `Create an issue on repository`, ArgsUsage: " ", // command does not accept arguments Action: runIssuesCreate, - Flags: flags.IssuePREditFlags, + Flags: flags.IssuePRCreateFlags, } func runIssuesCreate(cmd *cli.Context) error { @@ -32,7 +32,7 @@ func runIssuesCreate(cmd *cli.Context) error { return interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo) } - opts, err := flags.GetIssuePREditFlags(ctx) + opts, err := flags.GetIssuePRCreateFlags(ctx) if err != nil { return err } diff --git a/cmd/issues/edit.go b/cmd/issues/edit.go new file mode 100644 index 0000000..3880352 --- /dev/null +++ b/cmd/issues/edit.go @@ -0,0 +1,63 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package issues + +import ( + "fmt" + + "code.gitea.io/tea/cmd/flags" + "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/print" + "code.gitea.io/tea/modules/task" + "code.gitea.io/tea/modules/utils" + + "github.com/urfave/cli/v2" +) + +// CmdIssuesEdit is the subcommand of issues to edit issues +var CmdIssuesEdit = cli.Command{ + Name: "edit", + Aliases: []string{"e"}, + Usage: "Edit one or more issues", + Description: `Edit one or more issues. To unset a property again, +use an empty string (eg. --milestone "").`, + ArgsUsage: " [...]", + Action: runIssuesEdit, + Flags: flags.IssuePREditFlags, +} + +func runIssuesEdit(cmd *cli.Context) error { + ctx := context.InitCommand(cmd) + ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) + + if !cmd.Args().Present() { + return fmt.Errorf("must specify at least one issue index") + } + + opts, err := flags.GetIssuePREditFlags(ctx) + if err != nil { + return err + } + + indices, err := utils.ArgsToIndices(ctx.Args().Slice()) + if err != nil { + return err + } + + client := ctx.Login.Client() + for _, opts.Index = range indices { + issue, err := task.EditIssue(ctx, client, *opts) + if err != nil { + return err + } + if ctx.Args().Len() > 1 { + fmt.Println(issue.HTMLURL) + } else { + print.IssueDetails(issue, nil) + } + } + + return nil +} diff --git a/cmd/pulls/create.go b/cmd/pulls/create.go index 524b5ff..2d2f52a 100644 --- a/cmd/pulls/create.go +++ b/cmd/pulls/create.go @@ -36,7 +36,7 @@ var CmdPullsCreate = cli.Command{ Usage: "Enable maintainers to push to the base branch of created pull", Value: true, }, - }, flags.IssuePREditFlags...), + }, flags.IssuePRCreateFlags...), } func runPullsCreate(cmd *cli.Context) error { @@ -48,7 +48,7 @@ func runPullsCreate(cmd *cli.Context) error { } // else use args to create PR - opts, err := flags.GetIssuePREditFlags(ctx) + opts, err := flags.GetIssuePRCreateFlags(ctx) if err != nil { return err } diff --git a/modules/task/issue_edit.go b/modules/task/issue_edit.go new file mode 100644 index 0000000..9741e39 --- /dev/null +++ b/modules/task/issue_edit.go @@ -0,0 +1,135 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package task + +import ( + "fmt" + "time" + + "code.gitea.io/sdk/gitea" + "code.gitea.io/tea/modules/context" +) + +// EditIssueOption wraps around gitea.EditIssueOption which has bad & incosistent semantics. +type EditIssueOption struct { + Index int64 + Title *string + Body *string + Ref *string + Milestone *string + Deadline *time.Time + AddLabels []string + RemoveLabels []string + AddAssignees []string + // RemoveAssignees []string // NOTE: with the current go-sdk, clearing assignees is not possible. +} + +// Normalizes the options into parameters that can be passed to the sdk. +// the returned value will be nil, when no change to this part of the issue is requested. +func (o EditIssueOption) toSdkOptions(ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, error) { + // labels have a separate API call, so they get their own options. + var addLabelOpts, rmLabelOpts *gitea.IssueLabelsOption + if o.AddLabels != nil && len(o.AddLabels) != 0 { + ids, err := ResolveLabelNames(client, ctx.Owner, ctx.Repo, o.AddLabels) + if err != nil { + return nil, nil, nil, err + } + addLabelOpts = &gitea.IssueLabelsOption{Labels: ids} + } + + if o.RemoveLabels != nil && len(o.RemoveLabels) != 0 { + ids, err := ResolveLabelNames(client, ctx.Owner, ctx.Repo, o.RemoveLabels) + if err != nil { + return nil, nil, nil, err + } + rmLabelOpts = &gitea.IssueLabelsOption{Labels: ids} + } + + issueOpts := gitea.EditIssueOption{} + var issueOptsDirty bool + if o.Title != nil { + issueOpts.Title = *o.Title + issueOptsDirty = true + } + if o.Body != nil { + issueOpts.Body = o.Body + issueOptsDirty = true + } + if o.Ref != nil { + issueOpts.Ref = o.Ref + issueOptsDirty = true + } + if o.Milestone != nil { + if *o.Milestone == "" { + issueOpts.Milestone = gitea.OptionalInt64(0) + } else { + ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, *o.Milestone) + if err != nil { + return nil, nil, nil, fmt.Errorf("Milestone '%s' not found", *o.Milestone) + } + issueOpts.Milestone = &ms.ID + } + issueOptsDirty = true + } + if o.Deadline != nil { + issueOpts.Deadline = o.Deadline + issueOptsDirty = true + if o.Deadline.IsZero() { + issueOpts.RemoveDeadline = gitea.OptionalBool(true) + } + } + if o.AddAssignees != nil && len(o.AddAssignees) != 0 { + issueOpts.Assignees = o.AddAssignees + issueOptsDirty = true + } + + if issueOptsDirty { + return &issueOpts, addLabelOpts, rmLabelOpts, nil + } + return nil, addLabelOpts, rmLabelOpts, nil +} + +// EditIssue edits an issue and returns the updated issue. +func EditIssue(ctx *context.TeaContext, client *gitea.Client, opts EditIssueOption) (*gitea.Issue, error) { + if client == nil { + client = ctx.Login.Client() + } + + issueOpts, addLabelOpts, rmLabelOpts, err := opts.toSdkOptions(ctx, client) + if err != nil { + return nil, err + } + + if rmLabelOpts != nil { + // NOTE: as of 1.17, there is no API to remove multiple labels at once. + for _, id := range rmLabelOpts.Labels { + _, err := client.DeleteIssueLabel(ctx.Owner, ctx.Repo, opts.Index, id) + if err != nil { + return nil, fmt.Errorf("could not remove labels: %s", err) + } + } + } + + if addLabelOpts != nil { + _, _, err := client.AddIssueLabels(ctx.Owner, ctx.Repo, opts.Index, *addLabelOpts) + if err != nil { + return nil, fmt.Errorf("could not add labels: %s", err) + } + } + + var issue *gitea.Issue + if issueOpts != nil { + issue, _, err = client.EditIssue(ctx.Owner, ctx.Repo, opts.Index, *issueOpts) + if err != nil { + return nil, fmt.Errorf("could not edit issue: %s", err) + } + } else { + issue, _, err = client.GetIssue(ctx.Owner, ctx.Repo, opts.Index) + if err != nil { + return nil, fmt.Errorf("could not get issue: %s", err) + } + } + return issue, nil +} diff --git a/modules/task/labels.go b/modules/task/labels.go index 7d24cfc..0c1f080 100644 --- a/modules/task/labels.go +++ b/modules/task/labels.go @@ -11,7 +11,7 @@ import ( // ResolveLabelNames returns a list of label IDs for a given list of label names func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) { - labelIDs := make([]int64, len(labelNames)) + labelIDs := make([]int64, 0, len(labelNames)) labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{ ListOptions: gitea.ListOptions{Page: -1}, })