diff --git a/api/queries_branch_issue_reference.go b/api/queries_branch_issue_reference.go index a05b88476..f1265b484 100644 --- a/api/queries_branch_issue_reference.go +++ b/api/queries_branch_issue_reference.go @@ -1,5 +1,7 @@ package api +import "github.com/cli/cli/v2/internal/ghrepo" + type BranchIssueReference struct { ID string BranchName string @@ -58,6 +60,61 @@ func CreateBranchIssueReference(client *Client, repo *Repository, params map[str } +func ListLinkedBranches(client *Client, repo ghrepo.Interface, issueNumber int) ([]string, error) { + // query uses name and owner + query := ` + query BranchIssueReferenceListLinkedBranches($repositoryName: String!, $repositoryOwner: String!, $issueNumber: Int!) { + repository(name: $repositoryName, owner: $repositoryOwner) { + issue(number: $issueNumber) { + linkedBranches(first: 30) { + edges { + node { + ref { + name + } + } + } + } + } + } + } + ` + variables := map[string]interface{}{ + "repositoryName": repo.RepoName(), + "repositoryOwner": repo.RepoOwner(), + "issueNumber": issueNumber, + } + + result := struct { + Repository struct { + Issue struct { + LinkedBranches struct { + Edges []struct { + Node struct { + Ref struct { + Name string + } + } + } + } + } + } + }{} + + err := client.GraphQL(repo.RepoHost(), query, variables, &result) + var branchNames []string + if err != nil { + return branchNames, err + } + + for _, edge := range result.Repository.Issue.LinkedBranches.Edges { + branchNames = append(branchNames, edge.Node.Ref.Name) + } + + return branchNames, nil + +} + func FindBaseOid(client *Client, repo *Repository, ref string) (string, string, error) { query := ` query BranchIssueReferenceFindBaseOid($repositoryName: String!, $repositoryOwner: String!, $ref: String!) { diff --git a/pkg/cmd/issue/develop/develop.go b/pkg/cmd/issue/develop/develop.go index 49b5c1874..f5681108e 100644 --- a/pkg/cmd/issue/develop/develop.go +++ b/pkg/cmd/issue/develop/develop.go @@ -24,11 +24,12 @@ type DevelopOptions struct { BaseRepo func() (ghrepo.Interface, error) Remotes func() (context.Remotes, error) - IssueRepo string - IssueSelector string - Name string - BaseBranch string - Checkout bool + IssueRepoSelector string + IssueSelector string + Name string + BaseBranch string + Checkout bool + List bool } func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.Command { @@ -45,23 +46,28 @@ func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra. Short: "Manage linked branches for an issue", Example: heredoc.Doc(` $ gh issue develop --list 123 # list branches for issue 123 - $ gh issue develop --issue-repo "github/cli" 123 list branches for issue 123 in repo "github/cli" + $ gh issue develop --list --issue-repo "github/cli" 123 list branches for issue 123 in repo "github/cli" + $ gh issue develop --list https://github.com/github/cli/issues/123 # list branches for issue 123 in repo "github/cli" $ gh issue develop 123 --name "my-branch" --head main $ gh issue develop 123 --checkout # checkout the branch for issue 123 after creating it `), - Args: cmdutil.ExactArgs(1, "issue number is required"), + Args: cmdutil.ExactArgs(1, "issue number or url is required"), RunE: func(cmd *cobra.Command, args []string) error { if runF != nil { return runF(opts) } opts.IssueSelector = args[0] + if opts.List { + return developRunList(opts) + } return developRun(opts) }, } fl := cmd.Flags() fl.StringVarP(&opts.BaseBranch, "base-branch", "b", "", "Name of the base branch") fl.BoolVarP(&opts.Checkout, "checkout", "c", false, "Checkout the branch after creating it") - fl.StringVarP(&opts.IssueRepo, "issue-repo", "i", "", "Name or URL of the issue's repository") + fl.StringVarP(&opts.IssueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository") + fl.BoolVarP(&opts.List, "list", "l", false, "List branches for the issue") fl.StringVarP(&opts.Name, "name", "n", "", "Name of the branch to create") return cmd } @@ -120,6 +126,58 @@ func developRun(opts *DevelopOptions) (err error) { return } +func developRunList(opts *DevelopOptions) (err error) { + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + apiClient := api.NewClientFromHTTP(httpClient) + baseRepo, err := opts.BaseRepo() + if err != nil { + return err + } + + opts.IO.StartProgressIndicator() + var issueRepo ghrepo.Interface + if opts.IssueRepoSelector != "" { + issueRepo, err = ghrepo.FromFullNameWithHost(opts.IssueRepoSelector, baseRepo.RepoHost()) + if err != nil { + return err + } + } + + targetRepo := baseRepo + if issueRepo != nil { + targetRepo = issueRepo + } + issueNumber, issueRepo, err := shared.IssueNumberAndRepoFromArg(opts.IssueSelector, targetRepo) + if err != nil { + return err + } + + branches, err := api.ListLinkedBranches(apiClient, issueRepo, issueNumber) + if err != nil { + return err + } + + opts.IO.StopProgressIndicator() + if len(branches) == 0 { + return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber)) + } + + if opts.IO.IsStdoutTTY() { + fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber) + } + + for _, branch := range branches { + fmt.Fprintf(opts.IO.Out, "%s\n", branch) + } + + return nil + +} + func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBranch string) (err error) { remotes, err := opts.Remotes() diff --git a/pkg/cmd/issue/list/http.go b/pkg/cmd/issue/list/http.go index db7633157..8dfcfe65e 100644 --- a/pkg/cmd/issue/list/http.go +++ b/pkg/cmd/issue/list/http.go @@ -22,6 +22,7 @@ func listIssues(client *api.Client, repo ghrepo.Interface, filters prShared.Filt } fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields)) + // TODO try to paginate like this query := fragments + ` query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String) { repository(owner: $owner, name: $repo) { diff --git a/pkg/cmd/issue/shared/lookup.go b/pkg/cmd/issue/shared/lookup.go index e802cada0..3a4d6594e 100644 --- a/pkg/cmd/issue/shared/lookup.go +++ b/pkg/cmd/issue/shared/lookup.go @@ -60,6 +60,26 @@ func issueMetadataFromURL(s string) (int, ghrepo.Interface) { return issueNumber, repo } +//TODO: Returns the issue number and base repo if the issue URL is provided, falling back to the +// supplied repo if the base repo is not specified in the URL. +func IssueNumberAndRepoFromArg(arg string, fallbackRepo ghrepo.Interface) (int, ghrepo.Interface, error) { + issueNumber, baseRepo := issueMetadataFromURL(arg) + + if issueNumber == 0 { + var err error + issueNumber, err = strconv.Atoi(strings.TrimPrefix(arg, "#")) + if err != nil { + return 0, nil, fmt.Errorf("invalid issue format: %q", arg) + } + } + + if baseRepo == nil { + baseRepo = fallbackRepo + } + + return issueNumber, baseRepo, nil +} + type PartialLoadError struct { error }