Merge pull request #6254 from Bestra/create-linked-branch-command
Add `gh issue develop` command
This commit is contained in:
commit
a20192943d
6 changed files with 1129 additions and 4 deletions
|
|
@ -19,7 +19,7 @@ const (
|
|||
authorization = "Authorization"
|
||||
cacheTTL = "X-GH-CACHE-TTL"
|
||||
graphqlFeatures = "GraphQL-Features"
|
||||
mergeQueue = "merge_queue"
|
||||
features = "merge_queue"
|
||||
userAgent = "User-Agent"
|
||||
)
|
||||
|
||||
|
|
@ -55,7 +55,7 @@ func (err HTTPError) ScopesSuggestion() string {
|
|||
// GraphQLError will be returned, but the data will also be parsed into the receiver.
|
||||
func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error {
|
||||
opts := clientOptions(hostname, c.http.Transport)
|
||||
opts.Headers[graphqlFeatures] = mergeQueue
|
||||
opts.Headers[graphqlFeatures] = features
|
||||
gqlClient, err := gh.GQLClient(&opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -67,7 +67,7 @@ func (c Client) GraphQL(hostname string, query string, variables map[string]inte
|
|||
// GraphQLError will be returned, but the data will also be parsed into the receiver.
|
||||
func (c Client) Mutate(hostname, name string, mutation interface{}, variables map[string]interface{}) error {
|
||||
opts := clientOptions(hostname, c.http.Transport)
|
||||
opts.Headers[graphqlFeatures] = mergeQueue
|
||||
opts.Headers[graphqlFeatures] = features
|
||||
gqlClient, err := gh.GQLClient(&opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -79,7 +79,7 @@ func (c Client) Mutate(hostname, name string, mutation interface{}, variables ma
|
|||
// GraphQLError will be returned, but the data will also be parsed into the receiver.
|
||||
func (c Client) Query(hostname, name string, query interface{}, variables map[string]interface{}) error {
|
||||
opts := clientOptions(hostname, c.http.Transport)
|
||||
opts.Headers[graphqlFeatures] = mergeQueue
|
||||
opts.Headers[graphqlFeatures] = features
|
||||
gqlClient, err := gh.GQLClient(&opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
|||
219
api/queries_branch_issue_reference.go
Normal file
219
api/queries_branch_issue_reference.go
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cli/cli/v2/internal/ghrepo"
|
||||
)
|
||||
|
||||
type LinkedBranch struct {
|
||||
ID string
|
||||
BranchName string
|
||||
RepoUrl 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 {
|
||||
CreateLinkedBranch struct {
|
||||
LinkedBranch struct {
|
||||
ID string
|
||||
Ref struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, inputParams, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ref := LinkedBranch{
|
||||
ID: result.CreateLinkedBranch.LinkedBranch.ID,
|
||||
BranchName: result.CreateLinkedBranch.LinkedBranch.Ref.Name,
|
||||
}
|
||||
return &ref, 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 {
|
||||
Repository struct {
|
||||
Issue struct {
|
||||
LinkedBranches struct {
|
||||
Edges []struct {
|
||||
Node struct {
|
||||
Ref struct {
|
||||
Name string
|
||||
Repository struct {
|
||||
NameWithOwner string
|
||||
Url string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &result)
|
||||
var branchNames []LinkedBranch
|
||||
if err != nil {
|
||||
return branchNames, err
|
||||
}
|
||||
|
||||
for _, edge := range result.Repository.Issue.LinkedBranches.Edges {
|
||||
branch := LinkedBranch{
|
||||
BranchName: edge.Node.Ref.Name,
|
||||
RepoUrl: edge.Node.Ref.Repository.Url,
|
||||
}
|
||||
|
||||
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 {
|
||||
Name struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
}
|
||||
} `graphql:"LinkedBranch: __type(name: \"LinkedBranch\")"`
|
||||
}
|
||||
|
||||
err = client.Query(host, "LinkedBranch_fields", &featureDetection, nil)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(featureDetection.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 {
|
||||
Repository struct {
|
||||
DefaultBranchRef struct {
|
||||
Target struct {
|
||||
Oid string
|
||||
}
|
||||
}
|
||||
Ref struct {
|
||||
Target struct {
|
||||
Oid string
|
||||
}
|
||||
}
|
||||
}
|
||||
}{}
|
||||
|
||||
err := client.GraphQL(repo.RepoHost(), query, variables, &result)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return result.Repository.Ref.Target.Oid, result.Repository.DefaultBranchRef.Target.Oid, nil
|
||||
}
|
||||
284
pkg/cmd/issue/develop/develop.go
Normal file
284
pkg/cmd/issue/develop/develop.go
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
package develop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"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/run"
|
||||
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type DevelopOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
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
|
||||
}
|
||||
|
||||
func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.Command {
|
||||
opts := &DevelopOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
BaseRepo: f.BaseRepo,
|
||||
Remotes: f.Remotes,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "develop [flags] {<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
|
||||
`),
|
||||
Args: cmdutil.ExactArgs(1, "issue number or url is required"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
opts.IssueSelector = args[0]
|
||||
if opts.List {
|
||||
return developRunList(opts)
|
||||
}
|
||||
return developRunCreate(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")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func developRunCreate(opts *DevelopOptions) (err error) {
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient := api.NewClientFromHTTP(httpClient)
|
||||
baseRepo, err := opts.BaseRepo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo, err := api.GitHubRepo(apiClient, baseRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
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 ref != nil {
|
||||
baseRepo.RepoHost()
|
||||
fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", baseRepo.RepoHost(), baseRepo.RepoOwner(), baseRepo.RepoName(), ref.BranchName)
|
||||
|
||||
if opts.Checkout {
|
||||
return checkoutBranch(opts, baseRepo, ref.BranchName)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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 := utils.NewTablePrinter(io)
|
||||
|
||||
for _, branch := range branches {
|
||||
table.AddField(branch.BranchName, nil, cs.ColorFromString("cyan"))
|
||||
if table.IsTTY() {
|
||||
table.AddField(branch.Url(), nil, nil)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
opts.IO.StopProgressIndicator()
|
||||
if len(branches) == 0 {
|
||||
return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber))
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber)
|
||||
}
|
||||
|
||||
printLinkedBranches(opts.IO, branches)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBranch string) (err error) {
|
||||
|
||||
remotes, err := opts.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if git.HasLocalBranch(checkoutBranch) {
|
||||
if err := git.CheckoutBranch(checkoutBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
gitFetch, err := git.GitCommand("fetch", "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gitFetch.Stdout = opts.IO.Out
|
||||
gitFetch.Stderr = opts.IO.ErrOut
|
||||
err = run.PrepareCmd(gitFetch).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := git.CheckoutNewBranch(baseRemote.Name, checkoutBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.Pull(baseRemote.Name, checkoutBranch); err != nil {
|
||||
_, _ = fmt.Fprintf(opts.IO.ErrOut, "%s warning: not possible to fast-forward to: %q\n", opts.IO.ColorScheme().WarningIcon(), checkoutBranch)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
604
pkg/cmd/issue/develop/develop_test.go
Normal file
604
pkg/cmd/issue/develop/develop_test.go
Normal file
|
|
@ -0,0 +1,604 @@
|
|||
package develop
|
||||
|
||||
import (
|
||||
"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/httpmock"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/cli/cli/v2/pkg/prompt"
|
||||
"github.com/cli/cli/v2/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_developRun(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()
|
||||
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() {}
|
||||
},
|
||||
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, "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.StringResponse(featureDisabledPayload),
|
||||
)
|
||||
},
|
||||
wantErr: "the `gh issue develop` command is not currently available",
|
||||
},
|
||||
{name: "develop new branch with a name provided",
|
||||
setup: func(opts *DevelopOptions, t *testing.T) func() {
|
||||
opts.Name = "my-branch"
|
||||
opts.BaseBranch = "main"
|
||||
opts.IssueSelector = "123"
|
||||
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
|
||||
} } }`),
|
||||
)
|
||||
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"])
|
||||
}),
|
||||
)
|
||||
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\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() {}
|
||||
},
|
||||
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
|
||||
} } }`),
|
||||
)
|
||||
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.*\$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"])
|
||||
}),
|
||||
)
|
||||
|
||||
},
|
||||
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() {}
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
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
|
||||
} } }`),
|
||||
)
|
||||
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(`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"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
assert.Equal(t, "yar", inputs["issueId"])
|
||||
}),
|
||||
)
|
||||
|
||||
},
|
||||
runStubs: func(cs *run.CommandStubber) {
|
||||
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() {}
|
||||
},
|
||||
remotes: map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
},
|
||||
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
|
||||
} } }`),
|
||||
)
|
||||
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(`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"])
|
||||
assert.Equal(t, "my-branch", inputs["name"])
|
||||
assert.Equal(t, "yar", 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 checkout -b my-branch --track origin/my-branch`, 0, "")
|
||||
cs.Register(`git pull --ff-only origin my-branch`, 0, "")
|
||||
},
|
||||
expectedOut: "github.com/OWNER/REPO/tree/my-branch\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
if tt.httpStubs != nil {
|
||||
tt.httpStubs(reg, t)
|
||||
}
|
||||
|
||||
opts := DevelopOptions{}
|
||||
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
|
||||
ios.SetStdoutTTY(tt.tty)
|
||||
ios.SetStdinTTY(tt.tty)
|
||||
ios.SetStderrTTY(tt.tty)
|
||||
opts.IO = ios
|
||||
|
||||
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 {
|
||||
return nil, errors.New("no remotes")
|
||||
}
|
||||
var remotes context.Remotes
|
||||
for name, repo := range tt.remotes {
|
||||
r, err := ghrepo.FromFullName(repo)
|
||||
if err != nil {
|
||||
return remotes, err
|
||||
}
|
||||
remotes = append(remotes, &context.Remote{
|
||||
Remote: &git.Remote{Name: name},
|
||||
Repo: r,
|
||||
})
|
||||
}
|
||||
return remotes, nil
|
||||
}
|
||||
|
||||
cmdStubs, cmdTeardown := run.Stub()
|
||||
defer cmdTeardown(t)
|
||||
if tt.runStubs != nil {
|
||||
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,
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedOut, output.String())
|
||||
assert.Equal(t, tt.expectedErrOut, output.Stderr())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
cmdComment "github.com/cli/cli/v2/pkg/cmd/issue/comment"
|
||||
cmdCreate "github.com/cli/cli/v2/pkg/cmd/issue/create"
|
||||
cmdDelete "github.com/cli/cli/v2/pkg/cmd/issue/delete"
|
||||
cmdDevelop "github.com/cli/cli/v2/pkg/cmd/issue/develop"
|
||||
cmdEdit "github.com/cli/cli/v2/pkg/cmd/issue/edit"
|
||||
cmdList "github.com/cli/cli/v2/pkg/cmd/issue/list"
|
||||
cmdPin "github.com/cli/cli/v2/pkg/cmd/issue/pin"
|
||||
|
|
@ -50,6 +51,7 @@ func NewCmdIssue(f *cmdutil.Factory) *cobra.Command {
|
|||
cmd.AddCommand(cmdDelete.NewCmdDelete(f, nil))
|
||||
cmd.AddCommand(cmdEdit.NewCmdEdit(f, nil))
|
||||
cmd.AddCommand(cmdTransfer.NewCmdTransfer(f, nil))
|
||||
cmd.AddCommand(cmdDevelop.NewCmdDevelop(f, nil))
|
||||
cmd.AddCommand(cmdPin.NewCmdPin(f, nil))
|
||||
cmd.AddCommand(cmdUnpin.NewCmdUnpin(f, nil))
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,22 @@ func issueMetadataFromURL(s string) (int, ghrepo.Interface) {
|
|||
return issueNumber, repo
|
||||
}
|
||||
|
||||
// Returns the issue number and repo if the issue URL is provided.
|
||||
// If only the issue number is provided, returns the number and nil repo.
|
||||
func IssueNumberAndRepoFromArg(arg string) (int, ghrepo.Interface, error) {
|
||||
issueNumber, baseRepo := issueMetadataFromURL(arg)
|
||||
|
||||
if issueNumber == 0 {
|
||||
var err error
|
||||
issueNumber, err = strconv.Atoi(strings.TrimPrefix(arg, "#"))
|
||||
if err != nil {
|
||||
return 0, nil, fmt.Errorf("invalid issue format: %q", arg)
|
||||
}
|
||||
}
|
||||
|
||||
return issueNumber, baseRepo, nil
|
||||
}
|
||||
|
||||
type PartialLoadError struct {
|
||||
error
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue