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 }