Skip to content
111 changes: 2 additions & 109 deletions api/queries_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,114 +1033,6 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
return &result, nil
}

type RepoResolveInput struct {
Assignees []string
Reviewers []string
Labels []string
ProjectsV1 bool
ProjectsV2 bool
Milestones []string
}

// RepoResolveMetadataIDs looks up GraphQL node IDs in bulk
func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoResolveInput) (*RepoMetadataResult, error) {
users := input.Assignees
hasUser := func(target string) bool {
for _, u := range users {
if strings.EqualFold(u, target) {
return true
}
}
return false
}

var teams []string
for _, r := range input.Reviewers {
if i := strings.IndexRune(r, '/'); i > -1 {
teams = append(teams, r[i+1:])
} else if !hasUser(r) {
users = append(users, r)
}
}

// there is no way to look up projects nor milestones by name, so preload them all
mi := RepoMetadataInput{
ProjectsV1: input.ProjectsV1,
ProjectsV2: input.ProjectsV2,
Milestones: len(input.Milestones) > 0,
}
result, err := RepoMetadata(client, repo, mi)
if err != nil {
return result, err
}
if len(users) == 0 && len(teams) == 0 && len(input.Labels) == 0 {
return result, nil
}

query := &bytes.Buffer{}
fmt.Fprint(query, "query RepositoryResolveMetadataIDs {\n")
for i, u := range users {
fmt.Fprintf(query, "u%03d: user(login:%q){id,login}\n", i, u)
}
if len(input.Labels) > 0 {
fmt.Fprintf(query, "repository(owner:%q,name:%q){\n", repo.RepoOwner(), repo.RepoName())
for i, l := range input.Labels {
fmt.Fprintf(query, "l%03d: label(name:%q){id,name}\n", i, l)
}
fmt.Fprint(query, "}\n")
}
if len(teams) > 0 {
fmt.Fprintf(query, "organization(login:%q){\n", repo.RepoOwner())
for i, t := range teams {
fmt.Fprintf(query, "t%03d: team(slug:%q){id,slug}\n", i, t)
}
fmt.Fprint(query, "}\n")
}
fmt.Fprint(query, "}\n")

response := make(map[string]json.RawMessage)
err = client.GraphQL(repo.RepoHost(), query.String(), nil, &response)
if err != nil {
return result, err
}

for key, v := range response {
switch key {
case "repository":
repoResponse := make(map[string]RepoLabel)
err := json.Unmarshal(v, &repoResponse)
if err != nil {
return result, err
}
for _, l := range repoResponse {
result.Labels = append(result.Labels, l)
}
case "organization":
orgResponse := make(map[string]OrgTeam)
err := json.Unmarshal(v, &orgResponse)
if err != nil {
return result, err
}
for _, t := range orgResponse {
result.Teams = append(result.Teams, t)
}
default:
user := struct {
Id string
Login string
Name string
}{}
err := json.Unmarshal(v, &user)
if err != nil {
return result, err
}
result.AssignableUsers = append(result.AssignableUsers, NewAssignableUser(user.Id, user.Login, user.Name))
}
}

return result, nil
}

type RepoProject struct {
ID string `json:"id"`
Name string `json:"name"`
Expand Down Expand Up @@ -1190,6 +1082,7 @@ func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error)
// This is returned from assignable actors and issue/pr assigned actors.
// We use this to check if the actor is Copilot.
const CopilotActorLogin = "copilot-swe-agent"
const CopilotActorName = "Copilot"

type AssignableActor interface {
DisplayName() string
Expand Down Expand Up @@ -1250,7 +1143,7 @@ func NewAssignableBot(id, login string) AssignableBot {

func (b AssignableBot) DisplayName() string {
if b.login == CopilotActorLogin {
return "Copilot (AI)"
return fmt.Sprintf("%s (AI)", CopilotActorName)
}
return b.Login()
}
Expand Down
82 changes: 0 additions & 82 deletions api/queries_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,88 +379,6 @@ func Test_ProjectNamesToPaths(t *testing.T) {
})
}

func Test_RepoResolveMetadataIDs(t *testing.T) {
http := &httpmock.Registry{}
client := newTestClient(http)

repo, _ := ghrepo.FromFullName("OWNER/REPO")
input := RepoResolveInput{
Assignees: []string{"monalisa", "hubot"},
Reviewers: []string{"monalisa", "octocat", "OWNER/core", "/robots"},
Labels: []string{"bug", "help wanted"},
}

expectedQuery := `query RepositoryResolveMetadataIDs {
u000: user(login:"monalisa"){id,login}
u001: user(login:"hubot"){id,login}
u002: user(login:"octocat"){id,login}
repository(owner:"OWNER",name:"REPO"){
l000: label(name:"bug"){id,name}
l001: label(name:"help wanted"){id,name}
}
organization(login:"OWNER"){
t000: team(slug:"core"){id,slug}
t001: team(slug:"robots"){id,slug}
}
}
`
responseJSON := `
{ "data": {
"u000": { "login": "MonaLisa", "id": "MONAID" },
"u001": { "login": "hubot", "id": "HUBOTID" },
"u002": { "login": "octocat", "id": "OCTOID" },
"repository": {
"l000": { "name": "bug", "id": "BUGID" },
"l001": { "name": "Help Wanted", "id": "HELPID" }
},
"organization": {
"t000": { "slug": "core", "id": "COREID" },
"t001": { "slug": "Robots", "id": "ROBOTID" }
}
} }
`

http.Register(
httpmock.GraphQL(`query RepositoryResolveMetadataIDs\b`),
httpmock.GraphQLQuery(responseJSON, func(q string, _ map[string]interface{}) {
if q != expectedQuery {
t.Errorf("expected query %q, got %q", expectedQuery, q)
}
}))

result, err := RepoResolveMetadataIDs(client, repo, input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

expectedMemberIDs := []string{"MONAID", "HUBOTID", "OCTOID"}
memberIDs, err := result.MembersToIDs([]string{"monalisa", "hubot", "octocat"})
if err != nil {
t.Errorf("error resolving members: %v", err)
}
if !sliceEqual(memberIDs, expectedMemberIDs) {
t.Errorf("expected members %v, got %v", expectedMemberIDs, memberIDs)
}

expectedTeamIDs := []string{"COREID", "ROBOTID"}
teamIDs, err := result.TeamsToIDs([]string{"/core", "/robots"})
if err != nil {
t.Errorf("error resolving teams: %v", err)
}
if !sliceEqual(teamIDs, expectedTeamIDs) {
t.Errorf("expected members %v, got %v", expectedTeamIDs, teamIDs)
}

expectedLabelIDs := []string{"BUGID", "HELPID"}
labelIDs, err := result.LabelsToIDs([]string{"bug", "help wanted"})
if err != nil {
t.Errorf("error resolving labels: %v", err)
}
if !sliceEqual(labelIDs, expectedLabelIDs) {
t.Errorf("expected members %v, got %v", expectedLabelIDs, labelIDs)
}
}

func TestMembersToIDs(t *testing.T) {
t.Parallel()

Expand Down
34 changes: 27 additions & 7 deletions pkg/cmd/issue/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/set"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -68,13 +69,18 @@ func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co

Adding an issue to projects requires authorization with the %[1]sproject%[1]s scope.
To authorize, run %[1]sgh auth refresh -s project%[1]s.

The %[1]s--assignee%[1]s flag supports the following special values:
- %[1]s@me%[1]s: assign yourself
- %[1]s@copilot%[1]s: assign Copilot (not supported on GitHub Enterprise Server)
`, "`"),
Example: heredoc.Doc(`
$ gh issue create --title "I found a bug" --body "Nothing works"
$ gh issue create --label "bug,help wanted"
$ gh issue create --label bug --label "help wanted"
$ gh issue create --assignee monalisa,hubot
$ gh issue create --assignee "@me"
$ gh issue create --assignee "@copilot"
$ gh issue create --project "Roadmap"
$ gh issue create --template "Bug Report"
`),
Expand Down Expand Up @@ -158,6 +164,10 @@ func createRun(opts *CreateOptions) (err error) {
}

projectsV1Support := opts.Detector.ProjectsV1()
issueFeatures, err := opts.Detector.IssueFeatures()
if err != nil {
return err
}

isTerminal := opts.IO.IsStdoutTTY()

Expand All @@ -166,20 +176,30 @@ func createRun(opts *CreateOptions) (err error) {
milestones = []string{opts.Milestone}
}

// Replace special values in assignees
// For web mode, @copilot should be replaced by name; otherwise, login.
assigneeSet := set.NewStringSet()
meReplacer := prShared.NewMeReplacer(apiClient, baseRepo.RepoHost())
copilotReplacer := prShared.NewCopilotReplacer(!opts.WebMode)
assignees, err := meReplacer.ReplaceSlice(opts.Assignees)
if err != nil {
return err
}

if issueFeatures.ActorIsAssignable {
assignees = copilotReplacer.ReplaceSlice(assignees)
}
assigneeSet.AddValues(assignees)

tb := prShared.IssueMetadataState{
Type: prShared.IssueMetadata,
Assignees: assignees,
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
Type: prShared.IssueMetadata,
ActorAssignees: issueFeatures.ActorIsAssignable,
Assignees: assigneeSet.ToSlice(),
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
}

if opts.RecoverFile != "" {
Expand Down
Loading
Loading