mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-29 23:17:32 +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 {
|
func (self *BasicCommitsController) checkout(commit *models.Commit) error {
|
||||||
self.c.Confirm(types.ConfirmOpts{
|
return self.c.Helpers().Refs.CreateCheckoutMenu(commit)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BasicCommitsController) copyRange(*models.Commit) error {
|
func (self *BasicCommitsController) copyRange(*models.Commit) error {
|
||||||
|
@ -18,6 +18,7 @@ type IRefsHelper interface {
|
|||||||
CheckoutRef(ref string, options types.CheckoutRefOptions) error
|
CheckoutRef(ref string, options types.CheckoutRefOptions) error
|
||||||
GetCheckedOutRef() *models.Branch
|
GetCheckedOutRef() *models.Branch
|
||||||
CreateGitResetMenu(ref string) error
|
CreateGitResetMenu(ref string) error
|
||||||
|
CreateCheckoutMenu(commit *models.Commit) error
|
||||||
ResetToRef(ref string, strength string, envVars []string) error
|
ResetToRef(ref string, strength string, envVars []string) error
|
||||||
NewBranch(from string, fromDescription string, suggestedBranchname 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 {
|
func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggestedBranchName string) error {
|
||||||
message := utils.ResolvePlaceholderString(
|
message := utils.ResolvePlaceholderString(
|
||||||
self.c.Tr.NewBranchNameBranchOff,
|
self.c.Tr.NewBranchNameBranchOff,
|
||||||
|
@ -527,6 +527,7 @@ type TranslationSet struct {
|
|||||||
FetchingRemoteStatus string
|
FetchingRemoteStatus string
|
||||||
CheckoutCommit string
|
CheckoutCommit string
|
||||||
CheckoutCommitTooltip string
|
CheckoutCommitTooltip string
|
||||||
|
NoBranchesFoundAtCommitTooltip string
|
||||||
SureCheckoutThisCommit string
|
SureCheckoutThisCommit string
|
||||||
GitFlowOptions string
|
GitFlowOptions string
|
||||||
NotAGitFlowBranch string
|
NotAGitFlowBranch string
|
||||||
@ -860,8 +861,11 @@ type Log struct {
|
|||||||
|
|
||||||
type Actions struct {
|
type Actions struct {
|
||||||
CheckoutCommit string
|
CheckoutCommit string
|
||||||
|
CheckoutBranchAtCommit string
|
||||||
|
CheckoutCommitAsDetachedHead string
|
||||||
CheckoutTag string
|
CheckoutTag string
|
||||||
CheckoutBranch string
|
CheckoutBranch string
|
||||||
|
CheckoutBranchOrCommit string
|
||||||
ForceCheckoutBranch string
|
ForceCheckoutBranch string
|
||||||
DeleteLocalBranch string
|
DeleteLocalBranch string
|
||||||
Merge string
|
Merge string
|
||||||
@ -1522,21 +1526,22 @@ func EnglishTranslationSet() *TranslationSet {
|
|||||||
DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?",
|
DeleteRemoteTagPrompt: "Are you sure you want to delete the remote tag '{{.tagName}}' from '{{.upstream}}'?",
|
||||||
PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
|
PushTagTitle: "Remote to push tag '{{.tagName}}' to:",
|
||||||
// Using 'push tag' rather than just 'push' to disambiguate from a global push
|
// Using 'push tag' rather than just 'push' to disambiguate from a global push
|
||||||
PushTag: "Push tag",
|
PushTag: "Push tag",
|
||||||
PushTagTooltip: "Push the selected tag to a remote. You'll be prompted to select a remote.",
|
PushTagTooltip: "Push the selected tag to a remote. You'll be prompted to select a remote.",
|
||||||
NewTag: "New tag",
|
NewTag: "New tag",
|
||||||
NewTagTooltip: "Create new tag from current commit. You'll be prompted to enter a tag name and optional description.",
|
NewTagTooltip: "Create new tag from current commit. You'll be prompted to enter a tag name and optional description.",
|
||||||
CreatingTag: "Creating tag",
|
CreatingTag: "Creating tag",
|
||||||
ForceTag: "Force Tag",
|
ForceTag: "Force Tag",
|
||||||
ForceTagPrompt: "The tag '{{.tagName}}' exists already. Press {{.cancelKey}} to cancel, or {{.confirmKey}} to overwrite.",
|
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.",
|
FetchRemoteTooltip: "Fetch updates from the remote repository. This retrieves new commits and branches without merging them into your local branches.",
|
||||||
FetchingRemoteStatus: "Fetching remote",
|
FetchingRemoteStatus: "Fetching remote",
|
||||||
CheckoutCommit: "Checkout commit",
|
CheckoutCommit: "Checkout commit",
|
||||||
CheckoutCommitTooltip: "Checkout the selected commit as a detached HEAD.",
|
CheckoutCommitTooltip: "Checkout the selected commit as a detached HEAD.",
|
||||||
SureCheckoutThisCommit: "Are you sure you want to checkout this commit?",
|
NoBranchesFoundAtCommitTooltip: "No branches found at selected commit.",
|
||||||
GitFlowOptions: "Show git-flow options",
|
SureCheckoutThisCommit: "Are you sure you want to checkout this commit?",
|
||||||
NotAGitFlowBranch: "This does not seem to be a git flow branch",
|
GitFlowOptions: "Show git-flow options",
|
||||||
NewGitFlowBranchPrompt: "New {{.branchType}} name:",
|
NotAGitFlowBranch: "This does not seem to be a git flow branch",
|
||||||
|
NewGitFlowBranchPrompt: "New {{.branchType}} name:",
|
||||||
|
|
||||||
IgnoreTracked: "Ignore tracked file",
|
IgnoreTracked: "Ignore tracked file",
|
||||||
IgnoreTrackedPrompt: "Are you sure you want to ignore a tracked file?",
|
IgnoreTrackedPrompt: "Are you sure you want to ignore a tracked file?",
|
||||||
@ -1822,9 +1827,12 @@ func EnglishTranslationSet() *TranslationSet {
|
|||||||
Actions: Actions{
|
Actions: Actions{
|
||||||
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
|
||||||
CheckoutCommit: "Checkout commit",
|
CheckoutCommit: "Checkout commit",
|
||||||
|
CheckoutBranchAtCommit: "Checkout branch '%s'",
|
||||||
|
CheckoutCommitAsDetachedHead: "Checkout commit %s as detached head",
|
||||||
CheckoutTag: "Checkout tag",
|
CheckoutTag: "Checkout tag",
|
||||||
CheckoutBranch: "Checkout branch",
|
CheckoutBranch: "Checkout branch",
|
||||||
ForceCheckoutBranch: "Force checkout branch",
|
ForceCheckoutBranch: "Force checkout branch",
|
||||||
|
CheckoutBranchOrCommit: "Checkout branch or commit",
|
||||||
DeleteLocalBranch: "Delete local branch",
|
DeleteLocalBranch: "Delete local branch",
|
||||||
Merge: "Merge",
|
Merge: "Merge",
|
||||||
SquashMerge: "Squash 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().
|
SelectNextItem().
|
||||||
PressPrimaryAction().
|
PressPrimaryAction().
|
||||||
Tap(func() {
|
Tap(func() {
|
||||||
t.ExpectPopup().Confirmation().
|
t.ExpectPopup().Menu().
|
||||||
Title(Contains("Checkout commit")).
|
Title(Contains("Checkout branch or commit")).
|
||||||
Content(Contains("Are you sure you want to checkout this commit?")).
|
Select(MatchesRegexp("Checkout commit [a-f0-9]+ as detached head")).
|
||||||
Confirm()
|
Confirm()
|
||||||
}).
|
}).
|
||||||
TopLines(
|
TopLines(
|
||||||
|
@ -84,6 +84,7 @@ var tests = []*components.IntegrationTest{
|
|||||||
commit.AddCoAuthorWhileCommitting,
|
commit.AddCoAuthorWhileCommitting,
|
||||||
commit.Amend,
|
commit.Amend,
|
||||||
commit.AutoWrapMessage,
|
commit.AutoWrapMessage,
|
||||||
|
commit.Checkout,
|
||||||
commit.Commit,
|
commit.Commit,
|
||||||
commit.CommitMultiline,
|
commit.CommitMultiline,
|
||||||
commit.CommitSwitchToEditor,
|
commit.CommitSwitchToEditor,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user