jesseduffield.lazygit/pkg/gui/controllers/diff_paths.go
Stefan Haller bee03d3b98 Show renames when diffing a directory that a file was moved into or out of
Git limits its tree diff by the pathspec before it looks for renames, so
a directory only ever gets one end of a rename whose other end is outside
it. Nothing is left to pair up, and the file turns into an addition or a
deletion that the commit doesn't contain.

Pass the other end along with the directory. This is bounded by the
number of renames that cross the directory's boundary, so it costs
nothing at all for the vast majority of commits.
2026-08-17 09:32:30 +02:00

72 lines
2.3 KiB
Go

package controllers
import (
"strings"
"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
GetPath() string
GetPreviousPath() string
Names() []string
}
// pathsForDiff returns the paths to limit a diff command to for showing the
// changes of the given node. root is the root of the tree that the node belongs
// to, and isFiltering says whether that tree is reduced to the files matching a
// text filter.
func pathsForDiff[T any, PT fileWithNames[T]](node *filetree.Node[T], root *filetree.Node[T], isFiltering bool) []string {
if file := node.GetFile(); file != nil {
return PT(file).Names()
}
dir := node.GetPath()
if isFiltering {
// Passing the directory would bring back the files that the filter hides,
// so we spell out the ones it leaves.
var paths []string
forEachFileInDir[T, PT](root, dir, func(file PT) {
paths = append(paths, file.Names()...)
})
return paths
}
// The directory covers everything below it, but git only pairs up the two
// ends of a rename if both are in the pathspec, and one end can well be
// outside the directory. Without that end we would get an addition or a
// deletion where the diff has a rename.
paths := []string{dir}
forEachFileInDir[T, PT](root, dir, func(file PT) {
if path := file.GetPath(); !isInDir(path, dir) {
paths = append(paths, path)
}
if previousPath := file.GetPreviousPath(); previousPath != "" && !isInDir(previousPath, dir) {
paths = append(paths, previousPath)
}
})
return paths
}
// forEachFileInDir calls cb for each file in the tree that the given directory
// contains, either at its current or at its previous path.
func forEachFileInDir[T any, PT fileWithNames[T]](root *filetree.Node[T], dir string, cb func(PT)) {
_ = root.ForEachFile(func(f *T) error {
file := PT(f)
previousPath := file.GetPreviousPath()
if isInDir(file.GetPath(), dir) || (previousPath != "" && isInDir(previousPath, dir)) {
cb(file)
}
return nil
})
}
func isInDir(path string, dir string) bool {
// "." is the root item, which contains every file
return dir == "." || strings.HasPrefix(path, dir+"/")
}