1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-06 22:33:07 +02:00

Auto-stash modified files when cherry-picking or reverting commits (#4683)

- **PR Description**

For cherry-picking, this used to work in earlier versions, but it broke
in #4443. For reverting, it was never supported.

Also, we add some minor improvements while we're at it, such as slightly
better names for the auto-stashes that are created for the various
operations (so that, if an auto-stash pop fails and the stash is kept
around, you can tell more easily what it was for). Also, we now adjust
the selection of the commits list after cherry-picking, so that the same
commit stays selected.
This commit is contained in:
Stefan Haller
2025-07-04 10:13:52 +02:00
committed by GitHub
14 changed files with 100 additions and 46 deletions

View File

@ -99,9 +99,7 @@ linters:
- legacy - legacy
- std-error-handling - std-error-handling
paths: paths:
- third_party$ - vendor/
- builtin$
- examples$
formatters: formatters:
enable: enable:
- gofumpt - gofumpt
@ -109,6 +107,4 @@ formatters:
exclusions: exclusions:
generated: lax generated: lax
paths: paths:
- third_party$ - vendor/
- builtin$
- examples$

View File

@ -1,6 +1,7 @@
package git_commands package git_commands
import ( import (
"fmt"
"path/filepath" "path/filepath"
"time" "time"
@ -218,7 +219,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error { func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitIdx int, stash bool) error {
if stash { if stash {
if err := self.stash.Push(self.Tr.StashPrefix + commits[commitIdx].Hash()); err != nil { if err := self.stash.Push(fmt.Sprintf(self.Tr.AutoStashForMovingPatchToIndex, commits[commitIdx].ShortHash())); err != nil {
return err return err
} }
} }

View File

@ -3,7 +3,6 @@ package helpers
import ( import (
"strconv" "strconv"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
@ -77,14 +76,34 @@ func (self *CherryPickHelper) Paste() error {
"numCommits": strconv.Itoa(len(self.getData().CherryPickedCommits)), "numCommits": strconv.Itoa(len(self.getData().CherryPickedCommits)),
}), }),
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func(gocui.Task) error { return self.c.WithWaitingStatusSync(self.c.Tr.CherryPickingStatus, func() error {
mustStash := IsWorkingTreeDirty(self.c.Model().Files)
self.c.LogAction(self.c.Tr.Actions.CherryPick) self.c.LogAction(self.c.Tr.Actions.CherryPick)
result := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits)
err := self.rebaseHelper.CheckMergeOrRebase(result) if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.AutoStashForCherryPicking); err != nil {
return err
}
}
cherryPickedCommits := self.getData().CherryPickedCommits
result := self.c.Git().Rebase.CherryPickCommits(cherryPickedCommits)
err := self.rebaseHelper.CheckMergeOrRebaseWithRefreshOptions(result, types.RefreshOptions{Mode: types.SYNC})
if err != nil { if err != nil {
return result return result
} }
// Move the selection down by the number of commits we just
// cherry-picked, to keep the same commit selected as before.
// Don't do this if a rebase todo is selected, because in this
// case we are in a rebase and the cherry-picked commits end up
// below the selection.
if commit := self.c.Contexts().LocalCommits.GetSelected(); commit != nil && !commit.IsTODO() {
self.c.Contexts().LocalCommits.MoveSelection(len(cherryPickedCommits))
self.c.Contexts().LocalCommits.FocusLine()
}
// If we're in the cherry-picking state at this point, it must // If we're in the cherry-picking state at this point, it must
// be because there were conflicts. Don't clear the copied // be because there were conflicts. Don't clear the copied
// commits in this case, since we might want to abort and try // commits in this case, since we might want to abort and try
@ -96,7 +115,17 @@ func (self *CherryPickHelper) Paste() error {
if !isInCherryPick { if !isInCherryPick {
self.getData().DidPaste = true self.getData().DidPaste = true
self.rerender() self.rerender()
if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.STASH, types.FILES},
})
}
} }
return nil return nil
}) })
}, },

View File

@ -79,7 +79,7 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
Prompt: self.c.Tr.AutoStashPrompt, Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
return withCheckoutStatus(func(gocui.Task) error { return withCheckoutStatus(func(gocui.Task) error {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + ref); err != nil { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForCheckout, ref)); err != nil {
return err return err
} }
if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil { if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil {
@ -355,7 +355,7 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest
Title: self.c.Tr.AutoStashTitle, Title: self.c.Tr.AutoStashTitle,
Prompt: self.c.Tr.AutoStashPrompt, Prompt: self.c.Tr.AutoStashPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + newBranchName); err != nil { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil {
return err return err
} }
if err := newBranchFunc(newBranchName, from); err != nil { if err := newBranchFunc(newBranchName, from); err != nil {
@ -389,7 +389,7 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
return err return err
} }
withNewBranchNamePrompt := func(baseBranchName string, f func(string, string) error) error { withNewBranchNamePrompt := func(baseBranchName string, f func(string) error) error {
prompt := utils.ResolvePlaceholderString( prompt := utils.ResolvePlaceholderString(
self.c.Tr.NewBranchNameBranchOff, self.c.Tr.NewBranchNameBranchOff,
map[string]string{ map[string]string{
@ -408,7 +408,7 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch) self.c.LogAction(self.c.Tr.MoveCommitsToNewBranch)
newBranchName := SanitizedBranchName(response) newBranchName := SanitizedBranchName(response)
return self.c.WithWaitingStatus(self.c.Tr.MovingCommitsToNewBranchStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.MovingCommitsToNewBranchStatus, func(gocui.Task) error {
return f(currentBranch.Name, newBranchName) return f(newBranchName)
}) })
}, },
}) })
@ -447,8 +447,8 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
{ {
Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName), Label: fmt.Sprintf(self.c.Tr.MoveCommitsToNewBranchFromBaseItem, shortBaseBranchName),
OnPress: func() error { OnPress: func() error {
return withNewBranchNamePrompt(shortBaseBranchName, func(currentBranch string, newBranchName string) error { return withNewBranchNamePrompt(shortBaseBranchName, func(newBranchName string) error {
return self.moveCommitsToNewBranchOffOfMainBranch(currentBranch, newBranchName, baseBranchRef) return self.moveCommitsToNewBranchOffOfMainBranch(newBranchName, baseBranchRef)
}) })
}, },
}, },
@ -462,14 +462,14 @@ func (self *RefsHelper) MoveCommitsToNewBranch() error {
}) })
} }
func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(currentBranch string, newBranchName string) error { func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(newBranchName string) error {
if err := self.c.Git().Branch.NewWithoutCheckout(newBranchName, "HEAD"); err != nil { if err := self.c.Git().Branch.NewWithoutCheckout(newBranchName, "HEAD"); err != nil {
return err return err
} }
mustStash := IsWorkingTreeDirty(self.c.Model().Files) mustStash := IsWorkingTreeDirty(self.c.Model().Files)
if mustStash { if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil {
return err return err
} }
} }
@ -495,14 +495,14 @@ func (self *RefsHelper) moveCommitsToNewBranchStackedOnCurrentBranch(currentBran
return nil return nil
} }
func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(currentBranch string, newBranchName string, baseBranchRef string) error { func (self *RefsHelper) moveCommitsToNewBranchOffOfMainBranch(newBranchName string, baseBranchRef string) error {
commitsToCherryPick := lo.Filter(self.c.Model().Commits, func(commit *models.Commit, _ int) bool { commitsToCherryPick := lo.Filter(self.c.Model().Commits, func(commit *models.Commit, _ int) bool {
return commit.Status == models.StatusUnpushed return commit.Status == models.StatusUnpushed
}) })
mustStash := IsWorkingTreeDirty(self.c.Model().Files) mustStash := IsWorkingTreeDirty(self.c.Model().Files)
if mustStash { if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + currentBranch); err != nil { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForNewBranch, newBranchName)); err != nil {
return err return err
} }
} }

View File

@ -873,14 +873,30 @@ func (self *LocalCommitsController) revert(commits []*models.Commit, start, end
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.RevertCommit) self.c.LogAction(self.c.Tr.Actions.RevertCommit)
return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error { return self.c.WithWaitingStatusSync(self.c.Tr.RevertingStatus, func() error {
mustStash := helpers.IsWorkingTreeDirty(self.c.Model().Files)
if mustStash {
if err := self.c.Git().Stash.Push(self.c.Tr.AutoStashForReverting); err != nil {
return err
}
}
result := self.c.Git().Commit.Revert(hashes, isMerge) result := self.c.Git().Commit.Revert(hashes, isMerge)
if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(result); err != nil { if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(result, types.RefreshOptions{Mode: types.SYNC}); err != nil {
return err return err
} }
self.context().MoveSelection(len(commits)) self.context().MoveSelection(len(commits))
self.c.Refresh(types.RefreshOptions{ self.context().FocusLine()
Mode: types.SYNC, Scope: []types.RefreshableView{types.COMMITS, types.BRANCHES},
}) if mustStash {
if err := self.c.Git().Stash.Pop(0); err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.STASH, types.FILES},
})
}
return nil return nil
}) })
}, },

View File

@ -90,7 +90,7 @@ func (self *UndoController) reflogUndo() error {
case COMMIT: case COMMIT:
self.c.Confirm(types.ConfirmOpts{ self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Actions.Undo, Title: self.c.Tr.Actions.Undo,
Prompt: fmt.Sprintf(self.c.Tr.SoftResetPrompt, action.from), Prompt: fmt.Sprintf(self.c.Tr.SoftResetPrompt, utils.ShortHash(action.from)),
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.Undo) self.c.LogAction(self.c.Tr.Actions.Undo)
return self.c.WithWaitingStatus(undoingStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(undoingStatus, func(gocui.Task) error {
@ -103,7 +103,7 @@ func (self *UndoController) reflogUndo() error {
case REBASE: case REBASE:
self.c.Confirm(types.ConfirmOpts{ self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Actions.Undo, Title: self.c.Tr.Actions.Undo,
Prompt: fmt.Sprintf(self.c.Tr.HardResetAutostashPrompt, action.from), Prompt: fmt.Sprintf(self.c.Tr.HardResetAutostashPrompt, utils.ShortHash(action.from)),
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.Undo) self.c.LogAction(self.c.Tr.Actions.Undo)
return self.hardResetWithAutoStash(action.from, hardResetOptions{ return self.hardResetWithAutoStash(action.from, hardResetOptions{
@ -157,7 +157,7 @@ func (self *UndoController) reflogRedo() error {
case COMMIT, REBASE: case COMMIT, REBASE:
self.c.Confirm(types.ConfirmOpts{ self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.Actions.Redo, Title: self.c.Tr.Actions.Redo,
Prompt: fmt.Sprintf(self.c.Tr.HardResetAutostashPrompt, action.to), Prompt: fmt.Sprintf(self.c.Tr.HardResetAutostashPrompt, utils.ShortHash(action.to)),
HandleConfirm: func() error { HandleConfirm: func() error {
self.c.LogAction(self.c.Tr.Actions.Redo) self.c.LogAction(self.c.Tr.Actions.Redo)
return self.hardResetWithAutoStash(action.to, hardResetOptions{ return self.hardResetWithAutoStash(action.to, hardResetOptions{
@ -260,7 +260,7 @@ func (self *UndoController) hardResetWithAutoStash(commitHash string, options ha
dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirty() dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirty()
if dirtyWorkingTree { if dirtyWorkingTree {
return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(options.WaitingStatus, func(gocui.Task) error {
if err := self.c.Git().Stash.Push(self.c.Tr.StashPrefix + commitHash); err != nil { if err := self.c.Git().Stash.Push(fmt.Sprintf(self.c.Tr.AutoStashForUndo, utils.ShortHash(commitHash))); err != nil {
return err return err
} }
if err := reset(); err != nil { if err := reset(); err != nil {

View File

@ -447,7 +447,12 @@ type TranslationSet struct {
IncorrectNotARepository string IncorrectNotARepository string
AutoStashTitle string AutoStashTitle string
AutoStashPrompt string AutoStashPrompt string
StashPrefix string AutoStashForUndo string
AutoStashForCheckout string
AutoStashForNewBranch string
AutoStashForMovingPatchToIndex string
AutoStashForCherryPicking string
AutoStashForReverting string
Discard string Discard string
DiscardChangesTitle string DiscardChangesTitle string
DiscardFileChangesTooltip string DiscardFileChangesTooltip string
@ -1540,7 +1545,12 @@ func EnglishTranslationSet() *TranslationSet {
IncorrectNotARepository: "The value of 'notARepository' is incorrect. It should be one of 'prompt', 'create', 'skip', or 'quit'.", IncorrectNotARepository: "The value of 'notARepository' is incorrect. It should be one of 'prompt', 'create', 'skip', or 'quit'.",
AutoStashTitle: "Autostash?", AutoStashTitle: "Autostash?",
AutoStashPrompt: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)", AutoStashPrompt: "You must stash and pop your changes to bring them across. Do this automatically? (enter/esc)",
StashPrefix: "Auto-stashing changes for ", AutoStashForUndo: "Auto-stashing changes for undoing to %s",
AutoStashForCheckout: "Auto-stashing changes for checking out %s",
AutoStashForNewBranch: "Auto-stashing changes for creating new branch %s",
AutoStashForMovingPatchToIndex: "Auto-stashing changes for moving custom patch to index from %s",
AutoStashForCherryPicking: "Auto-stashing changes for cherry-picking commits",
AutoStashForReverting: "Auto-stashing changes for reverting commits",
Discard: "Discard", Discard: "Discard",
DiscardFileChangesTooltip: "View options for discarding changes to the selected file.", DiscardFileChangesTooltip: "View options for discarding changes to the selected file.",
DiscardChangesTitle: "Discard changes", DiscardChangesTitle: "Discard changes",

View File

@ -75,7 +75,7 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
Lines( Lines(
Contains("four"), Contains("four"),
Contains("three"), Contains("three"),
Contains("two"), Contains("two").IsSelected(),
Contains("one"), Contains("one"),
Contains("base"), Contains("base"),
) )
@ -104,7 +104,7 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
Lines( Lines(
Contains("four"), Contains("four"),
Contains("three"), Contains("three"),
Contains("base"), Contains("base").IsSelected(),
) )
}, },
}) })

View File

@ -43,7 +43,7 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Focus(). Focus().
TopLines( TopLines(
Contains("first change"), Contains("first change").IsSelected(),
). ).
Press(keys.Commits.PasteCommits) Press(keys.Commits.PasteCommits)
@ -76,7 +76,7 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits(). t.Views().Commits().
Focus(). Focus().
TopLines( TopLines(
Contains("second-change-branch unrelated change"), Contains("second-change-branch unrelated change").IsSelected(),
Contains("second change"), Contains("second change"),
Contains("first change"), Contains("first change"),
). ).

View File

@ -80,7 +80,7 @@ var CherryPickDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("pick CI two"), Contains("pick CI two"),
Contains("--- Commits ---"), Contains("--- Commits ---"),
Contains(" CI three"), Contains(" CI three"),
Contains(" CI one"), Contains(" CI one").IsSelected(),
Contains(" CI base"), Contains(" CI base"),
). ).
Tap(func() { Tap(func() {
@ -89,7 +89,7 @@ var CherryPickDuringRebase = NewIntegrationTest(NewIntegrationTestArgs{
Lines( Lines(
Contains("CI two"), Contains("CI two"),
Contains("CI three"), Contains("CI three"),
Contains("CI one"), Contains("CI one").IsSelected(),
Contains("CI base"), Contains("CI base"),
) )
}, },

View File

@ -63,9 +63,10 @@ var CherryPickMerge = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(DoesNotContain("commit copied")) t.Views().Information().Content(DoesNotContain("commit copied"))
}). }).
Lines( Lines(
Contains("Merge branch 'second-branch'").IsSelected(), Contains("Merge branch 'second-branch'"),
Contains("base"), Contains("base").IsSelected(),
) ).
SelectPreviousItem()
t.Views().Main().ContainsLines( t.Views().Main().ContainsLines(
Contains("Merge branch 'second-branch'"), Contains("Merge branch 'second-branch'"),

View File

@ -72,7 +72,7 @@ var CherryPickRange = NewIntegrationTest(NewIntegrationTestArgs{
Lines( Lines(
Contains("four"), Contains("four"),
Contains("three"), Contains("three"),
Contains("two"), Contains("two").IsSelected(),
Contains("one"), Contains("one"),
Contains("base"), Contains("base"),
) )

View File

@ -29,9 +29,10 @@ var Revert = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
Contains("Revert \"first commit\"").IsSelected(), Contains("Revert \"first commit\""),
Contains("first commit"), Contains("first commit").IsSelected(),
) ).
SelectPreviousItem()
t.Views().Main().Content(Contains("-myfile content")) t.Views().Main().Content(Contains("-myfile content"))
t.FileSystem().PathNotPresent("myfile") t.FileSystem().PathNotPresent("myfile")

View File

@ -43,8 +43,8 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
Confirm() Confirm()
}). }).
Lines( Lines(
Contains("three").IsSelected(), Contains("three"),
Contains("one"), Contains("one").IsSelected(),
) )
}, },
}) })