github.com/zaquestion/lab@v0.25.1/cmd/mr_checkout.go (about)

     1  package cmd
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/MakeNowJust/heredoc/v2"
     7  	"github.com/rsteube/carapace"
     8  	"github.com/spf13/cobra"
     9  	"github.com/zaquestion/lab/internal/action"
    10  	"github.com/zaquestion/lab/internal/git"
    11  	lab "github.com/zaquestion/lab/internal/gitlab"
    12  )
    13  
    14  // mrCheckoutConfig holds configuration values for calls to lab mr checkout
    15  type mrCheckoutConfig struct {
    16  	branch string
    17  	force  bool
    18  	track  bool
    19  }
    20  
    21  var (
    22  	mrCheckoutCfg mrCheckoutConfig
    23  )
    24  
    25  // listCmd represents the list command
    26  var checkoutCmd = &cobra.Command{
    27  	Use:     "checkout [remote] [<MR id or branch>]",
    28  	Aliases: []string{"co"},
    29  	Short:   "Checkout an open merge request",
    30  	Long: heredoc.Doc(`
    31  		Checkout an open merge request using the MR's source branch name as
    32  		local branch name; this behavior can be changed using --branch
    33  		option.`),
    34  	Args: cobra.RangeArgs(1, 2),
    35  	Example: heredoc.Doc(`
    36  		lab mr checkout origin 10
    37  		lab mr checkout upstream -b a_branch_name
    38  		lab mr checkout a_remote -f
    39  		lab mr checkout upstream --https
    40  		lab mr checkout upstream -t`),
    41  	PersistentPreRun: labPersistentPreRun,
    42  	Run: func(cmd *cobra.Command, args []string) {
    43  		rn, mrID, err := parseArgsRemoteAndID(args)
    44  		if err != nil {
    45  			log.Fatal(err)
    46  		}
    47  
    48  		targetRemote := defaultRemote
    49  		if len(args) == 2 {
    50  			// parseArgs above already validated this is a remote
    51  			targetRemote = args[0]
    52  		}
    53  
    54  		mr, err := lab.MRGet(rn, int(mrID))
    55  		if err != nil {
    56  			log.Fatal(err)
    57  		}
    58  
    59  		// If the config does not specify a branch, use the mr source branch name
    60  		if mrCheckoutCfg.branch == "" {
    61  			mrCheckoutCfg.branch = mr.SourceBranch
    62  		}
    63  
    64  		err = git.New("show-ref", "--verify", "--quiet", "refs/heads/"+mrCheckoutCfg.branch).Run()
    65  		if err == nil {
    66  			if mrCheckoutCfg.force {
    67  				err = git.New("branch", "-D", mrCheckoutCfg.branch).Run()
    68  				if err != nil {
    69  					log.Fatal(err)
    70  				}
    71  			} else {
    72  				log.Fatalf("branch %s already exists", mrCheckoutCfg.branch)
    73  			}
    74  		}
    75  
    76  		// By default, fetch to configured branch
    77  		fetchToRef := mrCheckoutCfg.branch
    78  
    79  		// If track, make sure we have a remote for the mr author and then set
    80  		// the fetchToRef to the mr author/sourceBranch
    81  		if mrCheckoutCfg.track {
    82  			// Check if remote already exists
    83  			project, err := lab.GetProject(mr.SourceProjectID)
    84  			if err != nil {
    85  				log.Fatal(err)
    86  			}
    87  
    88  			remotes, err := git.Remotes()
    89  			if err != nil {
    90  				log.Fatal(err)
    91  			}
    92  
    93  			remoteName := ""
    94  			for _, remote := range remotes {
    95  				path, err := git.PathWithNamespace(remote)
    96  				if err != nil {
    97  					continue
    98  				}
    99  				if path == project.PathWithNamespace {
   100  					remoteName = remote
   101  					break
   102  				}
   103  			}
   104  
   105  			if remoteName == "" {
   106  				remoteName = mr.Author.Username
   107  				urlToRepo := labURLToRepo(project)
   108  				err := git.RemoteAdd(remoteName, urlToRepo, ".")
   109  				if err != nil {
   110  					log.Fatal(err)
   111  				}
   112  			}
   113  
   114  			trackRef := fmt.Sprintf("refs/remotes/%s/%s", remoteName, mr.SourceBranch)
   115  			err = git.New("show-ref", "--verify", "--quiet", trackRef).Run()
   116  			if err == nil {
   117  				if mrCheckoutCfg.force {
   118  					err = git.New("update-ref", "-d", trackRef).Run()
   119  					if err != nil {
   120  						log.Fatal(err)
   121  					}
   122  				} else {
   123  					log.Fatalf("remote reference %s already exists", trackRef)
   124  				}
   125  			}
   126  
   127  			fetchToRef = trackRef
   128  		}
   129  
   130  		// https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#checkout-merge-requests-locally-through-the-head-ref
   131  		mrRef := fmt.Sprintf("refs/merge-requests/%d/head", mrID)
   132  		fetchRefSpec := fmt.Sprintf("%s:%s", mrRef, fetchToRef)
   133  		err = git.New("fetch", targetRemote, fetchRefSpec).Run()
   134  		if err != nil {
   135  			log.Fatal(err)
   136  		}
   137  
   138  		if mrCheckoutCfg.track {
   139  			// Create configured branch with tracking from fetchToRef
   140  			// git branch --flags <branchname> [<start-point>]
   141  			err = git.New("branch", "--track", mrCheckoutCfg.branch, fetchToRef).Run()
   142  			if err != nil {
   143  				log.Fatal(err)
   144  			}
   145  		}
   146  
   147  		err = git.New("checkout", mrCheckoutCfg.branch).Run()
   148  		if err != nil {
   149  			log.Fatal(err)
   150  		}
   151  	},
   152  }
   153  
   154  func init() {
   155  	checkoutCmd.Flags().StringVarP(&mrCheckoutCfg.branch, "branch", "b", "", "checkout merge request with <branch> name")
   156  	checkoutCmd.Flags().BoolVarP(&mrCheckoutCfg.track, "track", "t", false, "set branch to track remote branch, adds remote if needed")
   157  	// useHTTP is defined in "project_create.go"
   158  	checkoutCmd.Flags().BoolVar(&useHTTP, "http", false, "checkout using HTTP protocol instead of SSH")
   159  	checkoutCmd.Flags().BoolVarP(&mrCheckoutCfg.force, "force", "f", false, "force branch and remote reference override")
   160  	mrCmd.AddCommand(checkoutCmd)
   161  	carapace.Gen(checkoutCmd).PositionalCompletion(
   162  		carapace.ActionCallback(func(c carapace.Context) carapace.Action {
   163  			c.Args = []string{"origin"}
   164  			return action.MergeRequests(mrList).Invoke(c).ToA()
   165  		}),
   166  	)
   167  }