Rewrite issue develop command to fix numerous issues
This commit is contained in:
parent
5e8cc40f6c
commit
b59f3dc29f
6 changed files with 466 additions and 748 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] {<number> | <url>}",
|
||||
Use: "develop {<number> | <url>}",
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import (
|
|||
|
||||
func NewCmdProject(f *cmdutil.Factory) *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "project <command> [flags]",
|
||||
Use: "project <command>",
|
||||
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(`
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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\"",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue