Show an error if the file of the selected diff lines can't be found

If the path in the diff header of a selected line doesn't name a file
of the files panel, staging or discarding the line does nothing. The
command log shows "Apply patch", but nothing else happens, so the user
has nothing to go on when reporting it. Adding a line of a commit's
diff to a custom patch fails in the same silent way if the path
doesn't name a file of that diff. A diff renderer that names files
differently can cause this, and so can a bug in how we read the path,
like the one we had with diff.mnemonicPrefix.

Show an error that names the path instead, and do nothing else. If a
selection spans several files, don't act on the files we did find,
because that would do only part of what the user asked for. Do the
same for opening a line of a diff in its pull request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-10 13:04:41 +02:00
co-authored by Claude Opus 5.5
parent a29c70f1f3
commit 15ac3c28c9
8 changed files with 143 additions and 18 deletions
+5 -10
View File
@@ -108,9 +108,6 @@ func (self *CommitDiffActions) PrimaryAction(pane types.DiffPaneContext, firstBu
if err != nil {
return err
}
if len(selection.paths) == 0 {
return nil
}
patchBuilder := self.c.Git().Patch.PatchBuilder
from, reverse := self.patchEndpoints(target)
@@ -182,7 +179,7 @@ func (self *CommitDiffActions) removePatchLines(
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{}
@@ -243,9 +240,6 @@ func (self *CommitDiffActions) DiscardSelection(pane types.DiffPaneContext, firs
if err != nil {
return err
}
if len(selection.paths) == 0 {
return nil
}
patchBuilder := self.c.Git().Patch.PatchBuilder
prompt := lo.Ternary(patchBuilder.IsEmpty(),
@@ -353,7 +347,8 @@ type commitDiffSelection struct {
}
// selectionByFile groups the given lines of the commit's diff by the file they belong
// to. A line whose path is no file of this repo is left out.
// 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 {
@@ -362,8 +357,8 @@ func (self *CommitDiffActions) selectionByFile(lines []types.DiffLineInfo) (comm
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 := selection.linesByPath[path]; !seen {
selection.paths = append(selection.paths, path)
+14
View File
@@ -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,18 @@ 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}))
}
// 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.
+1 -1
View File
@@ -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)
@@ -59,7 +59,10 @@ func (self *WorkingTreeDiffActions) PrimaryAction(pane types.DiffPaneContext, fi
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
}
linesByFile, onStagedSide := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
if err != nil {
return err
}
if len(linesByFile) == 0 {
return nil
}
@@ -79,7 +82,10 @@ func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext,
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
}
linesByFile, onStagedSide := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
if err != nil {
return err
}
if len(linesByFile) == 0 {
return nil
}
@@ -117,7 +123,10 @@ func (self *WorkingTreeDiffActions) DiscardSelectionDisabledReason(types.DiffPan
func (self *WorkingTreeDiffActions) EditHunk(
pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int,
) error {
linesByFile, onStagedSide := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
linesByFile, onStagedSide, err := self.diffLineSelection(pane, firstBufferLine, lastBufferLine)
if err != nil {
return err
}
if len(linesByFile) == 0 {
return nil
}
@@ -203,21 +212,23 @@ type fileDiffLines struct {
// 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.
// 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,
) (linesByFile []fileDiffLines, onStagedSide bool) {
) (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 {
continue
return nil, false, diffLinesFileNotFoundError(self.c, path)
}
linesByFile = append(linesByFile, fileDiffLines{file: file, lines: infosByPath[path]})
}
return linesByFile, self.showsStagedSide(pane)
return linesByFile, self.showsStagedSide(pane), nil
}
// applyDiffLineSelection applies the selected change lines, a patch per file, and
+2
View File
@@ -748,6 +748,7 @@ type TranslationSet struct {
CopySelectedDiffLinesToClipboard string
SelectedDiffLinesCopiedToast string
SelectionNotFoundInDiffToast string
DiffLinesFileNotFound string
NoFilesStagedTitle string
NoFilesStagedPrompt string
BranchNotFoundTitle string
@@ -1938,6 +1939,7 @@ 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",
CommitPrefixPatternError: "Error in commitPrefix pattern",
NoFilesStagedTitle: "No files staged",
NoFilesStagedPrompt: "You have not staged any files. Commit all files?",
@@ -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"))
},
})
@@ -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"),
)
},
})
+2
View File
@@ -463,6 +463,8 @@ var tests = []*components.IntegrationTest{
main_view.RemovePartOfAddedFileFromCommit,
main_view.RemovePatchFromCommit,
main_view.RenderTheDiffBesideThePatchMarks,
main_view.ReportLinesOfAFileMissingFromTheCommitsDiff,
main_view.ReportLinesOfAFileMissingFromTheFilesPanel,
main_view.ResetAPatchBuiltFromACommitsDiff,
main_view.ResetPatchWithEscape,
main_view.ResetThePatchFromThePaneShowingIt,