diff --git a/pkg/gui/controllers/commit_diff_actions.go b/pkg/gui/controllers/commit_diff_actions.go index 7063deb9b..b5adae259 100644 --- a/pkg/gui/controllers/commit_diff_actions.go +++ b/pkg/gui/controllers/commit_diff_actions.go @@ -1,6 +1,7 @@ package controllers import ( + "errors" "fmt" "github.com/jesseduffield/generics/set" @@ -9,6 +10,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -104,6 +106,10 @@ func (self *CommitDiffActions) PrimaryAction(pane types.DiffPaneContext, firstBu if len(lines) == 0 { return nil } + selection, err := self.selectionByFile(lines) + if err != nil { + return err + } patchBuilder := self.c.Git().Patch.PatchBuilder from, reverse := self.patchEndpoints(target) @@ -121,14 +127,16 @@ func (self *CommitDiffActions) PrimaryAction(pane types.DiffPaneContext, firstBu patchBuilder.Start(from, target.to, reverse, target.canRebase) } - if err := self.togglePatchLines(lines); err != nil { - return err - } + err := self.togglePatchLines(selection) // Taking the last line back out ends the patch rather than leaving an empty // one, so that the pane previewing it and the marks over the diff go with it. + // The same goes for a patch just started that no line could be put into. if patchBuilder.IsEmpty() { patchBuilder.Reset() } + if err != nil { + return err + } // The diff on screen is the one the marks belong to, so they can be brought up // to date at once rather than waiting for the render below. @@ -167,11 +175,15 @@ func (self *CommitDiffActions) removePatchLines( } patchBuilder := self.c.Git().Patch.PatchBuilder - files := self.filesInDiff() + files, err := self.filesInDiff() + if err != nil { + return err + } + indicesByFilename := map[string][]int{} for path, ordinals := range self.c.Helpers().DiffLine.ChangeLineOrdinals(self.customPatchDiff(), lines) { filename := self.patchBuilderPath(path) if filename == "" { - continue + return diffLinesFileNotFoundError(self.c, path) } included := patchBuilder.IncludedChangeLineIndices(filename) indices := []int{} @@ -181,8 +193,11 @@ func (self *CommitDiffActions) removePatchLines( } } if len(indices) == 0 { - continue + return diffLinesNotFoundError(self.c, filename) } + indicesByFilename[filename] = indices + } + for filename, indices := range indicesByFilename { if err := patchBuilder.RemoveFileLineRange(filename, files.previousPath(filename), indices); err != nil { return err } @@ -223,7 +238,12 @@ func (self *CommitDiffActions) DiscardSelection(pane types.DiffPaneContext, firs } commitIndex := self.indexOfTargetCommit(target) if commitIndex == -1 { - return nil + return errors.New(utils.ResolvePlaceholderString(self.c.Tr.CommitNotInCommitsPanel, + map[string]string{"hash": utils.ShortHash(target.to)})) + } + selection, err := self.selectionByFile(lines) + if err != nil { + return err } patchBuilder := self.c.Git().Patch.PatchBuilder @@ -238,7 +258,10 @@ func (self *CommitDiffActions) DiscardSelection(pane types.DiffPaneContext, firs from, reverse := self.patchEndpoints(target) patchBuilder.Reset() patchBuilder.Start(from, target.to, reverse, target.canRebase) - if err := self.togglePatchLines(lines); err != nil { + if err := self.togglePatchLines(selection); err != nil { + // This patch exists only to be removed from the commit, so don't leave it + // behind. + patchBuilder.Reset() return err } if patchBuilder.IsEmpty() { @@ -318,41 +341,61 @@ func (self *CommitDiffActions) PatchInclusion() func(types.DiffLineInfo) bool { } } -// togglePatchLines takes the given lines of the commit's diff into the custom patch, or -// out of it. The first line of the selection decides which of the two happens, once for -// the whole selection: pointing at a line that is already in the patch takes the whole -// selection out of it, as toggling a selection of files in the commit files panel does. -func (self *CommitDiffActions) togglePatchLines(lines []types.DiffLineInfo) error { - patchBuilder := self.c.Git().Patch.PatchBuilder +// commitDiffSelection is the selected lines of a commit's diff, by the file they belong +// to: a patch is built a file at a time, while a selection can span several of them. +type commitDiffSelection struct { + // What the diff says about each of the files it covers. + files commitDiffFiles - // The files the selection covers, in the order the diff shows them, and per file the - // lines of it that are selected: a patch is built a file at a time, while a selection - // can span several of them. - paths := []string{} - linesByPath := map[string][]patch.LineIdentity{} + // The files the selection covers, in the order the diff shows them. + paths []string + + // Per file, the lines of it that are selected. + linesByPath map[string][]patch.LineIdentity +} + +// selectionByFile groups the given lines of the commit's diff by the file they belong +// to. If a line belongs to no file of the diff, the diff on screen and the one the +// patch is built from disagree, so this fails and we build nothing. +func (self *CommitDiffActions) selectionByFile(lines []types.DiffLineInfo) (commitDiffSelection, error) { + files, err := self.filesInDiff() + if err != nil { + return commitDiffSelection{}, err + } + selection := commitDiffSelection{files: files, linesByPath: map[string][]patch.LineIdentity{}} for _, line := range lines { path := self.patchBuilderPath(line.Path) - if path == "" { - continue + if _, ok := files[path]; !ok { + return commitDiffSelection{}, diffLinesFileNotFoundError(self.c, line.Path) } - if _, seen := linesByPath[path]; !seen { - paths = append(paths, path) + if _, seen := selection.linesByPath[path]; !seen { + selection.paths = append(selection.paths, path) } - linesByPath[path] = append(linesByPath[path], line.PatchLineIdentity()) - } - if len(paths) == 0 { - return nil + selection.linesByPath[path] = append(selection.linesByPath[path], line.PatchLineIdentity()) } + return selection, nil +} + +// togglePatchLines takes the selected lines of the commit's diff into the custom patch, +// or out of it. The first line of the selection decides which of the two happens, once +// for the whole selection: pointing at a line that is already in the patch takes the +// whole selection out of it, as toggling a selection of files in the commit files panel +// does. +func (self *CommitDiffActions) togglePatchLines(selection commitDiffSelection) error { + patchBuilder := self.c.Git().Patch.PatchBuilder + files, paths := selection.files, selection.paths - files := self.filesInDiff() indicesByPath := map[string][]int{} wholeFileByPath := map[string]bool{} for _, path := range paths { indices, everyChange, err := patchBuilder.PatchLineIndicesForLines( - path, files.previousPath(path), linesByPath[path]) + path, files.previousPath(path), selection.linesByPath[path]) if err != nil { return err } + if len(indices) == 0 { + return diffLinesNotFoundError(self.c, path) + } indicesByPath[path] = indices // Selecting every change of a file the commit adds or deletes is selecting the @@ -365,12 +408,9 @@ func (self *CommitDiffActions) togglePatchLines(lines []types.DiffLineInfo) erro if err != nil { return err } - removing := len(indicesByPath[paths[0]]) > 0 && lo.Contains(included, indicesByPath[paths[0]][0]) + removing := lo.Contains(included, indicesByPath[paths[0]][0]) for _, path := range paths { - if len(indicesByPath[path]) == 0 { - continue - } previousPath := files.previousPath(path) var err error switch { @@ -415,20 +455,20 @@ func (self commitDiffFiles) isWholeFileOperation(path string) bool { // filesInDiff asks git which files the diff a patch is being built from covers, and // what it does to each. -func (self *CommitDiffActions) filesInDiff() commitDiffFiles { +func (self *CommitDiffActions) filesInDiff() (commitDiffFiles, error) { target := self.target() if target == nil { - return nil + return nil, nil } from, reverse := self.patchEndpoints(target) files, err := self.c.Git().Loaders.CommitFileLoader.GetFilesInDiff(from, target.to, reverse) if err != nil { - return nil + return nil, err } return lo.SliceToMap(files, func(file *models.CommitFile) (string, *models.CommitFile) { return file.Path, file - }) + }), nil } // patchEndpoints gives the two ends of the diff a patch is built from. They are the ends diff --git a/pkg/gui/controllers/diff_paths.go b/pkg/gui/controllers/diff_paths.go index 91215ba2c..3f18c5628 100644 --- a/pkg/gui/controllers/diff_paths.go +++ b/pkg/gui/controllers/diff_paths.go @@ -1,11 +1,13 @@ package controllers import ( + "errors" "path" "path/filepath" "strings" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -132,6 +134,25 @@ func isInDir(path string, dir string) bool { return dir == "." || strings.HasPrefix(path, dir+"/") } +// diffLinesFileNotFoundError reports that the file selected diff lines belong to can't +// be found. It names the file by its path in the repo, or by the path the diff line +// carries for one outside of it. +func diffLinesFileNotFoundError(c *ControllerCommon, absolutePath string) error { + path := repoRelativePath(c.Git().RepoPaths.WorktreePath(), absolutePath) + if path == "" { + path = absolutePath + } + return errors.New(utils.ResolvePlaceholderString(c.Tr.DiffLinesFileNotFound, + map[string]string{"path": path})) +} + +// diffLinesNotFoundError reports that selected diff lines can't be found in the diff of +// the file at the given path in the repo. +func diffLinesNotFoundError(c *ControllerCommon, path string) error { + return errors.New(utils.ResolvePlaceholderString(c.Tr.DiffLinesNotFound, + map[string]string{"path": path})) +} + // repoRelativePath turns the absolute path a diff line carries into the one git speaks // of the file: relative to the worktree, with forward slashes. It is "" for a path that // is no file of this repo, which a diff renderer's own naming of a line can produce. diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 71aee5888..7129e1c43 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -1105,7 +1105,7 @@ func (self *MainViewController) openPullRequestAtSelectedLine() error { } relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), info.Path) if relativePath == "" { - return nil + return diffLinesFileNotFoundError(self.c, info.Path) } self.c.LogAction(self.c.Tr.Actions.OpenPullRequest) diff --git a/pkg/gui/controllers/working_tree_diff_actions.go b/pkg/gui/controllers/working_tree_diff_actions.go index b9ff5124a..6739d80a2 100644 --- a/pkg/gui/controllers/working_tree_diff_actions.go +++ b/pkg/gui/controllers/working_tree_diff_actions.go @@ -59,14 +59,17 @@ func (self *WorkingTreeDiffActions) PrimaryAction(pane types.DiffPaneContext, fi self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) } - infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) - if !ok { + linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if err != nil { + return err + } + if len(linesByFile) == 0 { return nil } // Either way the patch goes to the index: forwards from the unstaged side to stage // it, backwards from the staged side to take it back out. - return self.applyDiffLineSelection(pane, firstBufferLine, infos, onStagedSide, + return self.applyDiffLineSelection(pane, firstBufferLine, linesByFile, onStagedSide, git_commands.ApplyPatchOpts{Reverse: onStagedSide, Cached: true}) } @@ -79,8 +82,11 @@ func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext, self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) } - infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) - if !ok { + linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if err != nil { + return err + } + if len(linesByFile) == 0 { return nil } @@ -93,7 +99,7 @@ func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext, Title: self.c.Tr.DiscardChangeTitle, Prompt: self.c.Tr.DiscardChangePrompt, HandleConfirm: func() error { - return self.applyDiffLineSelection(pane, firstBufferLine, infos, onStagedSide, + return self.applyDiffLineSelection(pane, firstBufferLine, linesByFile, onStagedSide, git_commands.ApplyPatchOpts{Reverse: true, Cached: onStagedSide}) }, }) @@ -117,20 +123,20 @@ func (self *WorkingTreeDiffActions) DiscardSelectionDisabledReason(types.DiffPan func (self *WorkingTreeDiffActions) EditHunk( pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int, ) error { - infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) - if !ok { - return nil + linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if err != nil { + return err } - file := self.fileForDiffLinePath(infos[0].Path) - if file == nil { + if len(linesByFile) == 0 { return nil } + file := linesByFile[0].file parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, onStagedSide)) lineIndices := patch.ChangeLineIndicesForLines(parsedPatch, - []patch.LineIdentity{infos[0].PatchLineIdentity()}) + []patch.LineIdentity{linesByFile[0].lines[0].PatchLineIdentity()}) if len(lineIndices) == 0 { - return nil + return diffLinesNotFoundError(self.c, file.GetPath()) } hunkIdx := parsedPatch.HunkContainingLine(lineIndices[0]) @@ -193,19 +199,36 @@ func (self *WorkingTreeDiffActions) PatchInclusion() func(types.DiffLineInfo) bo return nil } +// fileDiffLines is the selected change lines of one file of the diff. +type fileDiffLines struct { + file *models.File + lines []types.DiffLineInfo +} + // diffLineSelection resolves what the user has selected in a pane of the focused main // view to the change lines to act on, and reports whether they are the staged side of // the diff — which is a question about the pane, so it is the same for every file of a -// directory's diff. ok is false when the selection holds no change line, in which case +// directory's diff. When the selection holds no change line, it returns no file, and // there is nothing to act on. +// +// A directory's diff spans several files, and a patch is of one file, so the lines are +// grouped by the file they belong to, in the order the diff shows the files. If a line +// belongs to no file of the files panel, the diff and the panel disagree. Acting on the +// other lines would do only part of what the user asked for, so this fails instead. func (self *WorkingTreeDiffActions) diffLineSelection( pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int, -) (infos []types.DiffLineInfo, onStagedSide bool, ok bool) { - infos = self.c.Helpers().DiffLine.ChangeLinesInBufferRange(pane.GetView(), firstBufferLine, lastBufferLine) - if len(infos) == 0 { - return nil, false, false +) (linesByFile []fileDiffLines, onStagedSide bool, err error) { + infos := self.c.Helpers().DiffLine.ChangeLinesInBufferRange(pane.GetView(), firstBufferLine, lastBufferLine) + infosByPath := lo.GroupBy(infos, func(info types.DiffLineInfo) string { return info.Path }) + paths := lo.Uniq(lo.Map(infos, func(info types.DiffLineInfo, _ int) string { return info.Path })) + for _, path := range paths { + file := self.fileForDiffLinePath(path) + if file == nil { + return nil, false, diffLinesFileNotFoundError(self.c, path) + } + linesByFile = append(linesByFile, fileDiffLines{file: file, lines: infosByPath[path]}) } - return infos, self.showsStagedSide(pane), true + return linesByFile, self.showsStagedSide(pane), nil } // applyDiffLineSelection applies the selected change lines, a patch per file, and @@ -215,26 +238,29 @@ func (self *WorkingTreeDiffActions) diffLineSelection( // from once the diff has changed under it. func (self *WorkingTreeDiffActions) applyDiffLineSelection( pane types.DiffPaneContext, firstBufferLine int, - infos []types.DiffLineInfo, onStagedSide bool, opts git_commands.ApplyPatchOpts, + linesByFile []fileDiffLines, onStagedSide bool, opts git_commands.ApplyPatchOpts, ) error { - self.c.LogAction(self.c.Tr.Actions.ApplyPatch) - - // A directory's diff spans several files, and a patch is of one file, so the - // selected lines are grouped by the file they belong to and applied file by file. - infosByFile := lo.GroupBy(infos, func(info types.DiffLineInfo) string { return info.Path }) - acted := set.New[string]() - actedSideRemains := false - for path, fileInfos := range infosByFile { - file := self.fileForDiffLinePath(path) - if file == nil { - continue - } - changesLeft, err := self.applyDiffLines(file, fileInfos, onStagedSide, opts) + // If the lines of one file can't be found, no file is acted on, so every file's + // patch is built before any is applied. + patches := make([]diffLinesPatch, 0, len(linesByFile)) + for _, fileLines := range linesByFile { + p, err := self.buildDiffLinesPatch(fileLines.file, fileLines.lines, onStagedSide, opts) if err != nil { return err } - acted.Add(file.GetPath()) - actedSideRemains = actedSideRemains || changesLeft + patches = append(patches, p) + } + + self.c.LogAction(self.c.Tr.Actions.ApplyPatch) + + acted := set.New[string]() + actedSideRemains := false + for _, p := range patches { + if err := self.applyDiffLinesPatch(p, opts); err != nil { + return err + } + acted.Add(p.file.GetPath()) + actedSideRemains = actedSideRemains || p.changesLeft } if !actedSideRemains { actedSideRemains = self.anyFileHasChangesOnSide(acted, onStagedSide) @@ -268,8 +294,23 @@ func (self *WorkingTreeDiffActions) fileForDiffLinePath(path string) *models.Fil return self.context().FileTreeViewModel.GetFile(relativePath) } -// applyDiffLines applies the given change lines of one file — a line, a hunk, a range — -// as a patch built from that file's own diff: +// diffLinesPatch is how the selected change lines of one file are applied. +type diffLinesPatch struct { + file *models.File + + // Whether the lines are every change of the file's diff. Acting on all of them is + // acting on the file itself, which is staged or unstaged as a whole. + wholeFile bool + + // The patch of the lines, where they are not the whole file. + patch string + + // Whether the diff the lines were found in holds changes they don't cover. + changesLeft bool +} + +// buildDiffLinesPatch builds what applies the given change lines of one file — a line, +// a hunk, a range — as a patch built from that file's own diff: // // - stage: read the unstaged diff, apply it to the index // - unstage: read the staged diff, apply it to the index backwards @@ -284,18 +325,25 @@ func (self *WorkingTreeDiffActions) fileForDiffLinePath(path string) *models.Fil // selected: a patch of the lines you picked keeps whatever context it needs around // them by itself. // -// It reports whether the diff it read holds changes the selection didn't cover. The +// It records whether the diff it read holds changes the selection didn't cover. The // caller uses this to tell whether the side acted on still has anything of this file // in it once we are done. -func (self *WorkingTreeDiffActions) applyDiffLines( +// +// It fails if none of the lines are in the diff. The diff on screen is then not the +// one it read; either the file changed since the diff was shown, or we read the +// lines wrongly. +func (self *WorkingTreeDiffActions) buildDiffLinesPatch( file *models.File, infos []types.DiffLineInfo, sourceCached bool, opts git_commands.ApplyPatchOpts, -) (bool, error) { +) (diffLinesPatch, error) { parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, sourceCached)) patchLineIndices := patch.ChangeLineIndicesForLines(parsedPatch, lo.Map(infos, func(info types.DiffLineInfo, _ int) patch.LineIdentity { return info.PatchLineIdentity() })) + if len(patchLineIndices) == 0 { + return diffLinesPatch{}, diffLinesNotFoundError(self.c, file.GetPath()) + } changesLeft := len(patchLineIndices) < changeLineCount(parsedPatch) @@ -306,10 +354,7 @@ func (self *WorkingTreeDiffActions) applyDiffLines( // and taking that back out leaves an empty file in the index rather than an // untracked one. if !changesLeft && opts.Cached { - if opts.Reverse { - return false, self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked) - } - return false, self.c.Git().WorkingTree.StageFile(file.GetPath()) + return diffLinesPatch{file: file, wholeFile: true}, nil } patchToApply := parsedPatch. @@ -319,11 +364,18 @@ func (self *WorkingTreeDiffActions) applyDiffLines( FileNameOverride: file.GetPath(), }). FormatPlain() - if patchToApply == "" { - return changesLeft, nil - } + return diffLinesPatch{file: file, patch: patchToApply, changesLeft: changesLeft}, nil +} - return changesLeft, self.c.Git().Patch.ApplyPatch(patchToApply, opts) +// applyDiffLinesPatch applies what buildDiffLinesPatch built, as opts says. +func (self *WorkingTreeDiffActions) applyDiffLinesPatch(p diffLinesPatch, opts git_commands.ApplyPatchOpts) error { + if p.wholeFile { + if opts.Reverse { + return self.c.Git().WorkingTree.UnStageFile(p.file.Names(), p.file.Tracked) + } + return self.c.Git().WorkingTree.StageFile(p.file.GetPath()) + } + return self.c.Git().Patch.ApplyPatch(p.patch, opts) } // changeLineCount returns how many of a patch's lines are changes rather than context diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index efd84d722..61f81a1f0 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -748,6 +748,9 @@ type TranslationSet struct { CopySelectedDiffLinesToClipboard string SelectedDiffLinesCopiedToast string SelectionNotFoundInDiffToast string + DiffLinesFileNotFound string + DiffLinesNotFound string + CommitNotInCommitsPanel string NoFilesStagedTitle string NoFilesStagedPrompt string BranchNotFoundTitle string @@ -1938,6 +1941,9 @@ func EnglishTranslationSet() *TranslationSet { CopySelectedDiffLinesToClipboard: "Copy selected diff lines to clipboard", SelectedDiffLinesCopiedToast: "Selected diff lines copied to clipboard", SelectionNotFoundInDiffToast: "Nothing in the selection could be found in the diff", + DiffLinesFileNotFound: "Can't find the file '{{.path}}' that the selected lines belong to", + DiffLinesNotFound: "Can't find the selected lines in the diff of '{{.path}}'", + CommitNotInCommitsPanel: "Can't find commit {{.hash}} in the commits panel", CommitPrefixPatternError: "Error in commitPrefix pattern", NoFilesStagedTitle: "No files staged", NoFilesStagedPrompt: "You have not staged any files. Commit all files?", diff --git a/pkg/integration/tests/main_view/report_lines_missing_from_a_commits_diff.go b/pkg/integration/tests/main_view/report_lines_missing_from_a_commits_diff.go new file mode 100644 index 000000000..e61aab103 --- /dev/null +++ b/pkg/integration/tests/main_view/report_lines_missing_from_a_commits_diff.go @@ -0,0 +1,51 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ReportLinesMissingFromACommitsDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Adding lines to a custom patch that the diff puts where the commit's diff has none shows an error", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInDiffView = false + // A renderer that announces the metadata protocol and passes the diff through, + // except that its hunk header puts the hunk ten lines further down. + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Command: `printf '\033]1717;1\007'; sed 's#@@ -1,3 +1,4 @@#@@ -11,3 +11,4 @@#'`}, + } + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFileAndAdd("file1", "one\nADD1\ntwo\nthree\n") + shell.Commit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("two").IsSelected(), + Contains("one"), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ). + PressPrimaryAction() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("Can't find the selected lines in the diff of 'file1'")). + Confirm() + + // No patch is left behind. + t.Views().Information().Content(DoesNotContain("Building patch")) + }, +}) diff --git a/pkg/integration/tests/main_view/report_lines_missing_from_a_file_changed_since.go b/pkg/integration/tests/main_view/report_lines_missing_from_a_file_changed_since.go new file mode 100644 index 000000000..b10618189 --- /dev/null +++ b/pkg/integration/tests/main_view/report_lines_missing_from_a_file_changed_since.go @@ -0,0 +1,47 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ReportLinesMissingFromAFileChangedSince = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Staging lines that are gone from the file since its diff was shown shows an error", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInDiffView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFile("file1", "one\nADD1\ntwo\nthree\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains(" M file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ) + + // The change is undone outside of lazygit, and nothing has refreshed the diff + // since. + t.Shell().UpdateFile("file1", "one\ntwo\nthree\n") + + t.Views().Main(). + PressPrimaryAction() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("Can't find the selected lines in the diff of 'file1'")). + Confirm() + }, +}) diff --git a/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_commits_diff.go b/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_commits_diff.go new file mode 100644 index 000000000..68a5d3a1e --- /dev/null +++ b/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_commits_diff.go @@ -0,0 +1,51 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ReportLinesOfAFileMissingFromTheCommitsDiff = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Adding lines to a custom patch that the diff puts in a file the commit doesn't change shows an error", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInDiffView = false + // A renderer that announces the metadata protocol and passes the diff through, + // except that its +++ line names a file in another directory. + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Command: `printf '\033]1717;1\007'; sed 's#+++ b/#+++ b/elsewhere/#'`}, + } + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFileAndAdd("file1", "one\nADD1\ntwo\nthree\n") + shell.Commit("two") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("two").IsSelected(), + Contains("one"), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ). + PressPrimaryAction() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("Can't find the file 'elsewhere/file1' that the selected lines belong to")). + Confirm() + + // No patch is left behind. + t.Views().Information().Content(DoesNotContain("Building patch")) + }, +}) diff --git a/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_files_panel.go b/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_files_panel.go new file mode 100644 index 000000000..1e03ba5eb --- /dev/null +++ b/pkg/integration/tests/main_view/report_lines_of_a_file_missing_from_the_files_panel.go @@ -0,0 +1,50 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ReportLinesOfAFileMissingFromTheFilesPanel = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Staging lines that the diff puts in a file the files panel doesn't have shows an error", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInDiffView = false + // A renderer that announces the metadata protocol and passes the diff through, + // except that its +++ line names a file in another directory. + cfg.GetUserConfig().Git.DiffRenderers = []config.DiffRendererConfig{ + {Command: `printf '\033]1717;1\007'; sed 's#+++ b/#+++ b/elsewhere/#'`}, + } + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFile("file1", "one\nADD1\ntwo\nthree\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains(" M file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ). + PressPrimaryAction() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Equals("Can't find the file 'elsewhere/file1' that the selected lines belong to")). + Confirm() + + t.Views().Files().Lines( + Contains(" M file1"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index c7a8219bc..566baf0dc 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -463,6 +463,10 @@ var tests = []*components.IntegrationTest{ main_view.RemovePartOfAddedFileFromCommit, main_view.RemovePatchFromCommit, main_view.RenderTheDiffBesideThePatchMarks, + main_view.ReportLinesMissingFromACommitsDiff, + main_view.ReportLinesMissingFromAFileChangedSince, + main_view.ReportLinesOfAFileMissingFromTheCommitsDiff, + main_view.ReportLinesOfAFileMissingFromTheFilesPanel, main_view.ResetAPatchBuiltFromACommitsDiff, main_view.ResetPatchWithEscape, main_view.ResetThePatchFromThePaneShowingIt,