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
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