1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2026-01-26 01:41:35 +03:00

Show an error when checking out a file would overwrite local modifications (#5154)

Checking out a file discards all local modifications to that file;
checking out a directory discards modifications to all files in that
directory. This can result in lost work if the user isn't aware of this.

Instead of showing a confirmation (to which the user might press enter
too hastily, still losing their work), or showing a menu with
auto-stash/discard options, we simply show an error. The assumption is
that this is an unusual situation that users won't run into often, and
if they do, it's easy enough to manually address it by stashing or
discarding the changes as appropriate.

Fixes #5142.
This commit is contained in:
Stefan Haller
2026-01-03 20:11:51 +01:00
committed by GitHub
5 changed files with 68 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -283,6 +284,12 @@ func (self *CommitFilesController) openCopyMenu() error {
}
func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error {
hasModifiedFiles := helpers.AnyTrackedFilesInPathExceptSubmodules(node.GetPath(),
self.c.Model().Files, self.c.Model().Submodules)
if hasModifiedFiles {
return errors.New(self.c.Tr.CannotCheckoutWithModifiedFilesErr)
}
self.c.LogAction(self.c.Tr.Actions.CheckoutFile)
_, to := self.context().GetFromAndToForDiff()
if err := self.c.Git().WorkingTree.CheckoutFile(to, node.GetPath()); err != nil {

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -71,6 +72,23 @@ func AnyTrackedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*m
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked && !f.IsSubmodule(submoduleConfigs) })
}
func isContainedInPath(candidate string, path string) bool {
return (
// If the path is the repo root (appears as "/" in the UI), then all candidates are contained in it
path == "." ||
// Exact match; will only be true for files
candidate == path ||
// Match for files within a directory. We need to match the trailing slash to avoid
// matching files with longer names.
strings.HasPrefix(candidate, path+"/"))
}
func AnyTrackedFilesInPathExceptSubmodules(path string, files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool {
return lo.SomeBy(files, func(f *models.File) bool {
return f.Tracked && isContainedInPath(f.GetPath(), path) && !f.IsSubmodule(submoduleConfigs)
})
}
func (self *WorkingTreeHelper) IsWorkingTreeDirtyExceptSubmodules() bool {
return IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
}

View File

@@ -425,6 +425,7 @@ type TranslationSet struct {
ViewItemFiles string
CommitFilesTitle string
CheckoutCommitFileTooltip string
CannotCheckoutWithModifiedFilesErr string
CanOnlyDiscardFromLocalCommits string
Remove string
DiscardOldFileChangeTooltip string
@@ -1525,6 +1526,7 @@ func EnglishTranslationSet() *TranslationSet {
ViewItemFiles: "View files",
CommitFilesTitle: "Commit files",
CheckoutCommitFileTooltip: "Checkout file. This replaces the file in your working tree with the version from the selected commit.",
CannotCheckoutWithModifiedFilesErr: "You have local modifications for the file(s) you are trying to check out. You need to stash or discard these first.",
CanOnlyDiscardFromLocalCommits: "Changes can only be discarded from local commits",
Remove: "Remove",
DiscardOldFileChangeTooltip: "Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file.",

View File

@@ -0,0 +1,40 @@
package commit
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CheckoutFileWithLocalModifications = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Checkout a file from a commit that has local modifications",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("dir/file1.txt", "file1\n")
shell.CreateFileAndAdd("dir/file2.txt", "file2\n")
shell.Commit("one")
shell.UpdateFile("dir/file1.txt", "file1\nfile1 change\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("one").IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Equals("▼ dir").IsSelected(),
Equals(" A file1.txt"),
Equals(" A file2.txt"),
).
Press(keys.CommitFiles.CheckoutCommitFile)
t.ExpectPopup().Alert().Title(Equals("Error")).
Content(Contains("local modifications")).
Confirm()
},
})

View File

@@ -107,6 +107,7 @@ var tests = []*components.IntegrationTest{
commit.Checkout,
commit.CheckoutFileFromCommit,
commit.CheckoutFileFromRangeSelectionOfCommits,
commit.CheckoutFileWithLocalModifications,
commit.Commit,
commit.CommitMultiline,
commit.CommitSkipHooks,