mirror of
https://gitea.com/gitea/tea.git
synced 2026-08-05 23:07:39 +02:00
feat(assignees): add set, add, and remove assignees APIs (#1045)
Implemented set, add, and remove assignees APIs. Closes https://gitea.com/gitea/tea/issues/965 and https://gitea.com/gitea/tea/issues/966Reviewed-on: https://gitea.com/gitea/tea/pulls/1045 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Minjie Fang <wingsallen@gmail.com>
This commit is contained in:
@@ -51,7 +51,7 @@ jobs:
|
|||||||
make integration-test
|
make integration-test
|
||||||
services:
|
services:
|
||||||
gitea:
|
gitea:
|
||||||
image: docker.gitea.com/gitea:1.26.2
|
image: docker.gitea.com/gitea:1.27.0
|
||||||
cmd:
|
cmd:
|
||||||
- bash
|
- bash
|
||||||
- -c
|
- -c
|
||||||
|
|||||||
+20
-4
@@ -177,10 +177,18 @@ func GetIssuePRCreateFlags(requestCtx stdctx.Context, ctx *context.TeaContext) (
|
|||||||
|
|
||||||
// IssuePREditFlags defines flags for editing properties of issues and PRs
|
// IssuePREditFlags defines flags for editing properties of issues and PRs
|
||||||
var IssuePREditFlags = append([]cli.Flag{
|
var IssuePREditFlags = append([]cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "set-assignees",
|
||||||
|
Usage: "Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees",
|
||||||
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "add-assignees",
|
Name: "add-assignees",
|
||||||
Aliases: []string{"a"},
|
Aliases: []string{"a"},
|
||||||
Usage: "Comma-separated list of usernames to assign",
|
Usage: "Comma-separated list of usernames to assign. Takes precedence over --remove-assignees",
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "remove-assignees",
|
||||||
|
Usage: "Comma-separated list of usernames to remove",
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "add-labels",
|
Name: "add-labels",
|
||||||
@@ -224,17 +232,25 @@ func GetIssuePREditFlags(ctx *context.TeaContext) (*task.EditIssueOption, error)
|
|||||||
opts.Deadline = &t
|
opts.Deadline = &t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ctx.IsSet("set-assignees") {
|
||||||
|
val := ctx.String("set-assignees")
|
||||||
|
opts.SetAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||||
|
}
|
||||||
if ctx.IsSet("add-assignees") {
|
if ctx.IsSet("add-assignees") {
|
||||||
val := ctx.String("add-assignees")
|
val := ctx.String("add-assignees")
|
||||||
opts.AddAssignees = strings.Split(val, ",")
|
opts.AddAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||||
|
}
|
||||||
|
if ctx.IsSet("remove-assignees") {
|
||||||
|
val := ctx.String("remove-assignees")
|
||||||
|
opts.RemoveAssignees = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||||
}
|
}
|
||||||
if ctx.IsSet("add-labels") {
|
if ctx.IsSet("add-labels") {
|
||||||
val := ctx.String("add-labels")
|
val := ctx.String("add-labels")
|
||||||
opts.AddLabels = strings.Split(val, ",")
|
opts.AddLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||||
}
|
}
|
||||||
if ctx.IsSet("remove-labels") {
|
if ctx.IsSet("remove-labels") {
|
||||||
val := ctx.String("remove-labels")
|
val := ctx.String("remove-labels")
|
||||||
opts.RemoveLabels = strings.Split(val, ",")
|
opts.RemoveLabels = strings.Split(strings.ReplaceAll(val, " ", ""), ",")
|
||||||
}
|
}
|
||||||
return &opts, nil
|
return &opts, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -239,7 +239,7 @@ Create an issue on repository
|
|||||||
|
|
||||||
Edit one or more issues
|
Edit one or more issues
|
||||||
|
|
||||||
**--add-assignees, -a**="": Comma-separated list of usernames to assign
|
**--add-assignees, -a**="": Comma-separated list of usernames to assign. Takes precedence over --remove-assignees
|
||||||
|
|
||||||
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
|
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
|
||||||
|
|
||||||
@@ -255,10 +255,14 @@ Edit one or more issues
|
|||||||
|
|
||||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--remove-assignees**="": Comma-separated list of usernames to remove
|
||||||
|
|
||||||
**--remove-labels**="": Comma-separated list of labels to remove
|
**--remove-labels**="": Comma-separated list of labels to remove
|
||||||
|
|
||||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
**--set-assignees**="": Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees
|
||||||
|
|
||||||
**--title, -t**="":
|
**--title, -t**="":
|
||||||
|
|
||||||
### reopen, open
|
### reopen, open
|
||||||
@@ -423,7 +427,7 @@ Change state of one or more pull requests to 'open'
|
|||||||
|
|
||||||
Edit one or more pull requests
|
Edit one or more pull requests
|
||||||
|
|
||||||
**--add-assignees, -a**="": Comma-separated list of usernames to assign
|
**--add-assignees, -a**="": Comma-separated list of usernames to assign. Takes precedence over --remove-assignees
|
||||||
|
|
||||||
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
|
**--add-labels, -L**="": Comma-separated list of labels to assign. Takes precedence over --remove-labels
|
||||||
|
|
||||||
@@ -445,12 +449,16 @@ Edit one or more pull requests
|
|||||||
|
|
||||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||||
|
|
||||||
|
**--remove-assignees**="": Comma-separated list of usernames to remove
|
||||||
|
|
||||||
**--remove-labels**="": Comma-separated list of labels to remove
|
**--remove-labels**="": Comma-separated list of labels to remove
|
||||||
|
|
||||||
**--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers
|
**--remove-reviewers**="": Comma-separated list of usernames to remove from reviewers
|
||||||
|
|
||||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||||
|
|
||||||
|
**--set-assignees**="": Clear all existing assignees and assign comma-separated list of usernames. Takes precedence over --add-assignees and --remove-assignees
|
||||||
|
|
||||||
**--title, -t**="":
|
**--title, -t**="":
|
||||||
|
|
||||||
### review
|
### review
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
gitea "gitea.dev/sdk"
|
||||||
"gitea.dev/tea/modules/config"
|
"gitea.dev/tea/modules/config"
|
||||||
"gitea.dev/tea/modules/context"
|
"gitea.dev/tea/modules/context"
|
||||||
"gitea.dev/tea/modules/task"
|
"gitea.dev/tea/modules/task"
|
||||||
@@ -40,9 +41,10 @@ func EditIssue(requestCtx stdctx.Context, ctx context.TeaContext, index int64) (
|
|||||||
Deadline: i.Deadline,
|
Deadline: i.Deadline,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i.Assignees = cleanAssignees(i.Assignees)
|
||||||
if len(i.Assignees) != 0 {
|
if len(i.Assignees) != 0 {
|
||||||
for _, a := range i.Assignees {
|
for _, a := range i.Assignees {
|
||||||
opts.AddAssignees = append(opts.AddAssignees, a.UserName)
|
opts.SetAssignees = append(opts.SetAssignees, a.UserName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
currAssignees := o.AddAssignees
|
currAssignees := o.SetAssignees
|
||||||
newAssignees := selectables.Assignees
|
newAssignees := selectables.Assignees
|
||||||
|
|
||||||
for _, c := range currAssignees {
|
for _, c := range currAssignees {
|
||||||
@@ -119,10 +121,14 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// assignees
|
// assignees
|
||||||
if o.AddAssignees, err = promptMultiSelect("Add Assignees:", newAssignees, "[other]"); err != nil {
|
if currAssignees, err = promptMultiSelectWithPreselect("Set Assignees:", currAssignees, newAssignees, "[other]"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printTitleAndContent("Assignees:", strings.Join(o.AddAssignees, "\n"))
|
if len(currAssignees) == 0 && len(o.SetAssignees) > 0 {
|
||||||
|
o.RemoveAssignees = o.SetAssignees
|
||||||
|
}
|
||||||
|
o.SetAssignees = currAssignees
|
||||||
|
printTitleAndContent("Assignees:", strings.Join(o.SetAssignees, "\n"))
|
||||||
|
|
||||||
// milestone
|
// milestone
|
||||||
if len(selectables.MilestoneList) != 0 {
|
if len(selectables.MilestoneList) != 0 {
|
||||||
@@ -175,3 +181,13 @@ func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContex
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanAssignees(list []*gitea.User) []*gitea.User {
|
||||||
|
out := make([]*gitea.User, 0, len(list))
|
||||||
|
for _, a := range list {
|
||||||
|
if strings.TrimSpace(a.UserName) != "" {
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,10 +92,26 @@ func promptDatetime(prompt string) (val *time.Time, err error) {
|
|||||||
|
|
||||||
// promptSelect creates a generic multiselect prompt, with processing of custom values.
|
// promptSelect creates a generic multiselect prompt, with processing of custom values.
|
||||||
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
|
func promptMultiSelect(prompt string, options []string, customVal string) ([]string, error) {
|
||||||
|
opts := huh.NewOptions(makeSelectOpts(options, customVal, "")...)
|
||||||
|
return runMultiSelect(prompt, opts, customVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptMultiSelectWithPreselect creates a generic multiselect prompt with preselected values and processing of custom values.
|
||||||
|
func promptMultiSelectWithPreselect(prompt string, selected []string, options []string, customVal string) ([]string, error) {
|
||||||
|
opts := make([]huh.Option[string], 0, len(selected)+len(options)+1)
|
||||||
|
for _, name := range selected {
|
||||||
|
opts = append(opts, huh.NewOption(name, name).Selected(true))
|
||||||
|
}
|
||||||
|
opts = append(opts, huh.NewOptions(makeSelectOpts(options, customVal, "")...)...)
|
||||||
|
|
||||||
|
return runMultiSelect(prompt, opts, customVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMultiSelect(prompt string, opts []huh.Option[string], customVal string) ([]string, error) {
|
||||||
var selection []string
|
var selection []string
|
||||||
if err := huh.NewMultiSelect[string]().
|
if err := huh.NewMultiSelect[string]().
|
||||||
Title(prompt).
|
Title(prompt).
|
||||||
Options(huh.NewOptions(makeSelectOpts(options, customVal, "")...)...).
|
Options(opts...).
|
||||||
Value(&selection).
|
Value(&selection).
|
||||||
WithTheme(theme.GetTheme()).
|
WithTheme(theme.GetTheme()).
|
||||||
Run(); err != nil {
|
Run(); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdctx "context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gitea "gitea.dev/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveAssigneeOpts resolves assignee names to IssueAssigneesOption. Returns nil if names is empty.
|
||||||
|
func ResolveAssigneeOpts(names []string) *gitea.IssueAssigneesOption {
|
||||||
|
names = cleanAssignees(names)
|
||||||
|
if len(names) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gitea.IssueAssigneesOption{Assignees: names}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAssigneeChanges adds and removes assignees on an issue or pull request.
|
||||||
|
func ApplyAssigneeChanges(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, index int64, add, rm *gitea.IssueAssigneesOption) error {
|
||||||
|
if rm != nil {
|
||||||
|
_, _, err := client.Issues.DeleteIssueAssignees(requestCtx, owner, repo, index, *rm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not remove assignees: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if add != nil {
|
||||||
|
_, _, err := client.Issues.AddIssueAssignees(requestCtx, owner, repo, index, *add)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not add assignees: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanAssignees(list []string) []string {
|
||||||
|
out := make([]string, 0, len(list))
|
||||||
|
for _, a := range list {
|
||||||
|
if strings.TrimSpace(a) != "" {
|
||||||
|
out = append(out, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
+18
-10
@@ -23,23 +23,26 @@ type EditIssueOption struct {
|
|||||||
Deadline *time.Time
|
Deadline *time.Time
|
||||||
AddLabels []string
|
AddLabels []string
|
||||||
RemoveLabels []string
|
RemoveLabels []string
|
||||||
|
SetAssignees []string
|
||||||
AddAssignees []string
|
AddAssignees []string
|
||||||
|
RemoveAssignees []string
|
||||||
AddReviewers []string
|
AddReviewers []string
|
||||||
RemoveReviewers []string
|
RemoveReviewers []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.
|
// 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.
|
// the returned value will be nil, when no change to this part of the issue is requested.
|
||||||
func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, error) {
|
func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, *gitea.IssueAssigneesOption, *gitea.IssueAssigneesOption, error) {
|
||||||
addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.AddLabels)
|
addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.AddLabels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.RemoveLabels)
|
rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.RemoveLabels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
addAssigneeOpts := ResolveAssigneeOpts(o.AddAssignees)
|
||||||
|
rmAssigneeOpts := ResolveAssigneeOpts(o.RemoveAssignees)
|
||||||
|
|
||||||
issueOpts := gitea.EditIssueOption{}
|
issueOpts := gitea.EditIssueOption{}
|
||||||
var issueOptsDirty bool
|
var issueOptsDirty bool
|
||||||
@@ -58,7 +61,7 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
|
|||||||
if o.Milestone != nil {
|
if o.Milestone != nil {
|
||||||
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone)
|
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, nil, nil, err
|
||||||
}
|
}
|
||||||
issueOpts.Milestone = gitea.OptionalInt64(id)
|
issueOpts.Milestone = gitea.OptionalInt64(id)
|
||||||
issueOptsDirty = true
|
issueOptsDirty = true
|
||||||
@@ -70,15 +73,16 @@ func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.Te
|
|||||||
issueOpts.RemoveDeadline = gitea.OptionalBool(true)
|
issueOpts.RemoveDeadline = gitea.OptionalBool(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(o.AddAssignees) != 0 {
|
o.SetAssignees = cleanAssignees(o.SetAssignees)
|
||||||
issueOpts.Assignees = o.AddAssignees
|
if len(o.SetAssignees) != 0 {
|
||||||
|
issueOpts.Assignees = o.SetAssignees
|
||||||
issueOptsDirty = true
|
issueOptsDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if issueOptsDirty {
|
if issueOptsDirty {
|
||||||
return &issueOpts, addLabelOpts, rmLabelOpts, nil
|
return &issueOpts, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, nil
|
||||||
}
|
}
|
||||||
return nil, addLabelOpts, rmLabelOpts, nil
|
return nil, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditIssue edits an issue and returns the updated issue.
|
// EditIssue edits an issue and returns the updated issue.
|
||||||
@@ -87,7 +91,7 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
|
|||||||
client = ctx.Login.Client()
|
client = ctx.Login.Client()
|
||||||
}
|
}
|
||||||
|
|
||||||
issueOpts, addLabelOpts, rmLabelOpts, err := opts.toSdkOptions(requestCtx, ctx, client)
|
issueOpts, addLabelOpts, rmLabelOpts, addAssigneeOpts, rmAssigneeOpts, err := opts.toSdkOptions(requestCtx, ctx, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,10 @@ func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ApplyAssigneeChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addAssigneeOpts, rmAssigneeOpts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var issue *gitea.Issue
|
var issue *gitea.Issue
|
||||||
if issueOpts != nil {
|
if issueOpts != nil {
|
||||||
issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
|
issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
addAssigneeOpts := ResolveAssigneeOpts(opts.AddAssignees)
|
||||||
|
rmAssigneeOpts := ResolveAssigneeOpts(opts.RemoveAssignees)
|
||||||
|
|
||||||
prOpts := gitea.EditPullRequestOption{}
|
prOpts := gitea.EditPullRequestOption{}
|
||||||
var prOptsDirty bool
|
var prOptsDirty bool
|
||||||
@@ -51,8 +53,9 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
|||||||
prOpts.RemoveDeadline = gitea.OptionalBool(true)
|
prOpts.RemoveDeadline = gitea.OptionalBool(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(opts.AddAssignees) != 0 {
|
opts.SetAssignees = cleanAssignees(opts.SetAssignees)
|
||||||
prOpts.Assignees = opts.AddAssignees
|
if len(opts.SetAssignees) != 0 {
|
||||||
|
prOpts.Assignees = opts.SetAssignees
|
||||||
prOptsDirty = true
|
prOptsDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +63,10 @@ func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := ApplyAssigneeChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addAssigneeOpts, rmAssigneeOpts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := ApplyReviewerChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil {
|
if err := ApplyReviewerChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gitea "gitea.dev/sdk"
|
||||||
|
"gitea.dev/tea/cmd"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEditIssue_ModifiesAssignees(t *testing.T) {
|
||||||
|
// This test verifies that EditIssue correctly modifies assignees of an issue via set, add, and remove.
|
||||||
|
// It sets up a test repository and organization with known users, then calls EditIssue and checks the results.
|
||||||
|
login := createIntegrationLogin(t)
|
||||||
|
client := login.Client()
|
||||||
|
orgName := fmt.Sprintf("issue-org-%d", time.Now().UnixNano()%1_000_000)
|
||||||
|
orgRepoName := fmt.Sprintf("issue-repo-%d", time.Now().UnixNano()%1_000_000)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Clean up any existing test data that might interfere with the test.
|
||||||
|
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
|
||||||
|
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user1")
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user2")
|
||||||
|
|
||||||
|
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
|
||||||
|
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
|
||||||
|
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user1")
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user2")
|
||||||
|
})
|
||||||
|
|
||||||
|
permission := gitea.AccessModeOwner
|
||||||
|
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, assigneeValid)
|
||||||
|
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, assigneeValid)
|
||||||
|
|
||||||
|
orgIssue, _, err := client.Issues.CreateIssue(ctx, orgName, orgRepoName, gitea.CreateIssueOption{Title: "issue_integration_test", Assignees: []string{integrationUsername}, Closed: false})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, integrationUsername, orgIssue.Assignees[0].UserName)
|
||||||
|
curUser, _, err := client.Users.GetMyUserInfo(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
getNames := func(issue *gitea.Issue) []string {
|
||||||
|
names := make([]string, len(issue.Assignees))
|
||||||
|
for i, u := range issue.Assignees {
|
||||||
|
names[i] = u.UserName
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAssignees := func(expected []string) {
|
||||||
|
updatedIssue, _, err := client.Issues.GetIssue(ctx, orgName, orgRepoName, orgIssue.Index)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, getNames(updatedIssue), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := cmd.App()
|
||||||
|
// test set overwrites add and remove
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user1.UserName})
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user2.UserName, curUser.UserName})
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user1.UserName})
|
||||||
|
|
||||||
|
// test remove one assignee
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{})
|
||||||
|
|
||||||
|
// test add multiple assignees and overwrites remove
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{curUser.UserName, user1.UserName})
|
||||||
|
|
||||||
|
// test add one assignee
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
|
||||||
|
|
||||||
|
// test remove multiple assignees
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{})
|
||||||
|
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--set-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--add-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
err = app.Run(ctx, []string{"tea", "i", "edit", "1", "--remove-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
@@ -80,7 +80,7 @@ func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
|
|||||||
runGit("commit", "--allow-empty", "-m", "Initial commit")
|
runGit("commit", "--allow-empty", "-m", "Initial commit")
|
||||||
runGit("push", "-u", "origin", "HEAD:branch-with-labels")
|
runGit("push", "-u", "origin", "HEAD:branch-with-labels")
|
||||||
|
|
||||||
waitForBranches(t, orgRepo.FullName)
|
waitForBranches(t, orgRepo.FullName, "branch-with-labels")
|
||||||
_ = runTeaCommand(
|
_ = runTeaCommand(
|
||||||
t, "pr", "create", "--repo", orgRepo.FullName,
|
t, "pr", "create", "--repo", orgRepo.FullName,
|
||||||
"--login", login.Name, "--base", "main", "--head", "branch-with-labels",
|
"--login", login.Name, "--base", "main", "--head", "branch-with-labels",
|
||||||
@@ -94,7 +94,7 @@ func TestResolveLabelNames_ReturnsRepoAndOrgLabels(t *testing.T) {
|
|||||||
require.ElementsMatch(t, labels, []*gitea.Label{orgLabel, repoLabel})
|
require.ElementsMatch(t, labels, []*gitea.Label{orgLabel, repoLabel})
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForBranches(t *testing.T, repoFullName string) {
|
func waitForBranches(t *testing.T, repoFullName string, branchName string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
url := fmt.Sprintf("%s/api/v1/repos/%s/branches", os.Getenv("GITEA_TEA_TEST_URL"), repoFullName)
|
url := fmt.Sprintf("%s/api/v1/repos/%s/branches", os.Getenv("GITEA_TEA_TEST_URL"), repoFullName)
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ func waitForBranches(t *testing.T, repoFullName string) {
|
|||||||
for _, b := range branches {
|
for _, b := range branches {
|
||||||
have[b.Name] = true
|
have[b.Name] = true
|
||||||
}
|
}
|
||||||
if have["main"] && have["branch-with-labels"] {
|
if have["main"] && have[branchName] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gitea "gitea.dev/sdk"
|
||||||
|
"gitea.dev/tea/cmd"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEditPull_ModifiesAssignees(t *testing.T) {
|
||||||
|
// This test verifies that EditPull correctly modifies assignees of an pull request via set, add, and remove.
|
||||||
|
// It sets up a test repository and organization with known users, then calls EditPull and checks the results.
|
||||||
|
login := createIntegrationLogin(t)
|
||||||
|
client := login.Client()
|
||||||
|
orgName := fmt.Sprintf("pull-org-%d", time.Now().UnixNano()%1_000_000)
|
||||||
|
orgRepoName := fmt.Sprintf("pull-repo-%d", time.Now().UnixNano()%1_000_000)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Clean up any existing test data that might interfere with the test.
|
||||||
|
_, _ = client.Repositories.DeleteRepo(ctx, orgName, orgRepoName)
|
||||||
|
_, _ = client.Organizations.DeleteOrg(ctx, orgName)
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user1")
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user2")
|
||||||
|
|
||||||
|
_, _, err := client.Admin.CreateOrg(ctx, integrationUsername, gitea.CreateOrgOption{Name: orgName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if _, delErr := client.Organizations.DeleteOrg(ctx, orgName); delErr != nil {
|
||||||
|
t.Logf("failed to delete integration test org %q: %v", orgName, delErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
orgRepo, _, err := client.Repositories.CreateOrgRepo(ctx, orgName, gitea.CreateRepoOption{Name: orgRepoName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if _, delErr := client.Repositories.DeleteRepo(ctx, orgName, orgRepoName); delErr != nil {
|
||||||
|
t.Logf("failed to delete integration test repo %q: %v", orgRepoName, delErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
user1, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user1", Password: "user1!1234", Email: "user1@test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
user2, _, err := client.Admin.CreateUser(ctx, gitea.CreateUserOption{Username: "user2", Password: "user2!1234", Email: "user2@test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user1")
|
||||||
|
_, _ = client.Admin.DeleteUser(ctx, "user2")
|
||||||
|
})
|
||||||
|
|
||||||
|
permission := gitea.AccessModeOwner
|
||||||
|
team, _, err := client.Organizations.CreateTeam(ctx, orgName, gitea.CreateTeamOption{Name: "writers", Permission: permission})
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamMember(ctx, team.ID, "user2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = client.Organizations.AddTeamRepository(ctx, team.ID, orgName, orgRepoName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assigneeValid, _, err := client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user1.UserName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, assigneeValid)
|
||||||
|
assigneeValid, _, err = client.Repositories.CheckRepoIssueAssignee(ctx, orgName, orgRepoName, user2.UserName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, assigneeValid)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
runGit := func(args ...string) {
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
cmd.Dir = tmpDir
|
||||||
|
require.NoError(t, cmd.Run())
|
||||||
|
}
|
||||||
|
|
||||||
|
runGit("init")
|
||||||
|
runGit("config", "user.email", "test@test.com")
|
||||||
|
runGit("config", "user.name", "test")
|
||||||
|
httpsURL := fmt.Sprintf("%s/%s.git", login.URL, orgRepo.FullName)
|
||||||
|
httpsURL = strings.Replace(httpsURL, "://", fmt.Sprintf("://%s:%s@", login.Name, login.Token), 1)
|
||||||
|
|
||||||
|
runGit("remote", "add", "origin", httpsURL)
|
||||||
|
|
||||||
|
runGit("checkout", "-b", "main")
|
||||||
|
runGit("commit", "--allow-empty", "-m", "Initial commit")
|
||||||
|
runGit("push", "-u", "origin", "HEAD:main")
|
||||||
|
|
||||||
|
runGit("checkout", "-b", "branch-with-assignees")
|
||||||
|
runGit("commit", "--allow-empty", "-m", "Initial commit")
|
||||||
|
runGit("push", "-u", "origin", "HEAD:branch-with-assignees")
|
||||||
|
|
||||||
|
waitForBranches(t, orgRepo.FullName, "branch-with-assignees")
|
||||||
|
getNames := func(pr *gitea.PullRequest) []string {
|
||||||
|
names := make([]string, len(pr.Assignees))
|
||||||
|
for i, u := range pr.Assignees {
|
||||||
|
names[i] = u.UserName
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
checkAssignees := func(expected []string) {
|
||||||
|
updatedPr, _, err := client.PullRequests.GetPullRequest(ctx, orgName, orgRepoName, 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.ElementsMatch(t, getNames(updatedPr), expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := cmd.App()
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "create", "--repo", orgRepo.FullName, "--base", "main", "--head", "branch-with-assignees", "--a", integrationUsername})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{integrationUsername})
|
||||||
|
curUser, _, err := client.Users.GetMyUserInfo(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// test set overwrites add and remove
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--add-assignees", "user2", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user1.UserName})
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user2," + integrationUsername, "--add-assignees", "user1", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user2.UserName, curUser.UserName})
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user1", "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{user1.UserName})
|
||||||
|
|
||||||
|
// test remove one assignee
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{})
|
||||||
|
|
||||||
|
// test add multiple assignees and overwrites remove
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user1," + integrationUsername, "--remove-assignees", integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{curUser.UserName, user1.UserName})
|
||||||
|
|
||||||
|
// test add one assignee
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user2", "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{curUser.UserName, user1.UserName, user2.UserName})
|
||||||
|
|
||||||
|
// test remove multiple assignees
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user1,user2," + integrationUsername, "--repo", orgRepo.FullName})
|
||||||
|
require.NoError(t, err)
|
||||||
|
checkAssignees([]string{})
|
||||||
|
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--set-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--add-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
err = app.Run(ctx, []string{"tea", "pr", "edit", "1", "--remove-assignees", "user_not_exists"})
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user