diff --git a/pkg/gui/controllers/working_tree_diff_actions.go b/pkg/gui/controllers/working_tree_diff_actions.go index 6feae8380..d0130ae49 100644 --- a/pkg/gui/controllers/working_tree_diff_actions.go +++ b/pkg/gui/controllers/working_tree_diff_actions.go @@ -171,6 +171,19 @@ func (self *WorkingTreeDiffActions) applyDiffLines( } } + // Acting on every change of a file is acting on the file itself, and saying so is + // not the same as applying its diff. The diff of a deleted file is its content + // going away, and putting that into the index line by line leaves an empty file + // there rather than the deletion; the diff of an added one is its whole content, + // and taking that back out leaves an empty file in the index rather than an + // untracked one. + if opts.Cached && len(patchLineIndices) == changeLineCount(parsedPatch) { + if opts.Reverse { + return self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked) + } + return self.c.Git().WorkingTree.StageFile(file.GetPath()) + } + patchToApply := parsedPatch. Transform(patch.TransformOpts{ Reverse: opts.Reverse, @@ -184,3 +197,11 @@ func (self *WorkingTreeDiffActions) applyDiffLines( return self.c.Git().Patch.ApplyPatch(patchToApply, opts) } + +// changeLineCount returns how many of a patch's lines are changes rather than context +// or header. A selection of the whole diff covers exactly that many. +func changeLineCount(p *patch.Patch) int { + return lo.CountBy(p.Lines(), func(line *patch.PatchLine) bool { + return line.IsAddition() || line.IsDeletion() + }) +} diff --git a/pkg/integration/tests/main_view/stage_deleted_file.go b/pkg/integration/tests/main_view/stage_deleted_file.go new file mode 100644 index 000000000..9b863ae72 --- /dev/null +++ b/pkg/integration/tests/main_view/stage_deleted_file.go @@ -0,0 +1,80 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageDeletedFile = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Acting on the whole of a file's block in a directory's diff acts on the file: staging a deletion, and unstaging an addition", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("fileA", "a\n") + shell.CreateFileAndAdd("fileB", "b1\nb2\n") + shell.Commit("one") + + shell.UpdateFile("fileA", "a\nfromA\n") + shell.DeleteFile("fileB") + shell.CreateFileAndAdd("fileC", "c1\nc2\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("▼ /").IsSelected(), + Contains(" M fileA"), + Contains(" D fileB"), + Contains("A fileC"), + ). + Press(keys.Universal.FocusMainView) + + // Select everything the deleted file contributes to the diff. + t.Views().Main(). + IsFocused(). + NavigateToLine(Contains("-b1")). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("-b2")). + SelectedLines( + Contains("-b1"), + Contains("-b2"), + ). + PressPrimaryAction() + + // The file is staged as deleted. Applying its lines as a patch would have left + // an empty file in the index instead, which is not what deleting a file means. + t.Views().Files().Lines( + Contains("▼ /"), + Contains(" M fileA"), + Contains("D fileB"), + Contains("A fileC"), + ) + + // The same the other way round: taking the whole of an added file back out of + // the index leaves it untracked, rather than tracked and empty. + t.Views().Main(). + IsFocused(). + Press(keys.Universal.TogglePanel) + + t.Views().Secondary(). + IsFocused(). + NavigateToLine(Contains("+c1")). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("+c2")). + SelectedLines( + Contains("+c1"), + Contains("+c2"), + ). + PressPrimaryAction() + + t.Views().Files().Lines( + Contains("▼ /"), + Contains(" M fileA"), + Contains("D fileB"), + Contains("?? fileC"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 9a449c0e5..44350dcb8 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -408,6 +408,7 @@ var tests = []*components.IntegrationTest{ main_view.SelectVisibleHunkOnFocusingMainView, main_view.SelectionCommandsOnlyWhereTheyApply, main_view.SelectionOverTheCustomPatch, + main_view.StageDeletedFile, main_view.StageDiffLines, main_view.StageDiffLinesOfAPathWithASpace, main_view.StageRangeSpanningFiles,