summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2024-03-09 13:36:44 +1100
committerJesse Duffield <jessedduffield@gmail.com>2024-03-09 13:53:14 +1100
commit4a07e037016d90a62b3a99765a1f687909f0c748 (patch)
tree3e1ab3e14bcdf5e03e6d579dc98c60ea9c92e437
parent44f553b6093c69d09718f617e0a7659c64f51015 (diff)
Local-ise remote branches when checking out by namecheckout-by-name
The idea here is that in the git CLI you will often want to checkout a remote branch like origin/blah by doing `git checkout blah`. That will automatically create a local branch named blah that tracks origin/blah. Previously in the suggestions view when checking out a branch by name, we would only show local branches and remote branches like origin/blah but wouldn't show blah as an option (assuming no local branch existed with that name). This meant users would checkout origin/blah and git would check out that branch as a detached head which is rarely what you actually want. Now we give them both options. The alternative approach we could have taken is to still show the branch as origin/blah but then ask if the user wants to check out the branch as detached or as a local branch tracking the remote branch. That approach is certainly more versatile, but it's also something you can do already by going to the remote branch directly via the remotes view. Admittedly, my approach involves less work.
-rw-r--r--pkg/gui/controllers/branches_controller.go2
-rw-r--r--pkg/gui/controllers/helpers/suggestions_helper.go27
-rw-r--r--pkg/integration/components/shell.go4
-rw-r--r--pkg/integration/tests/branch/checkout_by_name_remote.go66
-rw-r--r--pkg/integration/tests/test_list.go1
5 files changed, 97 insertions, 3 deletions
diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go
index 068238ec7..91918923d 100644
--- a/pkg/gui/controllers/branches_controller.go
+++ b/pkg/gui/controllers/branches_controller.go
@@ -433,7 +433,7 @@ func (self *BranchesController) forceCheckout() error {
func (self *BranchesController) checkoutByName() error {
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.BranchName + ":",
- FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(),
+ FindSuggestionsFunc: self.c.Helpers().Suggestions.GetCheckoutBranchesSuggestionsFunc(),
HandleConfirm: func(response string) error {
self.c.LogAction("Checkout branch")
return self.c.Helpers().Refs.CheckoutRef(response, types.CheckoutRefOptions{
diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go
index 2ae9d2158..5c10dcd5c 100644
--- a/pkg/gui/controllers/helpers/suggestions_helper.go
+++ b/pkg/gui/controllers/helpers/suggestions_helper.go
@@ -3,6 +3,7 @@ package helpers
import (
"fmt"
"os"
+ "strings"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -25,6 +26,8 @@ import (
// finding suggestions in this file, so that it's easy to see if a function already
// exists for fetching a particular model.
+var specialRefNames = []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
+
type ISuggestionsHelper interface {
GetRemoteSuggestionsFunc() func(string) []*types.Suggestion
GetBranchNameSuggestionsFunc() func(string) []*types.Suggestion
@@ -168,9 +171,29 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su
remoteBranchNames := self.getRemoteBranchNames("/")
localBranchNames := self.getBranchNames()
tagNames := self.getTagNames()
- additionalRefNames := []string{"HEAD", "FETCH_HEAD", "MERGE_HEAD", "ORIG_HEAD"}
- refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), additionalRefNames...)
+ refNames := append(append(append(remoteBranchNames, localBranchNames...), tagNames...), specialRefNames...)
+
+ return FuzzySearchFunc(refNames)
+}
+
+func (self *SuggestionsHelper) GetCheckoutBranchesSuggestionsFunc() func(string) []*types.Suggestion {
+ remoteBranchNames := self.getRemoteBranchNames("/")
+ // We include remote branches with the remote stripped off in the list of suggestions
+ // so that you can check out the branch as a local branch tracking the remote branch, just
+ // like you can do in the git CLI. I.e. if you checkout 'origin/blah' it will be
+ // checked out as as a detached head, but if you checkout 'blah' it will be checked out
+ // as a local branch tracking 'origin/blah'.
+ localisedRemoteBranchNames := lo.Map(remoteBranchNames, func(branchName string, _ int) string {
+ // strip the remote name from the branch name
+ return branchName[strings.Index(branchName, "/")+1:]
+ })
+ localBranchNames := self.getBranchNames()
+ tagNames := self.getTagNames()
+
+ refNames := append(append(append(append(remoteBranchNames, localBranchNames...), tagNames...), specialRefNames...), localisedRemoteBranchNames...)
+
+ refNames = lo.Uniq(refNames)
return FuzzySearchFunc(refNames)
}
diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go
index 60c627918..60652b2ad 100644
--- a/pkg/integration/components/shell.go
+++ b/pkg/integration/components/shell.go
@@ -148,6 +148,10 @@ func (self *Shell) Checkout(name string) *Shell {
return self.RunCommand([]string{"git", "checkout", name})
}
+func (self *Shell) DeleteBranch(name string) *Shell {
+ return self.RunCommand([]string{"git", "branch", "-D", name})
+}
+
func (self *Shell) Merge(name string) *Shell {
return self.RunCommand([]string{"git", "merge", "--commit", "--no-ff", name})
}
diff --git a/pkg/integration/tests/branch/checkout_by_name_remote.go b/pkg/integration/tests/branch/checkout_by_name_remote.go
new file mode 100644
index 000000000..cd9d01db6
--- /dev/null
+++ b/pkg/integration/tests/branch/checkout_by_name_remote.go
@@ -0,0 +1,66 @@
+package branch
+
+import (
+ "github.com/jesseduffield/lazygit/pkg/config"
+ . "github.com/jesseduffield/lazygit/pkg/integration/components"
+)
+
+var CheckoutByNameRemote = NewIntegrationTest(NewIntegrationTestArgs{
+ Description: "Checkout a remote branch by name, both using the full name and the local name.",
+ ExtraCmdArgs: []string{},
+ Skip: false,
+ SetupConfig: func(config *config.AppConfig) {},
+ SetupRepo: func(shell *Shell) {
+ shell.EmptyCommit("initial commit")
+ // create an origin/foo remote branch
+ shell.CloneIntoRemote("origin")
+ shell.NewBranch("foo")
+ shell.PushBranch("origin", "foo")
+ // delete the local version of the branch because we need to test checking it out from scratch
+ shell.Checkout("master")
+ shell.DeleteBranch("foo")
+ },
+ Run: func(t *TestDriver, keys config.KeybindingConfig) {
+ t.Views().Branches().
+ Focus().
+ Lines(
+ Contains("master").IsSelected(),
+ ).
+ // maximising window so that we can see the tracked branch
+ Press(keys.Universal.NextScreenMode).
+ Press(keys.Branches.CheckoutBranchByName).
+ Tap(func() {
+ t.ExpectPopup().Prompt().
+ Title(Equals("Branch name:")).
+ Type("foo").
+ SuggestionLines(
+ Contains("foo"),
+ Contains("origin/foo"),
+ ).
+ ConfirmFirstSuggestion()
+ }).
+ Lines(
+ Contains("foo").
+ // we have not checked out origin/foo...
+ DoesNotContain("origin/foo").
+ // ... but we are tracking it (formatted as '<remote> <branch>')
+ Contains("origin foo"),
+ Contains("master"),
+ ).
+ Press(keys.Branches.CheckoutBranchByName).
+ Tap(func() {
+ t.ExpectPopup().Prompt().
+ Title(Equals("Branch name:")).
+ Type("origin/foo").
+ SuggestionLines(
+ Contains("origin/foo"),
+ ).
+ ConfirmFirstSuggestion()
+ }).
+ Lines(
+ Contains("HEAD detached at origin/foo"),
+ Contains("foo"),
+ Contains("master"),
+ )
+ },
+})
diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go
index 531fce5d9..4fc6fe710 100644
--- a/pkg/integration/tests/test_list.go
+++ b/pkg/integration/tests/test_list.go
@@ -37,6 +37,7 @@ var tests = []*components.IntegrationTest{
bisect.FromOtherBranch,
bisect.Skip,
branch.CheckoutByName,
+ branch.CheckoutByNameRemote,
branch.CreateTag,
branch.Delete,
branch.DeleteRemoteBranchWithCredentialPrompt,