Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pkg/cmd/issue/edit/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,17 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman

Editing issues' projects requires authorization with the %[1]sproject%[1]s scope.
To authorize, run %[1]sgh auth refresh -s project%[1]s.

The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support
the following special values:
- %[1]s@me%[1]s: assign or unassign yourself
- %[1]s@copilot%[1]s: assign or unassign Copilot (not supported on GitHub Enterprise Server)
`, "`"),
Example: heredoc.Doc(`
$ gh issue edit 23 --title "I found a bug" --body "Nothing works"
$ gh issue edit 23 --add-label "bug,help wanted" --remove-label "core"
$ gh issue edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
$ gh issue edit 23 --add-assignee "@copilot"
$ gh issue edit 23 --add-project "Roadmap" --remove-project v1,v2
$ gh issue edit 23 --milestone "Version 1"
$ gh issue edit 23 --remove-milestone
Expand Down
9 changes: 9 additions & 0 deletions pkg/cmd/pr/edit/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,21 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman

Editing a pull request's projects requires authorization with the %[1]sproject%[1]s scope.
To authorize, run %[1]sgh auth refresh -s project%[1]s.

The %[1]s--add-assignee%[1]s and %[1]s--remove-assignee%[1]s flags both support
the following special values:
- %[1]s@me%[1]s: assign or unassign yourself
- %[1]s@copilot%[1]s: assign or unassign Copilot (not supported on GitHub Enterprise Server)

The %[1]s--add-reviewer%[1]s and %[1]s--remove-reviewer%[1]s flags do not support
these special values.
`, "`"),
Example: heredoc.Doc(`
$ gh pr edit 23 --title "I found a bug" --body "Nothing works"
$ gh pr edit 23 --add-label "bug,help wanted" --remove-label "core"
$ gh pr edit 23 --add-reviewer monalisa,hubot --remove-reviewer myorg/team-name
$ gh pr edit 23 --add-assignee "@me" --remove-assignee monalisa,hubot
$ gh pr edit 23 --add-assignee "@copilot"
$ gh pr edit 23 --add-project "Roadmap" --remove-project v1,v2
$ gh pr edit 23 --milestone "Version 1"
$ gh pr edit 23 --remove-milestone
Expand Down
36 changes: 28 additions & 8 deletions pkg/cmd/pr/shared/editable.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,24 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str
// curate the final list of assignees from the default list.
if len(e.Assignees.Add) != 0 || len(e.Assignees.Remove) != 0 {
meReplacer := NewMeReplacer(client, repo.RepoHost())
s := set.NewStringSet()
copilotReplacer := NewCopilotReplacer()

replaceSpecialAssigneeNames := func(value []string) ([]string, error) {
replaced, err := meReplacer.ReplaceSlice(value)
if err != nil {
return nil, err
}

// Only suppported for actor assignees.
if e.Assignees.ActorAssignees {
replaced = copilotReplacer.ReplaceSlice(replaced)
}

return replaced, nil
}

assigneeSet := set.NewStringSet()

// This check below is required because in a non-interactive flow,
// the user gives us a login and not the DisplayName, and when
// we have actor assignees e.Assignees.Default will contain
Expand All @@ -128,21 +145,24 @@ func (e Editable) AssigneeIds(client *api.Client, repo ghrepo.Interface) (*[]str
// Otherwise, the value the user provided won't be found in the
// set to be added or removed, causing unexpected behavior.
if e.Assignees.ActorAssignees {
s.AddValues(e.Assignees.DefaultLogins)
assigneeSet.AddValues(e.Assignees.DefaultLogins)
} else {
s.AddValues(e.Assignees.Default)
assigneeSet.AddValues(e.Assignees.Default)
}
add, err := meReplacer.ReplaceSlice(e.Assignees.Add)

add, err := replaceSpecialAssigneeNames(e.Assignees.Add)
if err != nil {
return nil, err
}
s.AddValues(add)
remove, err := meReplacer.ReplaceSlice(e.Assignees.Remove)
assigneeSet.AddValues(add)

remove, err := replaceSpecialAssigneeNames(e.Assignees.Remove)
if err != nil {
return nil, err
}
s.RemoveValues(remove)
e.Assignees.Value = s.ToSlice()
assigneeSet.RemoveValues(remove)

e.Assignees.Value = assigneeSet.ToSlice()
}
a, err := e.Metadata.MembersToIDs(e.Assignees.Value)
return &a, err
Expand Down
23 changes: 23 additions & 0 deletions pkg/cmd/pr/shared/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,26 @@ func (r *MeReplacer) ReplaceSlice(handles []string) ([]string, error) {
}
return res, nil
}

// CopilotReplacer resolves usages of `@copilot` to Copilot's login.
type CopilotReplacer struct{}

func NewCopilotReplacer() *CopilotReplacer {
return &CopilotReplacer{}
}

func (r *CopilotReplacer) replace(handle string) string {
if strings.EqualFold(handle, "@copilot") {
return "copilot-swe-agent"
}
return handle
}

// Replace replaces usages of `@copilot` in a slice with Copilot's login.
func (r *CopilotReplacer) ReplaceSlice(handles []string) []string {
res := make([]string, len(handles))
for i, h := range handles {
res[i] = r.replace(h)
}
return res
}
61 changes: 61 additions & 0 deletions pkg/cmd/pr/shared/params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,67 @@ func TestMeReplacer_Replace(t *testing.T) {
}
}

func TestCopilotReplacer_ReplaceSlice(t *testing.T) {
type args struct {
handles []string
}
tests := []struct {
name string
args args
want []string
}{
{
name: "replaces @copilot with copilot-swe-agent",
args: args{
Comment thread
BagToad marked this conversation as resolved.
handles: []string{"monalisa", "@copilot", "hubot"},
},
want: []string{"monalisa", "copilot-swe-agent", "hubot"},
},
{
name: "handles no @copilot mentions",
args: args{
handles: []string{"monalisa", "user", "hubot"},
},
want: []string{"monalisa", "user", "hubot"},
},
{
name: "replaces multiple @copilot mentions",
args: args{
handles: []string{"@copilot", "user", "@copilot"},
},
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
},
{
name: "handles @copilot case-insensitively",
args: args{
handles: []string{"@Copilot", "user", "@CoPiLoT"},
},
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
},
{
name: "handles nil slice",
args: args{
handles: nil,
},
want: []string{},
},
{
name: "handles empty slice",
args: args{
handles: []string{},
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewCopilotReplacer()
got := r.ReplaceSlice(tt.args.handles)
require.Equal(t, tt.want, got)
})
}
}

func Test_QueryHasStateClause(t *testing.T) {
tests := []struct {
searchQuery string
Expand Down
Loading