From e42cbf95ae73d60142ee02e9ef057dc7cce7d05a Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 15 Mar 2024 20:56:44 +0100 Subject: [PATCH] When checking out a remote branch by name, ask the user how The choices are to create a new local branch that tracks the remote, or a detached head. --- pkg/commands/git_commands/branch.go | 11 ++++ pkg/gui/controllers/branches_controller.go | 4 ++ pkg/gui/controllers/helpers/refs_helper.go | 69 ++++++++++++++++++++++ pkg/i18n/english.go | 10 ++++ 4 files changed, 94 insertions(+) diff --git a/pkg/commands/git_commands/branch.go b/pkg/commands/git_commands/branch.go index d05738ef3..6a347b8ac 100644 --- a/pkg/commands/git_commands/branch.go +++ b/pkg/commands/git_commands/branch.go @@ -28,6 +28,17 @@ func (self *BranchCommands) New(name string, base string) error { return self.cmd.New(cmdArgs).Run() } +// CreateWithUpstream creates a new branch with a given upstream, but without +// checking it out +func (self *BranchCommands) CreateWithUpstream(name string, upstream string) error { + cmdArgs := NewGitCmd("branch"). + Arg("--track"). + Arg(name, upstream). + ToArgv() + + return self.cmd.New(cmdArgs).Run() +} + // CurrentBranchInfo get the current branch information. func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) { branchName, err := self.cmd.New( diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 068238ec7..ff9473ab6 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -436,6 +436,10 @@ func (self *BranchesController) checkoutByName() error { FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(), HandleConfirm: func(response string) error { self.c.LogAction("Checkout branch") + _, branchName, found := self.c.Helpers().Refs.ParseRemoteBranchName(response) + if found { + return self.c.Helpers().Refs.CheckoutRemoteBranch(response, branchName) + } return self.c.Helpers().Refs.CheckoutRef(response, types.CheckoutRefOptions{ OnRefNotFound: func(ref string) error { return self.c.Confirm(types.ConfirmOpts{ diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index 8b066f447..362746288 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -96,6 +96,57 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions }) } +// Shows a prompt to choose between creating a new branch or checking out a detached head +func (self *RefsHelper) CheckoutRemoteBranch(fullBranchName string, localBranchName string) error { + checkout := func(branchName string) error { + if err := self.CheckoutRef(branchName, types.CheckoutRefOptions{}); err != nil { + return err + } + if self.c.CurrentContext() != self.c.Contexts().Branches { + return self.c.PushContext(self.c.Contexts().Branches) + } + return nil + } + + // If a branch with this name already exists locally, just check it out. We + // don't bother checking whether it actually tracks this remote branch, since + // it's very unlikely that it doesn't. + if lo.ContainsBy(self.c.Model().Branches, func(branch *models.Branch) bool { + return branch.Name == localBranchName + }) { + return checkout(localBranchName) + } + + return self.c.Menu(types.CreateMenuOptions{ + Title: utils.ResolvePlaceholderString(self.c.Tr.RemoteBranchCheckoutTitle, map[string]string{ + "branchName": fullBranchName, + }), + Items: []*types.MenuItem{ + { + Label: self.c.Tr.CheckoutTypeNewBranch, + Tooltip: self.c.Tr.CheckoutTypeNewBranchTooltip, + OnPress: func() error { + // First create the local branch with the upstream set, and + // then check it out. We could do that in one step using + // "git checkout -b", but we want to benefit from all the + // nice features of the CheckoutRef function. + if err := self.c.Git().Branch.CreateWithUpstream(localBranchName, fullBranchName); err != nil { + return self.c.Error(err) + } + return checkout(localBranchName) + }, + }, + { + Label: self.c.Tr.CheckoutTypeDetachedHead, + Tooltip: self.c.Tr.CheckoutTypeDetachedHeadTooltip, + OnPress: func() error { + return checkout(fullBranchName) + }, + }, + }, + }) +} + func (self *RefsHelper) GetCheckedOutRef() *models.Branch { if len(self.c.Model().Branches) == 0 { return nil @@ -232,3 +283,21 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest func SanitizedBranchName(input string) string { return strings.Replace(input, " ", "-", -1) } + +// Checks if the given branch name is a remote branch, and returns the name of +// the remote and the bare branch name if it is. +func (self *RefsHelper) ParseRemoteBranchName(fullBranchName string) (string, string, bool) { + remoteName, branchName, found := strings.Cut(fullBranchName, "/") + if !found { + return "", "", false + } + + // See if the part before the first slash is actually one of our remotes + if !lo.ContainsBy(self.c.Model().Remotes, func(remote *models.Remote) bool { + return remote.Name == remoteName + }) { + return "", "", false + } + + return remoteName, branchName, true +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 2f3f35ab5..5d3d37d74 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -113,6 +113,11 @@ type TranslationSet struct { ForceCheckoutTooltip string CheckoutByName string CheckoutByNameTooltip string + RemoteBranchCheckoutTitle string + CheckoutTypeNewBranch string + CheckoutTypeNewBranchTooltip string + CheckoutTypeDetachedHead string + CheckoutTypeDetachedHeadTooltip string NewBranch string NewBranchFromStashTooltip string NoBranchesThisRepo string @@ -1065,6 +1070,11 @@ func EnglishTranslationSet() TranslationSet { ForceCheckoutTooltip: "Force checkout selected branch. This will discard all local changes in your working directory before checking out the selected branch.", CheckoutByName: "Checkout by name", CheckoutByNameTooltip: "Checkout by name. In the input box you can enter '-' to switch to the last branch.", + RemoteBranchCheckoutTitle: "Checkout {{.branchName}}", + CheckoutTypeNewBranch: "New local branch", + CheckoutTypeNewBranchTooltip: "Checkout the remote branch as a local branch, tracking the remote branch.", + CheckoutTypeDetachedHead: "Detached head", + CheckoutTypeDetachedHeadTooltip: "Checkout the remote branch as a detached head, which can be useful if you just want to test the branch but not work on it yourself. You can still create a local branch from it later.", NewBranch: "New branch", NewBranchFromStashTooltip: "Create a new branch from the selected stash entry. This works by git checking out the commit that the stash entry was created from, creating a new branch from that commit, then applying the stash entry to the new branch as an additional commit.", NoBranchesThisRepo: "No branches for this repo",