Skip to content

Commit 77cc378

Browse files
authored
Merge pull request cli#2004 from marclop/f/add-self-assign-flag-to-issue-create
Allow assigning to or filtering by special `@me` keyword
2 parents 7fecaaa + e334a1f commit 77cc378

File tree

9 files changed

+303
-20
lines changed

9 files changed

+303
-20
lines changed

pkg/cmd/alias/set/set.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command
5454
#=> gh pr view -w 123
5555
5656
$ gh alias set bugs 'issue list --label="bugs"'
57+
$ gh bugs
58+
59+
$ gh alias set homework 'issue list --assigned @me'
60+
$ gh homework
5761
5862
$ gh alias set epicsBy 'issue list --author="$1" --label="epic"'
5963
$ gh epicsBy vilmibm

pkg/cmd/issue/create/create.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/cli/cli/api"
1010
"github.com/cli/cli/internal/config"
1111
"github.com/cli/cli/internal/ghrepo"
12+
"github.com/cli/cli/pkg/cmd/pr/shared"
1213
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
1314
"github.com/cli/cli/pkg/cmdutil"
1415
"github.com/cli/cli/pkg/iostreams"
@@ -53,6 +54,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
5354
$ gh issue create --label "bug,help wanted"
5455
$ gh issue create --label bug --label "help wanted"
5556
$ gh issue create --assignee monalisa,hubot
57+
$ gh issue create --assignee @me
5658
$ gh issue create --project "Roadmap"
5759
`),
5860
Args: cmdutil.NoArgsQuoteReminder,
@@ -84,7 +86,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
8486
cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Supply a title. Will prompt for one otherwise.")
8587
cmd.Flags().StringVarP(&opts.Body, "body", "b", "", "Supply a body. Will prompt for one otherwise.")
8688
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the browser to create an issue")
87-
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
89+
cmd.Flags().StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
8890
cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
8991
cmd.Flags().StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the issue to projects by `name`")
9092
cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Add the issue to a milestone by `name`")
@@ -114,9 +116,15 @@ func createRun(opts *CreateOptions) (err error) {
114116
milestones = []string{opts.Milestone}
115117
}
116118

119+
meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
120+
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
121+
if err != nil {
122+
return err
123+
}
124+
117125
tb := prShared.IssueMetadataState{
118126
Type: prShared.IssueMetadata,
119-
Assignees: opts.Assignees,
127+
Assignees: assignees,
120128
Labels: opts.Labels,
121129
Projects: opts.Projects,
122130
Milestones: milestones,
@@ -134,7 +142,7 @@ func createRun(opts *CreateOptions) (err error) {
134142

135143
if opts.WebMode {
136144
openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
137-
if opts.Title != "" || opts.Body != "" {
145+
if opts.Title != "" || opts.Body != "" || len(opts.Assignees) > 0 {
138146
openURL, err = prShared.WithPrAndIssueQueryParams(openURL, tb)
139147
if err != nil {
140148
return

pkg/cmd/issue/create/create_test.go

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,15 @@ func TestIssueCreate_web(t *testing.T) {
410410
http := &httpmock.Registry{}
411411
defer http.Verify(t)
412412

413+
http.Register(
414+
httpmock.GraphQL(`query UserCurrent\b`),
415+
httpmock.StringResponse(`
416+
{ "data": {
417+
"viewer": { "login": "MonaLisa" }
418+
} }
419+
`),
420+
)
421+
413422
var seenCmd *exec.Cmd
414423
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
415424
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
@@ -418,7 +427,7 @@ func TestIssueCreate_web(t *testing.T) {
418427
})
419428
defer restoreCmd()
420429

421-
output, err := runCommand(http, true, `--web`)
430+
output, err := runCommand(http, true, `--web -a @me`)
422431
if err != nil {
423432
t.Errorf("error running command `issue create`: %v", err)
424433
}
@@ -427,7 +436,7 @@ func TestIssueCreate_web(t *testing.T) {
427436
t.Fatal("expected a command to run")
428437
}
429438
url := seenCmd.Args[len(seenCmd.Args)-1]
430-
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new", url)
439+
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa", url)
431440
assert.Equal(t, "", output.String())
432441
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
433442
}
@@ -457,3 +466,91 @@ func TestIssueCreate_webTitleBody(t *testing.T) {
457466
assert.Equal(t, "", output.String())
458467
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
459468
}
469+
470+
func TestIssueCreate_webTitleBodyAtMeAssignee(t *testing.T) {
471+
http := &httpmock.Registry{}
472+
defer http.Verify(t)
473+
474+
http.Register(
475+
httpmock.GraphQL(`query UserCurrent\b`),
476+
httpmock.StringResponse(`
477+
{ "data": {
478+
"viewer": { "login": "MonaLisa" }
479+
} }
480+
`),
481+
)
482+
483+
var seenCmd *exec.Cmd
484+
//nolint:staticcheck // SA1019 TODO: rewrite to use run.Stub
485+
restoreCmd := run.SetPrepareCmd(func(cmd *exec.Cmd) run.Runnable {
486+
seenCmd = cmd
487+
return &test.OutputStub{}
488+
})
489+
defer restoreCmd()
490+
491+
output, err := runCommand(http, true, `-w -t mytitle -b mybody -a @me`)
492+
if err != nil {
493+
t.Errorf("error running command `issue create`: %v", err)
494+
}
495+
496+
if seenCmd == nil {
497+
t.Fatal("expected a command to run")
498+
}
499+
url := strings.ReplaceAll(seenCmd.Args[len(seenCmd.Args)-1], "^", "")
500+
assert.Equal(t, "https://github.com/OWNER/REPO/issues/new?assignees=MonaLisa&body=mybody&title=mytitle", url)
501+
assert.Equal(t, "", output.String())
502+
assert.Equal(t, "Opening github.com/OWNER/REPO/issues/new in your browser.\n", output.Stderr())
503+
}
504+
505+
func TestIssueCreate_AtMeAssignee(t *testing.T) {
506+
http := &httpmock.Registry{}
507+
defer http.Verify(t)
508+
509+
http.Register(
510+
httpmock.GraphQL(`query UserCurrent\b`),
511+
httpmock.StringResponse(`
512+
{ "data": {
513+
"viewer": { "login": "MonaLisa" }
514+
} }
515+
`),
516+
)
517+
http.Register(
518+
httpmock.GraphQL(`query RepositoryInfo\b`),
519+
httpmock.StringResponse(`
520+
{ "data": { "repository": {
521+
"id": "REPOID",
522+
"hasIssuesEnabled": true
523+
} } }
524+
`))
525+
http.Register(
526+
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
527+
httpmock.StringResponse(`
528+
{ "data": {
529+
"u000": { "login": "MonaLisa", "id": "MONAID" },
530+
"u001": { "login": "SomeOneElse", "id": "SOMEID" },
531+
"repository": {
532+
"l000": { "name": "bug", "id": "BUGID" },
533+
"l001": { "name": "TODO", "id": "TODOID" }
534+
}
535+
} }
536+
`),
537+
)
538+
http.Register(
539+
httpmock.GraphQL(`mutation IssueCreate\b`),
540+
httpmock.GraphQLMutation(`
541+
{ "data": { "createIssue": { "issue": {
542+
"URL": "https://github.com/OWNER/REPO/issues/12"
543+
} } } }
544+
`, func(inputs map[string]interface{}) {
545+
assert.Equal(t, "hello", inputs["title"])
546+
assert.Equal(t, "cash rules everything around me", inputs["body"])
547+
assert.Equal(t, []interface{}{"MONAID", "SOMEID"}, inputs["assigneeIds"])
548+
}))
549+
550+
output, err := runCommand(http, true, `-a @me -a someoneelse -t hello -b "cash rules everything around me"`)
551+
if err != nil {
552+
t.Errorf("error running command `issue create`: %v", err)
553+
}
554+
555+
assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
556+
}

pkg/cmd/issue/list/list.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/cli/cli/internal/config"
1010
"github.com/cli/cli/internal/ghrepo"
1111
issueShared "github.com/cli/cli/pkg/cmd/issue/shared"
12+
"github.com/cli/cli/pkg/cmd/pr/shared"
1213
prShared "github.com/cli/cli/pkg/cmd/pr/shared"
1314
"github.com/cli/cli/pkg/cmdutil"
1415
"github.com/cli/cli/pkg/iostreams"
@@ -46,6 +47,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
4647
Example: heredoc.Doc(`
4748
$ gh issue list -l "help wanted"
4849
$ gh issue list -A monalisa
50+
$ gh issue list -a @me
4951
$ gh issue list --web
5052
$ gh issue list --milestone 'MVP'
5153
`),
@@ -91,15 +93,29 @@ func listRun(opts *ListOptions) error {
9193

9294
isTerminal := opts.IO.IsStdoutTTY()
9395

96+
meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost())
97+
filterAssignee, err := meReplacer.Replace(opts.Assignee)
98+
if err != nil {
99+
return err
100+
}
101+
filterAuthor, err := meReplacer.Replace(opts.Author)
102+
if err != nil {
103+
return err
104+
}
105+
filterMention, err := meReplacer.Replace(opts.Mention)
106+
if err != nil {
107+
return err
108+
}
109+
94110
if opts.WebMode {
95111
issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues")
96112
openURL, err := prShared.ListURLWithQuery(issueListURL, prShared.FilterOptions{
97113
Entity: "issue",
98114
State: opts.State,
99-
Assignee: opts.Assignee,
115+
Assignee: filterAssignee,
100116
Labels: opts.Labels,
101-
Author: opts.Author,
102-
Mention: opts.Mention,
117+
Author: filterAuthor,
118+
Mention: filterMention,
103119
Milestone: opts.Milestone,
104120
})
105121
if err != nil {
@@ -111,7 +127,7 @@ func listRun(opts *ListOptions) error {
111127
return utils.OpenInBrowser(openURL)
112128
}
113129

114-
listResult, err := api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, opts.Assignee, opts.LimitResults, opts.Author, opts.Mention, opts.Milestone)
130+
listResult, err := api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, filterAssignee, opts.LimitResults, filterAuthor, filterMention, opts.Milestone)
115131
if err != nil {
116132
return err
117133
}

pkg/cmd/issue/list/list_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ No issues match your search in OWNER/REPO
147147
`, output.String())
148148
}
149149

150+
func TestIssueList_atMe(t *testing.T) {
151+
http := &httpmock.Registry{}
152+
defer http.Verify(t)
153+
154+
http.Register(
155+
httpmock.GraphQL(`query UserCurrent\b`),
156+
httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`))
157+
http.Register(
158+
httpmock.GraphQL(`query IssueList\b`),
159+
httpmock.GraphQLQuery(`
160+
{ "data": { "repository": {
161+
"hasIssuesEnabled": true,
162+
"issues": { "nodes": [] }
163+
} } }`, func(_ string, params map[string]interface{}) {
164+
assert.Equal(t, "monalisa", params["assignee"].(string))
165+
assert.Equal(t, "monalisa", params["author"].(string))
166+
assert.Equal(t, "monalisa", params["mention"].(string))
167+
}))
168+
169+
_, err := runCommand(http, true, "-a @me -A @me --mention @me")
170+
if err != nil {
171+
t.Errorf("error running command `issue list`: %v", err)
172+
}
173+
}
174+
150175
func TestIssueList_withInvalidLimitFlag(t *testing.T) {
151176
http := &httpmock.Registry{}
152177
defer http.Verify(t)

pkg/cmd/pr/create/create.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ type CreateContext struct {
6161
// This struct stores contextual data about the creation process and is for building up enough
6262
// data to create a pull request
6363
RepoContext *context.ResolvedRemotes
64-
BaseRepo ghrepo.Interface
64+
BaseRepo *api.Repository
6565
HeadRepo ghrepo.Interface
6666
BaseTrackingBranch string
6767
BaseBranch string
@@ -146,7 +146,7 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co
146146
fl.BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to create a pull request")
147147
fl.BoolVarP(&opts.Autofill, "fill", "f", false, "Do not prompt for title/body and just use commit info")
148148
fl.StringSliceVarP(&opts.Reviewers, "reviewer", "r", nil, "Request reviews from people or teams by their `handle`")
149-
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`")
149+
fl.StringSliceVarP(&opts.Assignees, "assignee", "a", nil, "Assign people by their `login`. Use \"@me\" to self-assign.")
150150
fl.StringSliceVarP(&opts.Labels, "label", "l", nil, "Add labels by `name`")
151151
fl.StringSliceVarP(&opts.Projects, "project", "p", nil, "Add the pull request to projects by `name`")
152152
fl.StringVarP(&opts.Milestone, "milestone", "m", "", "Add the pull request to a milestone by `name`")
@@ -264,7 +264,7 @@ func createRun(opts *CreateOptions) (err error) {
264264
}
265265
}
266266

267-
allowMetadata := ctx.BaseRepo.(*api.Repository).ViewerCanTriage()
267+
allowMetadata := ctx.BaseRepo.ViewerCanTriage()
268268
action, err := shared.ConfirmSubmission(!state.HasMetadata(), allowMetadata)
269269
if err != nil {
270270
return fmt.Errorf("unable to confirm: %w", err)
@@ -386,10 +386,16 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata
386386
milestoneTitles = []string{opts.Milestone}
387387
}
388388

389+
meReplacer := shared.NewMeReplacer(ctx.Client, ctx.BaseRepo.RepoHost())
390+
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
391+
if err != nil {
392+
return nil, err
393+
}
394+
389395
state := &shared.IssueMetadataState{
390396
Type: shared.PRMetadata,
391397
Reviewers: opts.Reviewers,
392-
Assignees: opts.Assignees,
398+
Assignees: assignees,
393399
Labels: opts.Labels,
394400
Projects: opts.Projects,
395401
Milestones: milestoneTitles,
@@ -591,7 +597,7 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
591597
}
592598

593599
opts.IO.StartProgressIndicator()
594-
pr, err := api.CreatePullRequest(client, ctx.BaseRepo.(*api.Repository), params)
600+
pr, err := api.CreatePullRequest(client, ctx.BaseRepo, params)
595601
opts.IO.StopProgressIndicator()
596602
if pr != nil {
597603
fmt.Fprintln(opts.IO.Out, pr.URL)

pkg/cmd/pr/create/create_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212

1313
"github.com/MakeNowJust/heredoc"
14+
"github.com/cli/cli/api"
1415
"github.com/cli/cli/context"
1516
"github.com/cli/cli/git"
1617
"github.com/cli/cli/internal/config"
@@ -873,7 +874,7 @@ func Test_generateCompareURL(t *testing.T) {
873874
{
874875
name: "basic",
875876
ctx: CreateContext{
876-
BaseRepo: ghrepo.New("OWNER", "REPO"),
877+
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
877878
BaseBranch: "main",
878879
HeadBranchLabel: "feature",
879880
},
@@ -883,7 +884,7 @@ func Test_generateCompareURL(t *testing.T) {
883884
{
884885
name: "with labels",
885886
ctx: CreateContext{
886-
BaseRepo: ghrepo.New("OWNER", "REPO"),
887+
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
887888
BaseBranch: "a",
888889
HeadBranchLabel: "b",
889890
},
@@ -896,7 +897,7 @@ func Test_generateCompareURL(t *testing.T) {
896897
{
897898
name: "complex branch names",
898899
ctx: CreateContext{
899-
BaseRepo: ghrepo.New("OWNER", "REPO"),
900+
BaseRepo: api.InitRepoHostname(&api.Repository{Name: "REPO", Owner: api.RepositoryOwner{Login: "OWNER"}}, "github.com"),
900901
BaseBranch: "main/trunk",
901902
HeadBranchLabel: "owner:feature",
902903
},

0 commit comments

Comments
 (0)