diff --git a/api/queries_repo.go b/api/queries_repo.go index b8ad760d0..abd96cbaf 100644 --- a/api/queries_repo.go +++ b/api/queries_repo.go @@ -388,6 +388,53 @@ func RepositoryReadme(client *Client, fullName string) (string, error) { } +type RepoMetadataResult struct { + AssignableUsers struct { + Nodes []struct { + ID string + Login string + } + } `graphql:"assignableUsers(first: 100)"` + Labels struct { + Nodes []struct { + ID string + Name string + } + } `graphql:"labels(first: 100)"` + Projects struct { + Nodes []struct { + ID string + Name string + } + } `graphql:"projects(first: 100, states: [OPEN])"` + Milestones struct { + Nodes []struct { + ID string + Title string + } + } `graphql:"milestones(first: 100, states: [OPEN])"` +} + +// RepoMetadata pre-fetches the metadata for attaching to issues and pull requests +func RepoMetadata(client *Client, repo ghrepo.Interface) (*RepoMetadataResult, error) { + var query struct { + Repository RepoMetadataResult `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + v4 := githubv4.NewClient(client.http) + err := v4.Query(context.Background(), &query, variables) + if err != nil { + return nil, err + } + + return &query.Repository, nil +} + func isMarkdownFile(filename string) bool { // kind of gross, but i'm assuming that 90% of the time the suffix will just be .md. it didn't // seem worth executing a regex for this given that assumption. diff --git a/command/issue.go b/command/issue.go index 2f4e145af..7236297a0 100644 --- a/command/issue.go +++ b/command/issue.go @@ -29,6 +29,10 @@ func init() { issueCreateCmd.Flags().StringP("body", "b", "", "Supply a body. Will prompt for one otherwise.") issueCreateCmd.Flags().BoolP("web", "w", false, "Open the browser to create an issue") + issueCreateCmd.Flags().StringSliceP("assignee", "a", nil, "Assign a person by their `login`") + issueCreateCmd.Flags().StringSliceP("label", "l", nil, "Add a label by `name`") + issueCreateCmd.Flags().StringSliceP("project", "p", nil, "Add the issue to a project by `name`") + issueCreateCmd.Flags().StringP("milestone", "m", "", "Add the issue to a milestone by `name`") issueCmd.AddCommand(issueListCmd) issueListCmd.Flags().StringP("assignee", "a", "", "Filter by assignee") @@ -350,6 +354,23 @@ func issueCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("could not parse body: %w", err) } + assignees, err := cmd.Flags().GetStringSlice("assignee") + if err != nil { + return fmt.Errorf("could not parse assignees: %w", err) + } + labelNames, err := cmd.Flags().GetStringSlice("label") + if err != nil { + return fmt.Errorf("could not parse labels: %w", err) + } + projectNames, err := cmd.Flags().GetStringSlice("project") + if err != nil { + return fmt.Errorf("could not parse projects: %w", err) + } + milestoneTitle, err := cmd.Flags().GetString("milestone") + if err != nil { + return fmt.Errorf("could not parse milestone: %w", err) + } + if isWeb, err := cmd.Flags().GetBool("web"); err == nil && isWeb { // TODO: move URL generation into GitHubRepository openURL := fmt.Sprintf("https://github.com/%s/issues/new", ghrepo.FullName(baseRepo)) @@ -423,6 +444,75 @@ func issueCreate(cmd *cobra.Command, args []string) error { "body": body, } + if len(assignees) > 0 || len(labelNames) > 0 || len(projectNames) > 0 || milestoneTitle != "" { + metadata, err := api.RepoMetadata(apiClient, baseRepo) + if err != nil { + return err + } + + var assigneeIDs []string + for _, assigneeLogin := range assignees { + var found bool + for _, u := range metadata.AssignableUsers.Nodes { + if strings.EqualFold(assigneeLogin, u.Login) { + assigneeIDs = append(assigneeIDs, u.ID) + found = true + break + } + } + if !found { + return fmt.Errorf("could not find user to assign: '%s'", assigneeLogin) + } + } + params["assigneeIds"] = assigneeIDs + + var labelIDs []string + for _, labelName := range labelNames { + var found bool + for _, l := range metadata.Labels.Nodes { + if strings.EqualFold(labelName, l.Name) { + labelIDs = append(labelIDs, l.ID) + found = true + break + } + } + if !found { + return fmt.Errorf("could not find label '%s'", labelName) + } + } + params["labelIds"] = labelIDs + + var projectIDs []string + for _, projectName := range projectNames { + var found bool + for _, p := range metadata.Projects.Nodes { + if strings.EqualFold(projectName, p.Name) { + projectIDs = append(projectIDs, p.ID) + found = true + break + } + } + if !found { + return fmt.Errorf("could not find project '%s'", projectName) + } + } + params["projectIds"] = projectIDs + + if milestoneTitle != "" { + var milestoneID string + for _, m := range metadata.Milestones.Nodes { + if strings.EqualFold(milestoneTitle, m.Title) { + milestoneID = m.ID + break + } + } + if milestoneID == "" { + return fmt.Errorf("could not find milestone '%s'", milestoneTitle) + } + params["milestoneId"] = milestoneID + } + } + newIssue, err := api.IssueCreate(apiClient, repo, params) if err != nil { return err