feat(issue/pr edit): support @copilot in assignee flags

- Introduced CopilotReplacer to handle `@copilot` mentions in assignee lists.
This commit is contained in:
Kynan Ware 2025-05-15 12:50:03 -06:00
parent 51b1e6cd6f
commit 375c6cd28f
3 changed files with 112 additions and 8 deletions

View file

@ -118,7 +118,28 @@ 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()
// A closure to replace special assignee names with the actual logins.
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, err = copilotReplacer.ReplaceSlice(replaced)
if err != nil {
return nil, err
}
}
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
@ -128,21 +149,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

View file

@ -312,3 +312,30 @@ 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, error) {
if strings.EqualFold(handle, "@copilot") {
return "copilot-swe-agent", nil
}
return handle, nil
}
// Replace replaces usages of `@copilot` in a slice with Copilot's login.
func (r *CopilotReplacer) ReplaceSlice(handles []string) ([]string, error) {
res := make([]string, len(handles))
for i, h := range handles {
var err error
res[i], err = r.replace(h)
if err != nil {
return nil, err
}
}
return res, nil
}

View file

@ -187,6 +187,59 @@ 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
wantErr bool
}{
{
name: "replaces @copilot with copilot-swe-agent",
args: args{
handles: []string{"monalisa", "@copilot", "hubot"},
},
want: []string{"monalisa", "copilot-swe-agent", "hubot"},
wantErr: false,
},
{
name: "handles no @copilot mentions",
args: args{
handles: []string{"monalisa", "user", "hubot"},
},
want: []string{"monalisa", "user", "hubot"},
wantErr: false,
},
{
name: "replaces multiple @copilot mentions",
args: args{
handles: []string{"@copilot", "user", "@copilot"},
},
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
wantErr: false,
},
{
name: "handles @copilot case-insensitively",
args: args{
handles: []string{"@Copilot", "user", "@CoPiLoT"},
},
want: []string{"copilot-swe-agent", "user", "copilot-swe-agent"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := NewCopilotReplacer()
got, err := r.ReplaceSlice(tt.args.handles)
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
func Test_QueryHasStateClause(t *testing.T) {
tests := []struct {
searchQuery string