Add `tea issue edit` (#506)
fixes #229 fixes #502 interactive mode will be in a follow up Co-authored-by: Norwin <git@nroo.de> 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 <xiaolunwen@gmail.com> Co-authored-by: Norwin <noerw@noreply.gitea.io> Co-committed-by: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
parent
54b535a527
commit
2a8c1daa67
|
@ -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
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ var CmdIssues = cli.Command{
|
|||
Subcommands: []*cli.Command{
|
||||
&issues.CmdIssuesList,
|
||||
&issues.CmdIssuesCreate,
|
||||
&issues.CmdIssuesEdit,
|
||||
&issues.CmdIssuesReopen,
|
||||
&issues.CmdIssuesClose,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: "<idx> [<idx>...]",
|
||||
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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue