From b59f3dc29f70984ae75ef238cb3e7ba2a9afaabc Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Fri, 30 Jun 2023 19:48:06 +0900 Subject: [PATCH] Rewrite issue develop command to fix numerous issues --- api/queries_branch_issue_reference.go | 198 ++----- pkg/cmd/issue/develop/develop.go | 238 +++----- pkg/cmd/issue/develop/develop_test.go | 770 +++++++++++--------------- pkg/cmd/project/project.go | 2 +- pkg/cmd/run/cancel/cancel.go | 2 +- pkg/cmd/run/cancel/cancel_test.go | 4 +- 6 files changed, 466 insertions(+), 748 deletions(-) diff --git a/api/queries_branch_issue_reference.go b/api/queries_branch_issue_reference.go index b85f94654..29c3ab322 100644 --- a/api/queries_branch_issue_reference.go +++ b/api/queries_branch_issue_reference.go @@ -4,63 +4,16 @@ import ( "fmt" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" ) type LinkedBranch struct { - ID string BranchName string - RepoUrl string + URL string } -// method to return url of linked branch, adds the branch name to the end of the repo url -func (b *LinkedBranch) Url() string { - return fmt.Sprintf("%s/tree/%s", b.RepoUrl, b.BranchName) -} - -func nameParam(params map[string]interface{}) string { - if params["name"] != "" { - return "name: $name," - } - return "" -} - -func nameArg(params map[string]interface{}) string { - if params["name"] != "" { - return "$name: String, " - } - return "" -} - -func CreateBranchIssueReference(client *Client, repo *Repository, params map[string]interface{}) (*LinkedBranch, error) { - query := fmt.Sprintf(` - mutation CreateLinkedBranch($issueId: ID!, $oid: GitObjectID!, %[1]s$repositoryId: ID) { - createLinkedBranch(input: { - issueId: $issueId, - %[2]s - oid: $oid, - repositoryId: $repositoryId - }) { - linkedBranch { - id - ref { - name - } - } - } - }`, nameArg(params), nameParam(params)) - - inputParams := map[string]interface{}{ - "repositoryId": repo.ID, - } - - for key, val := range params { - switch key { - case "issueId", "name", "oid": - inputParams[key] = val - } - } - - result := struct { +func CreateLinkedBranch(client *Client, host string, issueID, branchName, oid string) (string, error) { + var mutation struct { CreateLinkedBranch struct { LinkedBranch struct { ID string @@ -68,90 +21,72 @@ func CreateBranchIssueReference(client *Client, repo *Repository, params map[str Name string } } - } - }{} - - if err := client.GraphQL(repo.RepoHost(), query, inputParams, &result); err != nil { - return nil, err + } `graphql:"createLinkedBranch(input: $input)"` } - ref := LinkedBranch{ - ID: result.CreateLinkedBranch.LinkedBranch.ID, - BranchName: result.CreateLinkedBranch.LinkedBranch.Ref.Name, + input := githubv4.CreateLinkedBranchInput{ + IssueID: githubv4.ID(issueID), + Oid: githubv4.GitObjectID(oid), + } + if branchName != "" { + name := githubv4.String(branchName) + input.Name = &name + } + variables := map[string]interface{}{ + "input": input, } - return &ref, nil + err := client.Mutate(host, "CreateLinkedBranch", &mutation, variables) + if err != nil { + return "", err + } + + return mutation.CreateLinkedBranch.LinkedBranch.Ref.Name, nil } func ListLinkedBranches(client *Client, repo ghrepo.Interface, issueNumber int) ([]LinkedBranch, error) { - query := ` - query BranchIssueReferenceListLinkedBranches($repositoryName: String!, $repositoryOwner: String!, $issueNumber: Int!) { - repository(name: $repositoryName, owner: $repositoryOwner) { - issue(number: $issueNumber) { - linkedBranches(first: 30) { - edges { - node { - ref { - name - repository { - url - } - } - } - } - } - } - } - } - ` - - variables := map[string]interface{}{ - "repositoryName": repo.RepoName(), - "repositoryOwner": repo.RepoOwner(), - "issueNumber": issueNumber, - } - - result := struct { + var query struct { Repository struct { Issue struct { LinkedBranches struct { - Edges []struct { - Node struct { - Ref struct { - Name string - Repository struct { - NameWithOwner string - Url string - } + Nodes []struct { + Ref struct { + Name string + Repository struct { + Url string } } } - } - } - } - }{} + } `graphql:"linkedBranches(first: 30)"` + } `graphql:"issue(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } - if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + variables := map[string]interface{}{ + "number": githubv4.Int(issueNumber), + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + if err := client.Query(repo.RepoHost(), "ListLinkedBranches", &query, variables); err != nil { return []LinkedBranch{}, err } var branchNames []LinkedBranch - for _, edge := range result.Repository.Issue.LinkedBranches.Edges { + for _, node := range query.Repository.Issue.LinkedBranches.Nodes { branch := LinkedBranch{ - BranchName: edge.Node.Ref.Name, - RepoUrl: edge.Node.Ref.Repository.Url, + BranchName: node.Ref.Name, + URL: fmt.Sprintf("%s/tree/%s", node.Ref.Repository.Url, node.Ref.Name), } - branchNames = append(branchNames, branch) } return branchNames, nil } -// introspects the schema to see if we expose the LinkedBranch type -func CheckLinkedBranchFeature(client *Client, host string) (err error) { - var featureDetection struct { +func CheckLinkedBranchFeature(client *Client, host string) error { + var query struct { Name struct { Fields []struct { Name string @@ -159,42 +94,19 @@ func CheckLinkedBranchFeature(client *Client, host string) (err error) { } `graphql:"LinkedBranch: __type(name: \"LinkedBranch\")"` } - if err := client.Query(host, "LinkedBranch_fields", &featureDetection, nil); err != nil { + if err := client.Query(host, "LinkedBranchFeature", &query, nil); err != nil { return err } - if len(featureDetection.Name.Fields) == 0 { + if len(query.Name.Fields) == 0 { return fmt.Errorf("the `gh issue develop` command is not currently available") } return nil } -// This fetches the oids for the repo's default branch (`main`, etc) and the name the user might have provided in one shot. -func FindBaseOid(client *Client, repo *Repository, ref string) (string, string, error) { - query := ` - query BranchIssueReferenceFindBaseOid($repositoryName: String!, $repositoryOwner: String!, $ref: String!) { - repository(name: $repositoryName, owner: $repositoryOwner) { - defaultBranchRef { - target { - oid - } - } - ref(qualifiedName: $ref) { - target { - oid - } - } - } - }` - - variables := map[string]interface{}{ - "repositoryName": repo.Name, - "repositoryOwner": repo.RepoOwner(), - "ref": ref, - } - - result := struct { +func FindBaseOid(client *Client, repo ghrepo.Interface, ref string) (string, string, error) { + var query struct { Repository struct { DefaultBranchRef struct { Target struct { @@ -205,13 +117,19 @@ func FindBaseOid(client *Client, repo *Repository, ref string) (string, string, Target struct { Oid string } - } - } - }{} + } `graphql:"ref(qualifiedName: $ref)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } - if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + variables := map[string]interface{}{ + "ref": githubv4.String(ref), + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + if err := client.Query(repo.RepoHost(), "FindBaseOid", &query, variables); err != nil { return "", "", err } - return result.Repository.Ref.Target.Oid, result.Repository.DefaultBranchRef.Target.Oid, nil + return query.Repository.Ref.Target.Oid, query.Repository.DefaultBranchRef.Target.Oid, nil } diff --git a/pkg/cmd/issue/develop/develop.go b/pkg/cmd/issue/develop/develop.go index a32b414b6..21a81aa6a 100644 --- a/pkg/cmd/issue/develop/develop.go +++ b/pkg/cmd/issue/develop/develop.go @@ -9,7 +9,6 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" - "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/cmd/issue/shared" @@ -21,17 +20,15 @@ import ( type DevelopOptions struct { HttpClient func() (*http.Client, error) GitClient *git.Client - Config func() (config.Config, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Remotes func() (context.Remotes, error) - IssueRepoSelector string - IssueSelector string - Name string - BaseBranch string - Checkout bool - List bool + IssueSelector string + Name string + BaseBranch string + Checkout bool + List bool } func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.Command { @@ -39,200 +36,130 @@ func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra. IO: f.IOStreams, HttpClient: f.HttpClient, GitClient: f.GitClient, - Config: f.Config, BaseRepo: f.BaseRepo, Remotes: f.Remotes, } cmd := &cobra.Command{ - Use: "develop [flags] { | }", + Use: "develop { | }", Short: "Manage linked branches for an issue", Example: heredoc.Doc(` - $ gh issue develop --list 123 # list branches for issue 123 - $ 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" --base my-feature # create a branch for issue 123 based on the my-feature branch - $ gh issue develop 123 --checkout # fetch and checkout the branch for issue 123 after creating it + # List branches for issue 123 + $ gh issue develop --list 123 + + # List branches for issue 123 in repo cli/cli + $ gh issue develop --list --repo cli/cli 123 + + # Create a branch for issue 123 based on the my-feature branch + $ gh issue develop 123 --base my-feature + + # Create a branch for issue 123 and checkout it out + $ gh issue develop 123 --checkout `), Args: cmdutil.ExactArgs(1, "issue number or url is required"), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // This is all a hack to not break the issue-repo flag. It will be removed + // in the near future and this hack can be removed at the same time. + flags := cmd.Flags() + if flags.Changed("issue-repo") && !flags.Changed("repo") { + repo, _ := flags.GetString("issue-repo") + _ = flags.Set("repo", repo) + } + if cmd.Parent() != nil { + return cmd.Parent().PersistentPreRunE(cmd, args) + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { // support `-R, --repo` override opts.BaseRepo = f.BaseRepo + opts.IssueSelector = args[0] if runF != nil { return runF(opts) } - opts.IssueSelector = args[0] - if opts.List { - return developRunList(opts) - } - return developRunCreate(opts) + return developRun(opts) }, } + fl := cmd.Flags() fl.StringVarP(&opts.BaseBranch, "base", "b", "", "Name of the base branch you want to make your new branch from") fl.BoolVarP(&opts.Checkout, "checkout", "c", false, "Checkout the branch after creating it") - fl.StringVarP(&opts.IssueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository") fl.BoolVarP(&opts.List, "list", "l", false, "List linked branches for the issue") fl.StringVarP(&opts.Name, "name", "n", "", "Name of the branch to create") + + var issueRepoSelector string + fl.StringVarP(&issueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository") + _ = cmd.Flags().MarkDeprecated("issue-repo", "use `--repo` instead") + return cmd } -func developRunCreate(opts *DevelopOptions) (err error) { +func developRun(opts *DevelopOptions) 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() + issue, baseRepo, err := shared.IssueFromArgWithFields(httpClient, opts.BaseRepo, opts.IssueSelector, []string{"id", "number"}) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } + + apiClient := api.NewClientFromHTTP(httpClient) opts.IO.StartProgressIndicator() err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost()) + opts.IO.StopProgressIndicator() if err != nil { return err } - repo, err := api.GitHubRepo(apiClient, baseRepo) - if err != nil { - return err + if opts.List { + return developRunList(opts, apiClient, baseRepo, issue) } + return developRunCreate(opts, apiClient, baseRepo, issue) +} - issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo) - if err != nil { - return err - } - - // The mutation requires the issue id, not just its number - issue, _, err := shared.IssueFromArgWithFields(httpClient, func() (ghrepo.Interface, error) { return issueRepo, nil }, fmt.Sprint(issueNumber), []string{"id"}) - if err != nil { - return err - } - - // The mutation takes an oid instead of a branch name as it's a more stable reference - oid, default_branch_oid, err := api.FindBaseOid(apiClient, repo, opts.BaseBranch) +func developRunCreate(opts *DevelopOptions, apiClient *api.Client, baseRepo ghrepo.Interface, issue *api.Issue) error { + opts.IO.StartProgressIndicator() + oid, fallbackOID, err := api.FindBaseOid(apiClient, baseRepo, opts.BaseBranch) + opts.IO.StopProgressIndicator() if err != nil { return err } if oid == "" { - oid = default_branch_oid - } - - // get the oid of the branch from the base repo - params := map[string]interface{}{ - "issueId": issue.ID, - "name": opts.Name, - "oid": oid, - "repositoryId": repo.ID, - } - - ref, err := api.CreateBranchIssueReference(apiClient, repo, params) - opts.IO.StopProgressIndicator() - if err != nil { - return nil - } - - baseRepo.RepoHost() - fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", baseRepo.RepoHost(), baseRepo.RepoOwner(), baseRepo.RepoName(), ref.BranchName) - return checkoutBranch(opts, baseRepo, ref.BranchName) -} - -// If the issue is in the base repo, we can use the issue number directly. Otherwise, we need to use the issue's url or the IssueRepoSelector argument. -// If the repo from the URL doesn't match the IssueRepoSelector argument, we error. -func issueMetadata(issueSelector string, issueRepoSelector string, baseRepo ghrepo.Interface) (issueNumber int, issueFlagRepo ghrepo.Interface, err error) { - var targetRepo ghrepo.Interface - if issueRepoSelector != "" { - issueFlagRepo, err = ghrepo.FromFullNameWithHost(issueRepoSelector, baseRepo.RepoHost()) - if err != nil { - return 0, nil, err - } - } - - if issueFlagRepo != nil { - targetRepo = issueFlagRepo - } - - issueNumber, issueArgRepo, err := shared.IssueNumberAndRepoFromArg(issueSelector) - if err != nil { - return 0, nil, err - } - - if issueArgRepo != nil { - targetRepo = issueArgRepo - - if issueFlagRepo != nil { - differentOwner := (issueFlagRepo.RepoOwner() != issueArgRepo.RepoOwner()) - differentName := (issueFlagRepo.RepoName() != issueArgRepo.RepoName()) - if differentOwner || differentName { - return 0, nil, fmt.Errorf("issue repo in url %s/%s does not match the repo from --issue-repo %s/%s", issueArgRepo.RepoOwner(), issueArgRepo.RepoName(), issueFlagRepo.RepoOwner(), issueFlagRepo.RepoName()) - } - } - } - - if issueFlagRepo == nil && issueArgRepo == nil { - targetRepo = baseRepo - } - - if targetRepo == nil { - return 0, nil, fmt.Errorf("could not determine issue repo") - } - - return issueNumber, targetRepo, nil -} - -func printLinkedBranches(io *iostreams.IOStreams, branches []api.LinkedBranch) { - cs := io.ColorScheme() - table := tableprinter.New(io) - - for _, branch := range branches { - table.AddField(branch.BranchName, tableprinter.WithColor(cs.ColorFromString("cyan"))) - if io.CanPrompt() { - table.AddField(branch.Url()) - } - table.EndRow() - } - - _ = table.Render() -} - -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 + oid = fallbackOID } opts.IO.StartProgressIndicator() - - err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost()) - if err != nil { - return err - } - issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo) - if err != nil { - return err - } - - branches, err := api.ListLinkedBranches(apiClient, issueRepo, issueNumber) - if err != nil { - return err - } - + branchName, err := api.CreateLinkedBranch(apiClient, baseRepo.RepoHost(), issue.ID, opts.Name, oid) opts.IO.StopProgressIndicator() + if err != nil { + return err + } + + fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", baseRepo.RepoHost(), baseRepo.RepoOwner(), baseRepo.RepoName(), branchName) + + return checkoutBranch(opts, baseRepo, branchName) +} + +func developRunList(opts *DevelopOptions, apiClient *api.Client, baseRepo ghrepo.Interface, issue *api.Issue) error { + opts.IO.StartProgressIndicator() + branches, err := api.ListLinkedBranches(apiClient, baseRepo, issue.Number) + opts.IO.StopProgressIndicator() + if err != nil { + return err + } if len(branches) == 0 { - return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber)) + return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", baseRepo.RepoOwner(), baseRepo.RepoName(), issue.Number)) } if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber) + fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", baseRepo.RepoOwner(), baseRepo.RepoName(), issue.Number) } printLinkedBranches(opts.IO, branches) @@ -240,6 +167,17 @@ func developRunList(opts *DevelopOptions) (err error) { return nil } +func printLinkedBranches(io *iostreams.IOStreams, branches []api.LinkedBranch) { + cs := io.ColorScheme() + table := tableprinter.New(io) + for _, branch := range branches { + table.AddField(branch.BranchName, tableprinter.WithColor(cs.ColorFromString("cyan"))) + table.AddField(branch.URL) + table.EndRow() + } + _ = table.Render() +} + func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBranch string) (err error) { remotes, err := opts.Remotes() if err != nil { diff --git a/pkg/cmd/issue/develop/develop_test.go b/pkg/cmd/issue/develop/develop_test.go index 1f39353b2..1d8962847 100644 --- a/pkg/cmd/issue/develop/develop_test.go +++ b/pkg/cmd/issue/develop/develop_test.go @@ -1,418 +1,274 @@ package develop import ( + "bytes" "errors" "net/http" "testing" "github.com/cli/cli/v2/context" "github.com/cli/cli/v2/git" - "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/run" + "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" - "github.com/cli/cli/v2/test" + "github.com/google/shlex" "github.com/stretchr/testify/assert" ) -func Test_developRun(t *testing.T) { - featureEnabledPayload := `{ - "data": { - "LinkedBranch": { - "fields": [ - { - "name": "id" - }, - { - "name": "ref" - } - ] - } - } - }` +func TestNewCmdDevelop(t *testing.T) { + tests := []struct { + name string + input string + output DevelopOptions + wantStdout string + wantStderr string + wantErr bool + errMsg string + }{ + { + name: "no argument", + input: "", + output: DevelopOptions{}, + wantErr: true, + errMsg: "issue number or url is required", + }, + { + name: "issue number", + input: "1", + output: DevelopOptions{ + IssueSelector: "1", + }, + }, + { + name: "issue url", + input: "https://github.com/cli/cli/issues/1", + output: DevelopOptions{ + IssueSelector: "https://github.com/cli/cli/issues/1", + }, + }, + { + name: "base flag", + input: "1 --base feature", + output: DevelopOptions{ + IssueSelector: "1", + BaseBranch: "feature", + }, + }, + { + name: "checkout flag", + input: "1 --checkout", + output: DevelopOptions{ + IssueSelector: "1", + Checkout: true, + }, + }, + { + name: "list flag", + input: "1 --list", + output: DevelopOptions{ + IssueSelector: "1", + List: true, + }, + }, + { + name: "name flag", + input: "1 --name feature", + output: DevelopOptions{ + IssueSelector: "1", + Name: "feature", + }, + }, + { + name: "issue-repo flag", + input: "1 --issue-repo cli/cli", + output: DevelopOptions{ + IssueSelector: "1", + }, + wantStdout: "Flag --issue-repo has been deprecated, use `--repo` instead\n", + }, + } - featureDisabledPayload := `{ "data": { "LinkedBranch": null } }` + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ios, _, stdOut, stdErr := iostreams.Test() + f := &cmdutil.Factory{ + IOStreams: ios, + } + argv, err := shlex.Split(tt.input) + assert.NoError(t, err) + var gotOpts *DevelopOptions + cmd := NewCmdDevelop(f, func(opts *DevelopOptions) error { + gotOpts = opts + return nil + }) + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(stdOut) + cmd.SetErr(stdErr) + + _, err = cmd.ExecuteC() + if tt.wantErr { + assert.EqualError(t, err, tt.errMsg) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.output.IssueSelector, gotOpts.IssueSelector) + assert.Equal(t, tt.output.Name, gotOpts.Name) + assert.Equal(t, tt.output.BaseBranch, gotOpts.BaseBranch) + assert.Equal(t, tt.output.Checkout, gotOpts.Checkout) + assert.Equal(t, tt.output.List, gotOpts.List) + assert.Equal(t, tt.wantStdout, stdOut.String()) + assert.Equal(t, tt.wantStderr, stdErr.String()) + }) + } +} + +func TestDevelopRun(t *testing.T) { + featureEnabledPayload := `{"data":{"LinkedBranch":{"fields":[{"name":"id"},{"name":"ref"}]}}}` + featureDisabledPayload := `{"data":{"LinkedBranch":null}}` tests := []struct { name string - setup func(*DevelopOptions, *testing.T) func() + opts *DevelopOptions cmdStubs func(*run.CommandStubber) runStubs func(*run.CommandStubber) remotes map[string]string - askStubs func(*prompt.AskStubber) // TODO eventually migrate to PrompterMock httpStubs func(*httpmock.Registry, *testing.T) expectedOut string expectedErrOut string - expectedBrowse string wantErr string tty bool }{ { - name: "list branches for an issue", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "42" - opts.List = true - return func() {} + name: "returns an error when the feature is not supported by the API", + opts: &DevelopOptions{ + IssueSelector: "42", + List: true, }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`), ) reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), - httpmock.GraphQLQuery(`{ - "data": { - "repository": { - "issue": { - "linkedBranches": { - "edges": [ - { - "node": { - "ref": { - "name": "foo" - } - } - }, - { - "node": { - "ref": { - "name": "bar" - } - } - } - ] - } - } - } - } - } - `, func(query string, inputs map[string]interface{}) { - assert.Equal(t, float64(42), inputs["issueNumber"]) - assert.Equal(t, "OWNER", inputs["repositoryOwner"]) - assert.Equal(t, "REPO", inputs["repositoryName"]) - })) - }, - expectedOut: "foo\nbar\n", - }, - { - name: "list branches for an issue in tty", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "42" - opts.List = true - return func() {} - }, - tty: true, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), - httpmock.GraphQLQuery(`{ - "data": { - "repository": { - "issue": { - "linkedBranches": { - "edges": [ - { - "node": { - "ref": { - "name": "foo", - "repository": { - "url": "http://github.localhost/OWNER/REPO" - } - } - } - }, - { - "node": { - "ref": { - "name": "bar", - "repository": { - "url": "http://github.localhost/OWNER/OTHER-REPO" - } - } - } - } - ] - } - } - } - } - } - `, func(query string, inputs map[string]interface{}) { - assert.Equal(t, float64(42), inputs["issueNumber"]) - assert.Equal(t, "OWNER", inputs["repositoryOwner"]) - assert.Equal(t, "REPO", inputs["repositoryName"]) - })) - }, - expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo http://github.localhost/OWNER/REPO/tree/foo\nbar http://github.localhost/OWNER/OTHER-REPO/tree/bar\n", - }, - { - name: "list branches for an issue providing an issue url", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" - opts.List = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), - httpmock.GraphQLQuery(`{ - "data": { - "repository": { - "issue": { - "linkedBranches": { - "edges": [ - { - "node": { - "ref": { - "name": "foo" - } - } - }, - { - "node": { - "ref": { - "name": "bar" - } - } - } - ] - } - } - } - } - } - `, func(query string, inputs map[string]interface{}) { - assert.Equal(t, float64(42), inputs["issueNumber"]) - assert.Equal(t, "cli", inputs["repositoryOwner"]) - assert.Equal(t, "test-repo", inputs["repositoryName"]) - })) - }, - expectedOut: "foo\nbar\n", - }, - { - name: "list branches for an issue providing an issue repo", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "42" - opts.IssueRepoSelector = "cli/test-repo" - opts.List = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), - httpmock.GraphQLQuery(`{ - "data": { - "repository": { - "issue": { - "linkedBranches": { - "edges": [ - { - "node": { - "ref": { - "name": "foo" - } - } - }, - { - "node": { - "ref": { - "name": "bar" - } - } - } - ] - } - } - } - } - } - `, func(query string, inputs map[string]interface{}) { - assert.Equal(t, float64(42), inputs["issueNumber"]) - assert.Equal(t, "cli", inputs["repositoryOwner"]) - assert.Equal(t, "test-repo", inputs["repositoryName"]) - })) - }, - expectedOut: "foo\nbar\n", - }, - { - name: "list branches for an issue providing an issue url and specifying the same repo works", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" - opts.IssueRepoSelector = "cli/test-repo" - opts.List = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceListLinkedBranches\b`), - httpmock.GraphQLQuery(`{ - "data": { - "repository": { - "issue": { - "linkedBranches": { - "edges": [ - { - "node": { - "ref": { - "name": "foo" - } - } - }, - { - "node": { - "ref": { - "name": "bar" - } - } - } - ] - } - } - } - } - } - `, func(query string, inputs map[string]interface{}) { - assert.Equal(t, float64(42), inputs["issueNumber"]) - assert.Equal(t, "cli", inputs["repositoryOwner"]) - assert.Equal(t, "test-repo", inputs["repositoryName"]) - })) - }, - expectedOut: "foo\nbar\n", - }, - { - name: "list branches for an issue providing an issue url and specifying a different repo returns an error", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" - opts.IssueRepoSelector = "cli/other" - opts.List = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - }, - wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other", - }, - { - name: "returns an error when the feature isn't enabled in the GraphQL API", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" - opts.List = true - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), + httpmock.GraphQL(`query LinkedBranchFeature\b`), httpmock.StringResponse(featureDisabledPayload), ) }, wantErr: "the `gh issue develop` command is not currently available", }, { - name: "develop new branch with name specified", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.Name = "my-branch" - opts.BaseBranch = "main" - opts.IssueSelector = "123" - return func() {} - }, - remotes: map[string]string{ - "origin": "OWNER/REPO", + name: "list branches for an issue", + opts: &DevelopOptions{ + IssueSelector: "42", + List: true, }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), httpmock.StringResponse(featureEnabledPayload), ) reg.Register( - httpmock.GraphQL(`query RepositoryInfo\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "id": "REPOID", - "hasIssuesEnabled": true - } } }`), - ) - reg.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), - httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) - - reg.Register( - httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*issueId: \$issueId,\s+name: \$name,\s+oid: \$oid,`), - httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, - func(query string, inputs map[string]interface{}) { - assert.Equal(t, "REPOID", inputs["repositoryId"]) - assert.Equal(t, "my-branch", inputs["name"]) - assert.Equal(t, "yar", inputs["issueId"]) - }), - ) + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(42), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + })) }, - runStubs: func(cs *run.CommandStubber) { - cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") - }, - expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", + expectedOut: "foo\thttps://github.com/OWNER/REPO/tree/foo\nbar\thttps://github.com/OWNER/REPO/tree/bar\n", }, { - name: "develop new branch without a name provided omits the param from the mutation", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.Name = "" - opts.BaseBranch = "main" - opts.IssueSelector = "123" - return func() {} + name: "list branches for an issue in tty", + opts: &DevelopOptions{ + IssueSelector: "42", + List: true, + }, + tty: true, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/OTHER-REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(42), inputs["number"]) + assert.Equal(t, "OWNER", inputs["owner"]) + assert.Equal(t, "REPO", inputs["name"]) + })) + }, + expectedOut: "\nShowing linked branches for OWNER/REPO#42\n\nfoo https://github.com/OWNER/REPO/tree/foo\nbar https://github.com/OWNER/OTHER-REPO/tree/bar\n", + }, + { + name: "list branches for an issue providing an issue url", + opts: &DevelopOptions{ + IssueSelector: "https://github.com/cli/cli/issues/42", + List: true, + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":42}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query ListLinkedBranches\b`), + httpmock.GraphQLQuery(` + {"data":{"repository":{"issue":{"linkedBranches":{"nodes":[{"ref":{"name":"foo","repository":{"url":"https://github.com/OWNER/REPO"}}},{"ref":{"name":"bar","repository":{"url":"https://github.com/OWNER/OTHER-REPO"}}}]}}}}} + `, func(query string, inputs map[string]interface{}) { + assert.Equal(t, float64(42), inputs["number"]) + assert.Equal(t, "cli", inputs["owner"]) + assert.Equal(t, "cli", inputs["name"]) + })) + }, + expectedOut: "foo\thttps://github.com/OWNER/REPO/tree/foo\nbar\thttps://github.com/OWNER/OTHER-REPO/tree/bar\n", + }, + { + name: "develop new branch", + opts: &DevelopOptions{ + IssueSelector: "123", }, remotes: map[string]string{ "origin": "OWNER/REPO", }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), + httpmock.GraphQL(`query LinkedBranchFeature\b`), httpmock.StringResponse(featureEnabledPayload), ) reg.Register( - httpmock.GraphQL(`query RepositoryInfo\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "id": "REPOID", - "hasIssuesEnabled": true - } } }`), + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`), ) reg.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) + httpmock.GraphQL(`query FindBaseOid\b`), + httpmock.StringResponse(`{"data":{"repository":{"defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`), + ) reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), - httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) - - reg.Register( - httpmock.GraphQL(`(?s)mutation CreateLinkedBranch\b.*\$oid: GitObjectID!, \$repositoryId:.*issueId: \$issueId,\s+oid: \$oid,`), - httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-issue-1"} } } } }`, - func(query string, inputs map[string]interface{}) { - assert.Equal(t, "REPOID", inputs["repositoryId"]) - assert.Equal(t, "", inputs["name"]) - assert.Equal(t, "yar", inputs["issueId"]) + httpmock.GraphQL(`mutation CreateLinkedBranch\b`), + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`, + func(inputs map[string]interface{}) { + assert.Equal(t, "DEFAULTOID", inputs["oid"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) }), ) }, @@ -422,123 +278,148 @@ func Test_developRun(t *testing.T) { expectedOut: "github.com/OWNER/REPO/tree/my-issue-1\n", }, { - name: "develop providing an issue url and specifying a different repo returns an error", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.IssueSelector = "https://github.com/cli/test-repo/issues/42" - opts.IssueRepoSelector = "cli/other" - return func() {} - }, - httpStubs: func(reg *httpmock.Registry, t *testing.T) { - reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), - httpmock.StringResponse(featureEnabledPayload), - ) - reg.Register( - httpmock.GraphQL(`query RepositoryInfo\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "id": "REPOID", - "hasIssuesEnabled": true - } } }`), - ) - }, - wantErr: "issue repo in url cli/test-repo does not match the repo from --issue-repo cli/other", - }, - { - name: "develop new branch with checkout when the branch exists locally", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.Name = "my-branch" - opts.BaseBranch = "main" - opts.IssueSelector = "123" - opts.Checkout = true - return func() {} + name: "develop new branch with name and base specified", + opts: &DevelopOptions{ + Name: "my-branch", + BaseBranch: "main", + IssueSelector: "123", }, remotes: map[string]string{ "origin": "OWNER/REPO", }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), + httpmock.GraphQL(`query LinkedBranchFeature\b`), httpmock.StringResponse(featureEnabledPayload), ) reg.Register( - httpmock.GraphQL(`query RepositoryInfo\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "id": "REPOID", - "hasIssuesEnabled": true - } } }`), + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled":true,"issue":{"id":"SOMEID","number":123,"title":"my issue"}}}}`), ) reg.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), - httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) - + httpmock.GraphQL(`query FindBaseOid\b`), + httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"OID"}}}}}`)) reg.Register( httpmock.GraphQL(`mutation CreateLinkedBranch\b`), - httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, - func(query string, inputs map[string]interface{}) { - assert.Equal(t, "REPOID", inputs["repositoryId"]) + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, + func(inputs map[string]interface{}) { assert.Equal(t, "my-branch", inputs["name"]) - assert.Equal(t, "yar", inputs["issueId"]) + assert.Equal(t, "OID", inputs["oid"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) }), ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") + }, + expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", + }, + { + name: "develop new branch outside of local git repo", + opts: &DevelopOptions{ + IssueSelector: "https://github.com/cli/cli/issues/123", + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query FindBaseOid\b`), + httpmock.StringResponse(`{"data":{"repository":{"defaultBranchRef":{"target":{"oid":"DEFAULTOID"}},"ref":{"target":{"oid":""}}}}}`), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateLinkedBranch\b`), + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-issue-1"}}}}}`, + func(inputs map[string]interface{}) { + assert.Equal(t, "DEFAULTOID", inputs["oid"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) + }), + ) + }, + expectedOut: "github.com/cli/cli/tree/my-issue-1\n", + }, + { + name: "develop new branch with checkout when local branch exists", + opts: &DevelopOptions{ + Name: "my-branch", + IssueSelector: "123", + Checkout: true, + }, + remotes: map[string]string{ + "origin": "OWNER/REPO", + }, + httpStubs: func(reg *httpmock.Registry, t *testing.T) { + reg.Register( + httpmock.GraphQL(`query LinkedBranchFeature\b`), + httpmock.StringResponse(featureEnabledPayload), + ) + reg.Register( + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`), + ) + reg.Register( + httpmock.GraphQL(`query FindBaseOid\b`), + httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"OID"}}}}}`), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateLinkedBranch\b`), + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, + func(inputs map[string]interface{}) { + assert.Equal(t, "my-branch", inputs["name"]) + assert.Equal(t, "OID", inputs["oid"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) + }), + ) + }, + runStubs: func(cs *run.CommandStubber) { + cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") + cs.Register(`git rev-parse --verify refs/heads/my-branch`, 0, "") cs.Register(`git checkout my-branch`, 0, "") cs.Register(`git pull --ff-only origin my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", }, { - name: "develop new branch with checkout when the branch does not exist locally", - setup: func(opts *DevelopOptions, t *testing.T) func() { - opts.Name = "my-branch" - opts.BaseBranch = "main" - opts.IssueSelector = "123" - opts.Checkout = true - return func() {} + name: "develop new branch with checkout when local branch does not exist", + opts: &DevelopOptions{ + Name: "my-branch", + IssueSelector: "123", + Checkout: true, }, remotes: map[string]string{ "origin": "OWNER/REPO", }, httpStubs: func(reg *httpmock.Registry, t *testing.T) { reg.Register( - httpmock.GraphQL(`query LinkedBranch_fields\b`), + httpmock.GraphQL(`query LinkedBranchFeature\b`), httpmock.StringResponse(featureEnabledPayload), ) reg.Register( - httpmock.GraphQL(`query RepositoryInfo\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "id": "REPOID", - "hasIssuesEnabled": true - } } }`), + httpmock.GraphQL(`query IssueByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{"hasIssuesEnabled":true,"issue":{"id": "SOMEID","number":123,"title":"my issue"}}}}`), ) reg.Register( - httpmock.GraphQL(`query IssueByNumber\b`), - httpmock.StringResponse(`{"data":{"repository":{ "hasIssuesEnabled": true, "issue":{"id": "yar", "number":123, "title":"my issue"} }}}`)) - reg.Register( - httpmock.GraphQL(`query BranchIssueReferenceFindBaseOid\b`), - httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"123"}}}}}`)) - + httpmock.GraphQL(`query FindBaseOid\b`), + httpmock.StringResponse(`{"data":{"repository":{"ref":{"target":{"oid":"OID"}}}}}`), + ) reg.Register( httpmock.GraphQL(`mutation CreateLinkedBranch\b`), - httpmock.GraphQLQuery(`{ "data": { "createLinkedBranch": { "linkedBranch": {"id": "2", "ref": {"name": "my-branch"} } } } }`, - func(query string, inputs map[string]interface{}) { - assert.Equal(t, "REPOID", inputs["repositoryId"]) + httpmock.GraphQLMutation(`{"data":{"createLinkedBranch":{"linkedBranch":{"id":"2","ref":{"name":"my-branch"}}}}}`, + func(inputs map[string]interface{}) { assert.Equal(t, "my-branch", inputs["name"]) - assert.Equal(t, "yar", inputs["issueId"]) + assert.Equal(t, "OID", inputs["oid"]) + assert.Equal(t, "SOMEID", inputs["issueId"]) }), ) }, runStubs: func(cs *run.CommandStubber) { - cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "") cs.Register(`git fetch origin \+refs/heads/my-branch:refs/remotes/origin/my-branch`, 0, "") + cs.Register(`git rev-parse --verify refs/heads/my-branch`, 1, "") cs.Register(`git checkout -b my-branch --track origin/my-branch`, 0, "") }, expectedOut: "github.com/OWNER/REPO/tree/my-branch\n", @@ -546,16 +427,18 @@ func Test_developRun(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + opts := tt.opts + reg := &httpmock.Registry{} defer reg.Verify(t) if tt.httpStubs != nil { tt.httpStubs(reg, t) } - - opts := DevelopOptions{} + opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } ios, _, stdout, stderr := iostreams.Test() - ios.SetStdoutTTY(tt.tty) ios.SetStdinTTY(tt.tty) ios.SetStderrTTY(tt.tty) @@ -564,12 +447,6 @@ func Test_developRun(t *testing.T) { opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil } - opts.HttpClient = func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - } - opts.Config = func() (config.Config, error) { - return config.NewBlankConfig(), nil - } opts.Remotes = func() (context.Remotes, error) { if len(tt.remotes) == 0 { @@ -600,29 +477,14 @@ func Test_developRun(t *testing.T) { tt.runStubs(cmdStubs) } - cleanSetup := func() {} - if tt.setup != nil { - cleanSetup = tt.setup(&opts, t) - } - defer cleanSetup() - - var err error - if opts.List { - err = developRunList(&opts) - } else { - - err = developRunCreate(&opts) - } - output := &test.CmdOut{ - OutBuf: stdout, - ErrBuf: stderr, - } + err := developRun(opts) if tt.wantErr != "" { assert.EqualError(t, err, tt.wantErr) + return } else { assert.NoError(t, err) - assert.Equal(t, tt.expectedOut, output.String()) - assert.Equal(t, tt.expectedErrOut, output.Stderr()) + assert.Equal(t, tt.expectedOut, stdout.String()) + assert.Equal(t, tt.expectedErrOut, stderr.String()) } }) } diff --git a/pkg/cmd/project/project.go b/pkg/cmd/project/project.go index 520463f64..dc3365696 100644 --- a/pkg/cmd/project/project.go +++ b/pkg/cmd/project/project.go @@ -24,7 +24,7 @@ import ( func NewCmdProject(f *cmdutil.Factory) *cobra.Command { var cmd = &cobra.Command{ - Use: "project [flags]", + Use: "project ", Short: "Work with GitHub Projects.", Long: "Work with GitHub Projects. Note that the token you are using must have 'project' scope, which is not set by default. You can verify your token scope by running 'gh auth status' and add the project scope by running 'gh auth refresh -s project'.", Example: heredoc.Doc(` diff --git a/pkg/cmd/run/cancel/cancel.go b/pkg/cmd/run/cancel/cancel.go index c29de543a..3e6cd9ffb 100644 --- a/pkg/cmd/run/cancel/cancel.go +++ b/pkg/cmd/run/cancel/cancel.go @@ -63,7 +63,7 @@ func runCancel(opts *CancelOptions) error { if opts.RunID != "" { _, err := strconv.Atoi(opts.RunID) if err != nil { - return fmt.Errorf("invalid run_id %#v", opts.RunID) + return fmt.Errorf("invalid run-id %#v", opts.RunID) } } httpClient, err := opts.HttpClient() diff --git a/pkg/cmd/run/cancel/cancel_test.go b/pkg/cmd/run/cancel/cancel_test.go index 78906ad2e..d3f580870 100644 --- a/pkg/cmd/run/cancel/cancel_test.go +++ b/pkg/cmd/run/cancel/cancel_test.go @@ -204,14 +204,14 @@ func TestRunCancel(t *testing.T) { wantOut: "✓ Request to cancel workflow 1234 submitted.\n", }, { - name: "invalid run_id", + name: "invalid run-id", opts: &CancelOptions{ RunID: "12\n34", }, httpStubs: func(reg *httpmock.Registry) { }, wantErr: true, - errMsg: "invalid run_id \"12\\n34\"", + errMsg: "invalid run-id \"12\\n34\"", }, }