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.
This commit is contained in:
Stefan Haller
2026-08-17 09:32:30 +02:00
parent 2c9187acb9
commit 40cb4bb24d
5 changed files with 48 additions and 55 deletions
+6 -12
View File
@@ -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()
@@ -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 {
+35
View File
@@ -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()}
}
+3 -20
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
}