From 40cb4bb24df312f759ee658307ab8ab78d03b2ec Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 12 Aug 2026 10:41:50 +0200 Subject: [PATCH] Extract a single helper for the paths a node's diff is limited to The files and commit files panels each had their own copy of this, one of which used to be missing the previous path of a rename. Growing them apart again is the last thing we want, since the next commit needs to teach both of them about renames that cross a directory boundary. The files panel version only returned paths for the filtered case, and left it to WorktreeFileDiffCmdObj to derive the rest from the node; now that all callers pass the paths in, that command doesn't need to know about renames at all. --- pkg/commands/git_commands/working_tree.go | 18 ++++------ .../controllers/commits_files_controller.go | 25 ++----------- pkg/gui/controllers/diff_paths.go | 35 +++++++++++++++++++ pkg/gui/controllers/files_controller.go | 23 ++---------- pkg/gui/controllers/submodules_controller.go | 2 +- 5 files changed, 48 insertions(+), 55 deletions(-) create mode 100644 pkg/gui/controllers/diff_paths.go diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index 1f9d9653b..846b359b3 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -385,27 +385,22 @@ func (self *WorkingTreeCommands) Exclude(filename string) error { // WorktreeFileDiff returns the diff of a file func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool, cached bool) string { // for now we assume an error means the file was deleted - s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, nil).RunWithOutput() + s, _ := self.WorktreeFileDiffCmdObj(file, plain, cached, file.Names()).RunWithOutput() return s } -// WorktreeFileDiffCmdObj returns a command object for diffing a file or directory -// in the working tree. When pathOverrides is non-empty, those paths are used instead of -// the node's path (used to diff only filtered/visible files within a directory). -func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, pathOverrides []string) *oscommands.CmdObj { +// WorktreeFileDiffCmdObj returns a command object for diffing the given paths +// in the working tree. node is the item they belong to; all it decides is +// whether git has to compare against /dev/null, which is the case for a file +// that isn't in the index yet. +func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, paths []string) *oscommands.CmdObj { colorArg := self.diffRendererConfigManager.GetColorArg() if plain { colorArg = "never" } - prevPath := node.GetPreviousPath() noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() - paths := pathOverrides - if len(paths) == 0 { - paths = []string{node.GetPath()} - } - cmdArgs := NewGitCmd("diff"). AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), !plain). Arg("--submodule"). @@ -415,7 +410,6 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain Arg("--"). ArgIf(noIndex, "/dev/null"). Arg(paths...). - ArgIf(prevPath != "", prevPath). Dir(self.repoPaths.worktreePath). ToArgv() diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 748355f67..c01658b03 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -175,7 +175,7 @@ func (self *CommitFilesController) GetOnRenderToMain() func() { from, to := self.context().GetFromAndToForDiff() from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from) - paths := self.pathsForDiff(node) + paths := pathsForDiff(node.Raw(), self.context().IsFiltering()) cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj(from, to, reverse, paths, false) task := types.NewRunPtyTask(cmdObj.GetCmd()) @@ -263,7 +263,8 @@ func (self *CommitFilesController) openCopyMenu() error { copyFileDiffItem := &types.MenuItem{ Label: self.c.Tr.CopySelectedDiff, OnPress: func() error { - return self.copyDiffToClipboard(self.pathsForDiff(node), self.c.Tr.FileDiffCopiedToast) + paths := pathsForDiff(node.Raw(), self.context().IsFiltering()) + return self.copyDiffToClipboard(paths, self.c.Tr.FileDiffCopiedToast) }, DisabledReason: self.require(self.singleItemSelected())(), Keys: menuKey('s'), @@ -616,26 +617,6 @@ func (self *CommitFilesController) GetOnClickFocusedMainView() func(mainViewName } } -// pathsForDiff returns the file paths to use for a diff command. When a text -// filter is active and the node is a directory, only the visible (filtered) -// file paths are returned so the diff reflects what the user sees. -func (self *CommitFilesController) pathsForDiff(node *filetree.CommitFileNode) []string { - if !node.IsFile() && self.context().IsFiltering() { - var paths []string - _ = node.ForEachFile(func(file *models.CommitFile) error { - // For a rename we need to pass both paths so that git detects it as - // a rename rather than an unrelated delete and add. - paths = append(paths, file.Names()...) - return nil - }) - return paths - } - if file := node.GetFile(); file != nil { - return file.Names() - } - return []string{node.GetPath()} -} - // NOTE: these functions are identical to those in files_controller.go (except for types) and // could also be cleaned up with some generics func normalisedSelectedCommitFileNodes(selectedNodes []*filetree.CommitFileNode) []*filetree.CommitFileNode { diff --git a/pkg/gui/controllers/diff_paths.go b/pkg/gui/controllers/diff_paths.go new file mode 100644 index 000000000..31fc81757 --- /dev/null +++ b/pkg/gui/controllers/diff_paths.go @@ -0,0 +1,35 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/filetree" +) + +// Both models.File and models.CommitFile satisfy this. Names returns the file's +// path, plus the path it was renamed from if it is a rename. +type fileWithNames[T any] interface { + *T + Names() []string +} + +// pathsForDiff returns the paths to limit a diff command to for showing the +// changes of the given node. For a directory this is the directory itself, +// unless a filter is active, in which case we list the files that are visible +// under it. +func pathsForDiff[T any, PT fileWithNames[T]](node *filetree.Node[T], isFiltering bool) []string { + if file := node.GetFile(); file != nil { + return PT(file).Names() + } + + if isFiltering { + var paths []string + _ = node.ForEachFile(func(file *T) error { + // For a rename we need to pass both paths so that git detects it as + // a rename rather than an unrelated delete and add. + paths = append(paths, PT(file).Names()...) + return nil + }) + return paths + } + + return []string{node.GetPath()} +} diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 5d8cf1c41..9e7f3bf1c 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -369,8 +369,8 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) { split := self.c.UserConfig().Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) mainShowsStaged := !split && node.GetHasStagedChanges() - pathOverrides := self.pathOverridesForDiff(node) - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, pathOverrides) + paths := pathsForDiff(node.Raw(), self.context().IsFiltering()) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, paths) title := self.c.Tr.UnstagedChanges if mainShowsStaged { title = self.c.Tr.StagedChanges @@ -385,7 +385,7 @@ func (self *FilesController) renderWorkingTreeDiff(node *filetree.FileNode) { } if split { - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, pathOverrides) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, paths) title := self.c.Tr.StagedChanges if mainShowsStaged { @@ -643,23 +643,6 @@ func (self *FilesController) press(nodes []*filetree.FileNode) error { return nil } -// pathOverridesForDiff returns file paths to override the node's path in diff -// commands when a text filter is active and the node is a directory. This -// ensures the diff only shows filtered/visible files. -func (self *FilesController) pathOverridesForDiff(node *filetree.FileNode) []string { - if !node.IsFile() && self.context().IsFiltering() { - var paths []string - _ = node.ForEachFile(func(file *models.File) error { - // For a rename we need to pass both paths so that git detects it as - // a rename rather than an unrelated delete and add. - paths = append(paths, file.Names()...) - return nil - }) - return paths - } - return nil -} - // unstageFilteredFiles unstages only the visible (filtered) files from the // given nodes, correctly partitioning by tracked/untracked. func (self *FilesController) unstageFilteredFiles(nodes []*filetree.FileNode) error { diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index a2dd22ed3..82ca509ca 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -123,7 +123,7 @@ func (self *SubmodulesController) GetOnRenderToMain() func() { if file == nil { task = types.NewRenderStringTask(prefix) } else { - cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, nil) + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, file.Names()) task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) } }