Skip to content

Commit

Permalink
Allow to switch branches in Commit View (#4115) (#4117)
Browse files Browse the repository at this point in the history
- **PR Description**

When the user checks out a commit which has a local branch ref attached
to it, they can select between checking out the branch or checking out
the commit as detached head. If no local branch is attached to the
commit, the behavior is like before: They are asked to confirm, if they
want to checkout the commit as detached head.

Requested in #4115.

Note: I tried also to consider remote branches, but because I wasn't
able to correlate remote branches to their commits, I deferred it and
leave it open for later improvement.
  • Loading branch information
stefanhaller authored Jan 1, 2025
2 parents 03d7bc8 + f4c8287 commit f884cc2
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 27 deletions.
10 changes: 1 addition & 9 deletions pkg/gui/controllers/basic_commits_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,7 @@ func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error
}

func (self *BasicCommitsController) checkout(commit *models.Commit) error {
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.CheckoutCommit,
Prompt: self.c.Tr.SureCheckoutThisCommit,
HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutCommit)
return self.c.Helpers().Refs.CheckoutRef(commit.Hash, types.CheckoutRefOptions{})
},
})
return nil
return self.c.Helpers().Refs.CreateCheckoutMenu(commit)
}

func (self *BasicCommitsController) copyRange(*models.Commit) error {
Expand Down
48 changes: 48 additions & 0 deletions pkg/gui/controllers/helpers/refs_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type IRefsHelper interface {
CheckoutRef(ref string, options types.CheckoutRefOptions) error
GetCheckedOutRef() *models.Branch
CreateGitResetMenu(ref string) error
CreateCheckoutMenu(commit *models.Commit) error
ResetToRef(ref string, strength string, envVars []string) error
NewBranch(from string, fromDescription string, suggestedBranchname string) error
}
Expand Down Expand Up @@ -271,6 +272,53 @@ func (self *RefsHelper) CreateGitResetMenu(ref string) error {
})
}

func (self *RefsHelper) CreateCheckoutMenu(commit *models.Commit) error {
branches := lo.Filter(self.c.Model().Branches, func(branch *models.Branch, _ int) bool {
return commit.Hash == branch.CommitHash && branch.Name != self.c.Model().CheckedOutBranch
})

hash := commit.Hash
var menuItems []*types.MenuItem

if len(branches) > 0 {
menuItems = append(menuItems, lo.Map(branches, func(branch *models.Branch, index int) *types.MenuItem {
var key types.Key
if index < 9 {
key = rune(index + 1 + '0') // Convert 1-based index to key
}
return &types.MenuItem{
LabelColumns: []string{fmt.Sprintf(self.c.Tr.Actions.CheckoutBranchAtCommit, branch.Name)},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutBranch)
return self.CheckoutRef(branch.RefName(), types.CheckoutRefOptions{})
},
Key: key,
}
})...)
} else {
menuItems = append(menuItems, &types.MenuItem{
LabelColumns: []string{self.c.Tr.Actions.CheckoutBranch},
OnPress: func() error { return nil },
DisabledReason: &types.DisabledReason{Text: self.c.Tr.NoBranchesFoundAtCommitTooltip},
Key: '1',
})
}

menuItems = append(menuItems, &types.MenuItem{
LabelColumns: []string{fmt.Sprintf(self.c.Tr.Actions.CheckoutCommitAsDetachedHead, utils.ShortHash(hash))},
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.CheckoutCommit)
return self.CheckoutRef(hash, types.CheckoutRefOptions{})
},
Key: 'd',
})

return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.Actions.CheckoutBranchOrCommit,
Items: menuItems,
})
}

func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggestedBranchName string) error {
message := utils.ResolvePlaceholderString(
self.c.Tr.NewBranchNameBranchOff,
Expand Down
38 changes: 23 additions & 15 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ type TranslationSet struct {
FetchingRemoteStatus string
CheckoutCommit string
CheckoutCommitTooltip string
NoBranchesFoundAtCommitTooltip string
SureCheckoutThisCommit string
GitFlowOptions string
NotAGitFlowBranch string
Expand Down Expand Up @@ -860,8 +861,11 @@ type Log struct {

type Actions struct {
CheckoutCommit string
CheckoutBranchAtCommit string
CheckoutCommitAsDetachedHead string
CheckoutTag string
CheckoutBranch string
CheckoutBranchOrCommit string
ForceCheckoutBranch string
DeleteLocalBranch string
Merge string
Expand Down Expand Up @@ -1522,21 +1526,22 @@ func EnglishTranslationSet() *TranslationSet {
DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?",
PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
// Using 'push tag' rather than just 'push' to disambiguate from a global push
PushTag: "Push tag",
PushTagTooltip: "Push the selected tag to a remote. You'll be prompted to select a remote.",
NewTag: "New tag",
NewTagTooltip: "Create new tag from current commit. You'll be prompted to enter a tag name and optional description.",
CreatingTag: "Creating tag",
ForceTag: "Force Tag",
ForceTagPrompt: "The tag '{{.tagName}}' exists already. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to overwrite.",
FetchRemoteTooltip: "Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches.",
FetchingRemoteStatus: "Fetching remote",
CheckoutCommit: "Checkout commit",
CheckoutCommitTooltip: "Checkout the selected commit as a detached HEAD.",
SureCheckoutThisCommit: "Are you sure you want to checkout this commit?",
GitFlowOptions: "Show git-flow options",
NotAGitFlowBranch: "This does not seem to be a git flow branch",
NewGitFlowBranchPrompt: "New {{.branchType}} name:",
PushTag: "Push tag",
PushTagTooltip: "Push the selected tag to a remote. You'll be prompted to select a remote.",
NewTag: "New tag",
NewTagTooltip: "Create new tag from current commit. You'll be prompted to enter a tag name and optional description.",
CreatingTag: "Creating tag",
ForceTag: "Force Tag",
ForceTagPrompt: "The tag '{{.tagName}}' exists already. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to overwrite.",
FetchRemoteTooltip: "Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches.",
FetchingRemoteStatus: "Fetching remote",
CheckoutCommit: "Checkout commit",
CheckoutCommitTooltip: "Checkout the selected commit as a detached HEAD.",
NoBranchesFoundAtCommitTooltip: "No branches found at selected commit.",
SureCheckoutThisCommit: "Are you sure you want to checkout this commit?",
GitFlowOptions: "Show git-flow options",
NotAGitFlowBranch: "This does not seem to be a git flow branch",
NewGitFlowBranchPrompt: "New {{.branchType}} name:",

IgnoreTracked: "Ignore tracked file",
IgnoreTrackedPrompt: "Are you sure you want to ignore a tracked file?",
Expand Down Expand Up @@ -1822,9 +1827,12 @@ func EnglishTranslationSet() *TranslationSet {
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",
CheckoutBranchAtCommit: "Checkout branch '%s'",
CheckoutCommitAsDetachedHead: "Checkout commit %s as detached head",
CheckoutTag: "Checkout tag",
CheckoutBranch: "Checkout branch",
ForceCheckoutBranch: "Force checkout branch",
CheckoutBranchOrCommit: "Checkout branch or commit",
DeleteLocalBranch: "Delete local branch",
Merge: "Merge",
SquashMerge: "Squash merge",
Expand Down
69 changes: 69 additions & 0 deletions pkg/integration/tests/commit/checkout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package commit

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var Checkout = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Checkout a commit as a detached head, or checkout an existing branch at a commit",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.NewBranch("branch1")
shell.NewBranch("branch2")
shell.EmptyCommit("three")
shell.EmptyCommit("four")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("four").IsSelected(),
Contains("three"),
Contains("two"),
Contains("one"),
).
PressPrimaryAction()

t.ExpectPopup().Menu().
Title(Contains("Checkout branch or commit")).
Lines(
Contains("Checkout branch").IsSelected(),
MatchesRegexp("Checkout commit [a-f0-9]+ as detached head"),
Contains("Cancel"),
).
Tooltip(Contains("Disabled: No branches found at selected commit.")).
Select(MatchesRegexp("Checkout commit [a-f0-9]+ as detached head")).
Confirm()
t.Views().Branches().Lines(
Contains("* (HEAD detached at"),
Contains("branch2"),
Contains("branch1"),
Contains("master"),
)

t.Views().Commits().
NavigateToLine(Contains("two")).
PressPrimaryAction()

t.ExpectPopup().Menu().
Title(Contains("Checkout branch or commit")).
Lines(
Contains("Checkout branch 'branch1'").IsSelected(),
Contains("Checkout branch 'master'"),
MatchesRegexp("Checkout commit [a-f0-9]+ as detached head"),
Contains("Cancel"),
).
Select(Contains("Checkout branch 'master'")).
Confirm()
t.Views().Branches().Lines(
Contains("master"),
Contains("branch2"),
Contains("branch1"),
)
},
})
6 changes: 3 additions & 3 deletions pkg/integration/tests/reflog/checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ var Checkout = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
PressPrimaryAction().
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Contains("Checkout commit")).
Content(Contains("Are you sure you want to checkout this commit?")).
t.ExpectPopup().Menu().
Title(Contains("Checkout branch or commit")).
Select(MatchesRegexp("Checkout commit [a-f0-9]+ as detached head")).
Confirm()
}).
TopLines(
Expand Down
1 change: 1 addition & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ var tests = []*components.IntegrationTest{
commit.AddCoAuthorWhileCommitting,
commit.Amend,
commit.AutoWrapMessage,
commit.Checkout,
commit.Commit,
commit.CommitMultiline,
commit.CommitSwitchToEditor,
Expand Down

0 comments on commit f884cc2

Please sign in to comment.