Skip to content

Commit 810f6b2

Browse files
authored
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.
2 parents d34266d + 3ccd33b commit 810f6b2

File tree

5 files changed

+68
-0
lines changed

5 files changed

+68
-0
lines changed

pkg/gui/controllers/commits_files_controller.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/jesseduffield/lazygit/pkg/commands/patch"
1313
"github.com/jesseduffield/lazygit/pkg/constants"
1414
"github.com/jesseduffield/lazygit/pkg/gui/context"
15+
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
1516
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
1617
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
1718
"github.com/jesseduffield/lazygit/pkg/gui/types"
@@ -283,6 +284,12 @@ func (self *CommitFilesController) openCopyMenu() error {
283284
}
284285

285286
func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error {
287+
hasModifiedFiles := helpers.AnyTrackedFilesInPathExceptSubmodules(node.GetPath(),
288+
self.c.Model().Files, self.c.Model().Submodules)
289+
if hasModifiedFiles {
290+
return errors.New(self.c.Tr.CannotCheckoutWithModifiedFilesErr)
291+
}
292+
286293
self.c.LogAction(self.c.Tr.Actions.CheckoutFile)
287294
_, to := self.context().GetFromAndToForDiff()
288295
if err := self.c.Git().WorkingTree.CheckoutFile(to, node.GetPath()); err != nil {

pkg/gui/controllers/helpers/working_tree_helper.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"regexp"
8+
"strings"
89

910
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
1011
"github.com/jesseduffield/lazygit/pkg/commands/models"
@@ -71,6 +72,23 @@ func AnyTrackedFilesExceptSubmodules(files []*models.File, submoduleConfigs []*m
7172
return lo.SomeBy(files, func(f *models.File) bool { return f.Tracked && !f.IsSubmodule(submoduleConfigs) })
7273
}
7374

75+
func isContainedInPath(candidate string, path string) bool {
76+
return (
77+
// If the path is the repo root (appears as "/" in the UI), then all candidates are contained in it
78+
path == "." ||
79+
// Exact match; will only be true for files
80+
candidate == path ||
81+
// Match for files within a directory. We need to match the trailing slash to avoid
82+
// matching files with longer names.
83+
strings.HasPrefix(candidate, path+"/"))
84+
}
85+
86+
func AnyTrackedFilesInPathExceptSubmodules(path string, files []*models.File, submoduleConfigs []*models.SubmoduleConfig) bool {
87+
return lo.SomeBy(files, func(f *models.File) bool {
88+
return f.Tracked && isContainedInPath(f.GetPath(), path) && !f.IsSubmodule(submoduleConfigs)
89+
})
90+
}
91+
7492
func (self *WorkingTreeHelper) IsWorkingTreeDirtyExceptSubmodules() bool {
7593
return IsWorkingTreeDirtyExceptSubmodules(self.c.Model().Files, self.c.Model().Submodules)
7694
}

pkg/i18n/english.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ type TranslationSet struct {
425425
ViewItemFiles string
426426
CommitFilesTitle string
427427
CheckoutCommitFileTooltip string
428+
CannotCheckoutWithModifiedFilesErr string
428429
CanOnlyDiscardFromLocalCommits string
429430
Remove string
430431
DiscardOldFileChangeTooltip string
@@ -1525,6 +1526,7 @@ func EnglishTranslationSet() *TranslationSet {
15251526
ViewItemFiles: "View files",
15261527
CommitFilesTitle: "Commit files",
15271528
CheckoutCommitFileTooltip: "Checkout file. This replaces the file in your working tree with the version from the selected commit.",
1529+
CannotCheckoutWithModifiedFilesErr: "You have local modifications for the file(s) you are trying to check out. You need to stash or discard these first.",
15281530
CanOnlyDiscardFromLocalCommits: "Changes can only be discarded from local commits",
15291531
Remove: "Remove",
15301532
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.",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package commit
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var CheckoutFileWithLocalModifications = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Checkout a file from a commit that has local modifications",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.CreateFileAndAdd("dir/file1.txt", "file1\n")
15+
shell.CreateFileAndAdd("dir/file2.txt", "file2\n")
16+
shell.Commit("one")
17+
shell.UpdateFile("dir/file1.txt", "file1\nfile1 change\n")
18+
},
19+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
20+
t.Views().Commits().
21+
Focus().
22+
Lines(
23+
Contains("one").IsSelected(),
24+
).
25+
PressEnter()
26+
27+
t.Views().CommitFiles().
28+
IsFocused().
29+
Lines(
30+
Equals("▼ dir").IsSelected(),
31+
Equals(" A file1.txt"),
32+
Equals(" A file2.txt"),
33+
).
34+
Press(keys.CommitFiles.CheckoutCommitFile)
35+
36+
t.ExpectPopup().Alert().Title(Equals("Error")).
37+
Content(Contains("local modifications")).
38+
Confirm()
39+
},
40+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ var tests = []*components.IntegrationTest{
107107
commit.Checkout,
108108
commit.CheckoutFileFromCommit,
109109
commit.CheckoutFileFromRangeSelectionOfCommits,
110+
commit.CheckoutFileWithLocalModifications,
110111
commit.Commit,
111112
commit.CommitMultiline,
112113
commit.CommitSkipHooks,

0 commit comments

Comments
 (0)