mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-27 23:08:02 +02:00
- **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.
This commit is contained in:
commit
f884cc2af9
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
@ -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,
|
||||
|
@ -527,6 +527,7 @@ type TranslationSet struct {
|
||||
FetchingRemoteStatus string
|
||||
CheckoutCommit string
|
||||
CheckoutCommitTooltip string
|
||||
NoBranchesFoundAtCommitTooltip string
|
||||
SureCheckoutThisCommit string
|
||||
GitFlowOptions string
|
||||
NotAGitFlowBranch string
|
||||
@ -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
|
||||
@ -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?",
|
||||
@ -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",
|
||||
|
69
pkg/integration/tests/commit/checkout.go
Normal file
69
pkg/integration/tests/commit/checkout.go
Normal file
@ -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"),
|
||||
)
|
||||
},
|
||||
})
|
@ -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(
|
||||
|
@ -84,6 +84,7 @@ var tests = []*components.IntegrationTest{
|
||||
commit.AddCoAuthorWhileCommitting,
|
||||
commit.Amend,
|
||||
commit.AutoWrapMessage,
|
||||
commit.Checkout,
|
||||
commit.Commit,
|
||||
commit.CommitMultiline,
|
||||
commit.CommitSwitchToEditor,
|
||||
|
Loading…
x
Reference in New Issue
Block a user