github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/develop/develop.go (about)

     1  package develop
     2  
     3  import (
     4  	ctx "context"
     5  	"fmt"
     6  	"net/http"
     7  
     8  	"github.com/MakeNowJust/heredoc"
     9  	"github.com/ungtb10d/cli/v2/api"
    10  	"github.com/ungtb10d/cli/v2/context"
    11  	"github.com/ungtb10d/cli/v2/git"
    12  	"github.com/ungtb10d/cli/v2/internal/config"
    13  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    14  	"github.com/ungtb10d/cli/v2/internal/tableprinter"
    15  	"github.com/ungtb10d/cli/v2/pkg/cmd/issue/shared"
    16  	"github.com/ungtb10d/cli/v2/pkg/cmdutil"
    17  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  type DevelopOptions struct {
    22  	HttpClient func() (*http.Client, error)
    23  	GitClient  *git.Client
    24  	Config     func() (config.Config, error)
    25  	IO         *iostreams.IOStreams
    26  	BaseRepo   func() (ghrepo.Interface, error)
    27  	Remotes    func() (context.Remotes, error)
    28  
    29  	IssueRepoSelector string
    30  	IssueSelector     string
    31  	Name              string
    32  	BaseBranch        string
    33  	Checkout          bool
    34  	List              bool
    35  }
    36  
    37  func NewCmdDevelop(f *cmdutil.Factory, runF func(*DevelopOptions) error) *cobra.Command {
    38  	opts := &DevelopOptions{
    39  		IO:         f.IOStreams,
    40  		HttpClient: f.HttpClient,
    41  		GitClient:  f.GitClient,
    42  		Config:     f.Config,
    43  		BaseRepo:   f.BaseRepo,
    44  		Remotes:    f.Remotes,
    45  	}
    46  
    47  	cmd := &cobra.Command{
    48  		Use:   "develop [flags] {<number> | <url>}",
    49  		Short: "Manage linked branches for an issue",
    50  		Example: heredoc.Doc(`
    51  			$ gh issue develop --list 123 # list branches for issue 123
    52  			$ gh issue develop --list --issue-repo "github/cli" 123 # list branches for issue 123 in repo "github/cli"
    53  			$ gh issue develop --list https://github.com/github/cli/issues/123 # list branches for issue 123 in repo "github/cli"
    54  			$ gh issue develop 123 --name "my-branch" --base my-feature # create a branch for issue 123 based on the my-feature branch
    55  			$ gh issue develop 123 --checkout # fetch and checkout the branch for issue 123 after creating it
    56  			`),
    57  		Args: cmdutil.ExactArgs(1, "issue number or url is required"),
    58  		RunE: func(cmd *cobra.Command, args []string) error {
    59  			if runF != nil {
    60  				return runF(opts)
    61  			}
    62  			opts.IssueSelector = args[0]
    63  			if opts.List {
    64  				return developRunList(opts)
    65  			}
    66  			return developRunCreate(opts)
    67  		},
    68  	}
    69  	fl := cmd.Flags()
    70  	fl.StringVarP(&opts.BaseBranch, "base", "b", "", "Name of the base branch you want to make your new branch from")
    71  	fl.BoolVarP(&opts.Checkout, "checkout", "c", false, "Checkout the branch after creating it")
    72  	fl.StringVarP(&opts.IssueRepoSelector, "issue-repo", "i", "", "Name or URL of the issue's repository")
    73  	fl.BoolVarP(&opts.List, "list", "l", false, "List linked branches for the issue")
    74  	fl.StringVarP(&opts.Name, "name", "n", "", "Name of the branch to create")
    75  	return cmd
    76  }
    77  
    78  func developRunCreate(opts *DevelopOptions) (err error) {
    79  	httpClient, err := opts.HttpClient()
    80  	if err != nil {
    81  		return err
    82  	}
    83  	apiClient := api.NewClientFromHTTP(httpClient)
    84  	baseRepo, err := opts.BaseRepo()
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	opts.IO.StartProgressIndicator()
    90  	err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost())
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	repo, err := api.GitHubRepo(apiClient, baseRepo)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	// The mutation requires the issue id, not just its number
   106  	issue, _, err := shared.IssueFromArgWithFields(httpClient, func() (ghrepo.Interface, error) { return issueRepo, nil }, fmt.Sprint(issueNumber), []string{"id"})
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	// The mutation takes an oid instead of a branch name as it's a more stable reference
   112  	oid, default_branch_oid, err := api.FindBaseOid(apiClient, repo, opts.BaseBranch)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	if oid == "" {
   118  		oid = default_branch_oid
   119  	}
   120  
   121  	// get the oid of the branch from the base repo
   122  	params := map[string]interface{}{
   123  		"issueId":      issue.ID,
   124  		"name":         opts.Name,
   125  		"oid":          oid,
   126  		"repositoryId": repo.ID,
   127  	}
   128  
   129  	ref, err := api.CreateBranchIssueReference(apiClient, repo, params)
   130  	opts.IO.StopProgressIndicator()
   131  	if ref != nil {
   132  		baseRepo.RepoHost()
   133  		fmt.Fprintf(opts.IO.Out, "%s/%s/%s/tree/%s\n", baseRepo.RepoHost(), baseRepo.RepoOwner(), baseRepo.RepoName(), ref.BranchName)
   134  
   135  		if opts.Checkout {
   136  			return checkoutBranch(opts, baseRepo, ref.BranchName)
   137  		}
   138  	}
   139  	if err != nil {
   140  		return err
   141  	}
   142  	return
   143  }
   144  
   145  // 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.
   146  // If the repo from the URL doesn't match the IssueRepoSelector argument, we error.
   147  func issueMetadata(issueSelector string, issueRepoSelector string, baseRepo ghrepo.Interface) (issueNumber int, issueFlagRepo ghrepo.Interface, err error) {
   148  	var targetRepo ghrepo.Interface
   149  	if issueRepoSelector != "" {
   150  		issueFlagRepo, err = ghrepo.FromFullNameWithHost(issueRepoSelector, baseRepo.RepoHost())
   151  		if err != nil {
   152  			return 0, nil, err
   153  		}
   154  	}
   155  
   156  	if issueFlagRepo != nil {
   157  		targetRepo = issueFlagRepo
   158  	}
   159  
   160  	issueNumber, issueArgRepo, err := shared.IssueNumberAndRepoFromArg(issueSelector)
   161  	if err != nil {
   162  		return 0, nil, err
   163  	}
   164  
   165  	if issueArgRepo != nil {
   166  		targetRepo = issueArgRepo
   167  
   168  		if issueFlagRepo != nil {
   169  			differentOwner := (issueFlagRepo.RepoOwner() != issueArgRepo.RepoOwner())
   170  			differentName := (issueFlagRepo.RepoName() != issueArgRepo.RepoName())
   171  			if differentOwner || differentName {
   172  				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())
   173  			}
   174  		}
   175  	}
   176  
   177  	if issueFlagRepo == nil && issueArgRepo == nil {
   178  		targetRepo = baseRepo
   179  	}
   180  
   181  	if targetRepo == nil {
   182  		return 0, nil, fmt.Errorf("could not determine issue repo")
   183  	}
   184  
   185  	return issueNumber, targetRepo, nil
   186  }
   187  
   188  func printLinkedBranches(io *iostreams.IOStreams, branches []api.LinkedBranch) {
   189  
   190  	cs := io.ColorScheme()
   191  	table := tableprinter.New(io)
   192  
   193  	for _, branch := range branches {
   194  		table.AddField(branch.BranchName, tableprinter.WithColor(cs.ColorFromString("cyan")))
   195  		if io.CanPrompt() {
   196  			table.AddField(branch.Url())
   197  		}
   198  		table.EndRow()
   199  	}
   200  
   201  	_ = table.Render()
   202  }
   203  
   204  func developRunList(opts *DevelopOptions) (err error) {
   205  	httpClient, err := opts.HttpClient()
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	apiClient := api.NewClientFromHTTP(httpClient)
   211  	baseRepo, err := opts.BaseRepo()
   212  	if err != nil {
   213  		return err
   214  	}
   215  
   216  	opts.IO.StartProgressIndicator()
   217  
   218  	err = api.CheckLinkedBranchFeature(apiClient, baseRepo.RepoHost())
   219  	if err != nil {
   220  		return err
   221  	}
   222  	issueNumber, issueRepo, err := issueMetadata(opts.IssueSelector, opts.IssueRepoSelector, baseRepo)
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	branches, err := api.ListLinkedBranches(apiClient, issueRepo, issueNumber)
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	opts.IO.StopProgressIndicator()
   233  	if len(branches) == 0 {
   234  		return cmdutil.NewNoResultsError(fmt.Sprintf("no linked branches found for %s/%s#%d", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber))
   235  	}
   236  
   237  	if opts.IO.IsStdoutTTY() {
   238  		fmt.Fprintf(opts.IO.Out, "\nShowing linked branches for %s/%s#%d\n\n", issueRepo.RepoOwner(), issueRepo.RepoName(), issueNumber)
   239  	}
   240  
   241  	printLinkedBranches(opts.IO, branches)
   242  
   243  	return nil
   244  
   245  }
   246  
   247  func checkoutBranch(opts *DevelopOptions, baseRepo ghrepo.Interface, checkoutBranch string) (err error) {
   248  	remotes, err := opts.Remotes()
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	baseRemote, err := remotes.FindByRepo(baseRepo.RepoOwner(), baseRepo.RepoName())
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	if opts.GitClient.HasLocalBranch(ctx.Background(), checkoutBranch) {
   259  		if err := opts.GitClient.CheckoutBranch(ctx.Background(), checkoutBranch); err != nil {
   260  			return err
   261  		}
   262  	} else {
   263  		err := opts.GitClient.Fetch(ctx.Background(), "origin", fmt.Sprintf("+refs/heads/%[1]s:refs/remotes/origin/%[1]s", checkoutBranch))
   264  		if err != nil {
   265  			return err
   266  		}
   267  
   268  		if err := opts.GitClient.CheckoutNewBranch(ctx.Background(), baseRemote.Name, checkoutBranch); err != nil {
   269  			return err
   270  		}
   271  	}
   272  
   273  	if err := opts.GitClient.Pull(ctx.Background(), baseRemote.Name, checkoutBranch); err != nil {
   274  		_, _ = fmt.Fprintf(opts.IO.ErrOut, "%s warning: not possible to fast-forward to: %q\n", opts.IO.ColorScheme().WarningIcon(), checkoutBranch)
   275  	}
   276  
   277  	return nil
   278  }