mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-14 09:36:24 -04:00
145 lines
5 KiB
Go
145 lines
5 KiB
Go
package controllers
|
|
|
|
import (
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// diffPathsForNode returns the paths to limit a diff command to for showing the
|
|
// changes of the given node. files are all the files that the diff contains,
|
|
// while root is the root of the tree the node belongs to, which holds only the
|
|
// files matching the text filter when there is one.
|
|
func diffPathsForNode[T any, PT fileWithNames[T]](node *filetree.Node[T], root *filetree.Node[T], files []*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
|
|
for _, file := range filesInDir[T, PT](filesInTree(root), dir) {
|
|
paths = append(paths, PT(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.
|
|
var outsidePaths []string
|
|
for _, f := range filesInDir[T, PT](files, dir) {
|
|
file := PT(f)
|
|
if p := file.GetPath(); !isInDir(p, dir) {
|
|
outsidePaths = append(outsidePaths, p)
|
|
}
|
|
if p := file.GetPreviousPath(); p != "" && !isInDir(p, dir) {
|
|
outsidePaths = append(outsidePaths, p)
|
|
}
|
|
}
|
|
|
|
return dropContainedPaths(append([]string{dir}, collapseToDirs[T, PT](outsidePaths, files, dir)...))
|
|
}
|
|
|
|
// dropContainedPaths removes the paths that another one of them contains, since
|
|
// a pathspec that matches a directory matches everything below it anyway.
|
|
func dropContainedPaths(paths []string) []string {
|
|
return lo.Filter(paths, func(p string, _ int) bool {
|
|
return !lo.SomeBy(paths, func(other string) bool {
|
|
return other != p && isInDir(p, other)
|
|
})
|
|
})
|
|
}
|
|
|
|
// collapseToDirs replaces each of the given paths with the highest directory
|
|
// that can stand in for it, so that moving a whole directory elsewhere costs a
|
|
// single pathspec rather than one per file. There is a limit to how long a
|
|
// command line may get, and a commit can move a great many files at once.
|
|
func collapseToDirs[T any, PT fileWithNames[T]](paths []string, files []*T, dir string) []string {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// A directory can stand in for the paths under it as long as everything it
|
|
// contains ends up in the diff anyway, which is to say as long as all of it
|
|
// is in the directory we are diffing too.
|
|
canStandIn := make(map[string]bool)
|
|
standsIn := func(candidate string) bool {
|
|
if result, ok := canStandIn[candidate]; ok {
|
|
return result
|
|
}
|
|
|
|
result := lo.EveryBy(files, func(file *T) bool {
|
|
return !fileIsInDir[T, PT](file, candidate) || fileIsInDir[T, PT](file, dir)
|
|
})
|
|
canStandIn[candidate] = result
|
|
return result
|
|
}
|
|
|
|
return lo.Uniq(lo.Map(paths, func(p string, _ int) string {
|
|
// A directory that can't stand in for the path rules out its parents
|
|
// too, since they contain everything it contains. We stop short of the
|
|
// repository root: it would leave the command with nothing to say about
|
|
// the directory whose diff we are showing.
|
|
for candidate := path.Dir(p); candidate != "." && standsIn(candidate); candidate = path.Dir(candidate) {
|
|
p = candidate
|
|
}
|
|
return p
|
|
}))
|
|
}
|
|
|
|
func filesInTree[T any](root *filetree.Node[T]) []*T {
|
|
files := []*T{}
|
|
_ = root.ForEachFile(func(file *T) error {
|
|
files = append(files, file)
|
|
return nil
|
|
})
|
|
return files
|
|
}
|
|
|
|
// filesInDir returns the files that the given directory contains, either at
|
|
// their current or at their previous path.
|
|
func filesInDir[T any, PT fileWithNames[T]](files []*T, dir string) []*T {
|
|
return lo.Filter(files, func(file *T, _ int) bool {
|
|
return fileIsInDir[T, PT](file, dir)
|
|
})
|
|
}
|
|
|
|
func fileIsInDir[T any, PT fileWithNames[T]](f *T, dir string) bool {
|
|
file := PT(f)
|
|
previousPath := file.GetPreviousPath()
|
|
return isInDir(file.GetPath(), dir) || (previousPath != "" && isInDir(previousPath, dir))
|
|
}
|
|
|
|
func isInDir(path string, dir string) bool {
|
|
// "." is the root item, which contains every file
|
|
return dir == "." || strings.HasPrefix(path, dir+"/")
|
|
}
|
|
|
|
// 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.
|
|
func repoRelativePath(worktreePath string, absolutePath string) string {
|
|
relativePath, err := filepath.Rel(worktreePath, absolutePath)
|
|
if err != nil || strings.HasPrefix(relativePath, "..") {
|
|
return ""
|
|
}
|
|
return filepath.ToSlash(relativePath)
|
|
}
|