Skip to content

Commit

Permalink
Show the number of lines changed per file in working file tree view (#…
Browse files Browse the repository at this point in the history
…4015)

- **PR Description**
Implements Issue #3643
Adds the number of line changes to the end of each file line in the
Files view.
Also adds the possibility for the user to enable and disable this
feature through the UserConfig.
<img width="323" alt="screenshot"
src="https://github.com/user-attachments/assets/6f818dd4-fbf5-49f2-b338-1b1fcc73f73a">

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [ ] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
* [ ] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
  • Loading branch information
stefanhaller authored Dec 8, 2024
2 parents f3a5c18 + f455f99 commit 4a7cf60
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 33 deletions.
3 changes: 3 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ gui:
# This can be toggled from within Lazygit with the '~' key, but that will not change the default.
showFileTree: true

# If true, show the number of lines changed per file in the Files view
showNumstatInFilesView: false

# If true, show a random tip in the command log when Lazygit starts
showRandomTip: true

Expand Down
63 changes: 63 additions & 0 deletions pkg/commands/git_commands/file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git_commands
import (
"fmt"
"path/filepath"
"strconv"
"strings"

"github.com/jesseduffield/lazygit/pkg/commands/models"
Expand Down Expand Up @@ -48,6 +49,14 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
}
files := []*models.File{}

fileDiffs := map[string]FileDiff{}
if self.GitCommon.Common.UserConfig().Gui.ShowNumstatInFilesView {
fileDiffs, err = self.getFileDiffs()
if err != nil {
self.Log.Error(err)
}
}

for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
Expand All @@ -60,6 +69,11 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
DisplayString: status.StatusString,
}

if diff, ok := fileDiffs[status.Name]; ok {
file.LinesAdded = diff.LinesAdded
file.LinesDeleted = diff.LinesDeleted
}

models.SetStatusFields(file, status.Change)
files = append(files, file)
}
Expand Down Expand Up @@ -87,6 +101,45 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
return files
}

type FileDiff struct {
LinesAdded int
LinesDeleted int
}

func (fileLoader *FileLoader) getFileDiffs() (map[string]FileDiff, error) {
diffs, err := fileLoader.gitDiffNumStat()
if err != nil {
return nil, err
}

splitLines := strings.Split(diffs, "\x00")

fileDiffs := map[string]FileDiff{}
for _, line := range splitLines {
splitLine := strings.Split(line, "\t")
if len(splitLine) != 3 {
continue
}

linesAdded, err := strconv.Atoi(splitLine[0])
if err != nil {
continue
}
linesDeleted, err := strconv.Atoi(splitLine[1])
if err != nil {
continue
}

fileName := splitLine[2]
fileDiffs[fileName] = FileDiff{
LinesAdded: linesAdded,
LinesDeleted: linesDeleted,
}
}

return fileDiffs, nil
}

// GitStatus returns the file status of the repo
type GitStatusOptions struct {
NoRenames bool
Expand All @@ -100,6 +153,16 @@ type FileStatus struct {
PreviousName string
}

func (fileLoader *FileLoader) gitDiffNumStat() (string, error) {
return fileLoader.cmd.New(
NewGitCmd("diff").
Arg("--numstat").
Arg("-z").
Arg("HEAD").
ToArgv(),
).DontLog().RunWithOutput()
}

func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
cmdArgs := NewGitCmd("status").
Arg(opts.UntrackedFilesArg).
Expand Down
72 changes: 47 additions & 25 deletions pkg/commands/git_commands/file_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,35 @@ import (

func TestFileGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
similarityThreshold int
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
testName string
similarityThreshold int
runner oscommands.ICmdObjRunner
showNumstatInFilesView bool
expectedFiles []*models.File
}

scenarios := []scenario{
{
"No files found",
50,
oscommands.NewFakeRunner(t).
testName: "No files found",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil),
[]*models.File{},
expectedFiles: []*models.File{},
},
{
"Several files found",
50,
oscommands.NewFakeRunner(t).
testName: "Several files found",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
).
ExpectGitArgs([]string{"diff", "--numstat", "-z", "HEAD"},
"4\t1\tfile1.txt\x001\t0\tfile2.txt\x002\t2\tfile3.txt\x000\t2\tfile4.txt\x002\t2\tfile5.txt",
nil,
),
[]*models.File{
showNumstatInFilesView: true,
expectedFiles: []*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
Expand All @@ -45,6 +51,8 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
ShortStatus: "MM",
LinesAdded: 4,
LinesDeleted: 1,
},
{
Name: "file3.txt",
Expand All @@ -57,6 +65,8 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
ShortStatus: "A ",
LinesAdded: 2,
LinesDeleted: 2,
},
{
Name: "file2.txt",
Expand All @@ -69,6 +79,8 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
ShortStatus: "AM",
LinesAdded: 1,
LinesDeleted: 0,
},
{
Name: "file4.txt",
Expand All @@ -81,6 +93,8 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
ShortStatus: "??",
LinesAdded: 0,
LinesDeleted: 2,
},
{
Name: "file5.txt",
Expand All @@ -93,15 +107,17 @@ func TestFileGetStatusFiles(t *testing.T) {
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
ShortStatus: "UU",
LinesAdded: 2,
LinesDeleted: 2,
},
},
},
{
"File with new line char",
50,
oscommands.NewFakeRunner(t).
testName: "File with new line char",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM a\nb.txt", nil),
[]*models.File{
expectedFiles: []*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
Expand All @@ -117,14 +133,14 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
{
"Renamed files",
50,
oscommands.NewFakeRunner(t).
testName: "Renamed files",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
expectedFiles: []*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
Expand Down Expand Up @@ -154,14 +170,14 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
{
"File with arrow in name",
50,
oscommands.NewFakeRunner(t).
testName: "File with arrow in name",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
`?? a -> b.txt`,
nil,
),
[]*models.File{
expectedFiles: []*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
Expand All @@ -185,8 +201,14 @@ func TestFileGetStatusFiles(t *testing.T) {
appState := &config.AppState{}
appState.RenameSimilarityThreshold = s.similarityThreshold

userConfig := &config.UserConfig{
Gui: config.GuiConfig{
ShowNumstatInFilesView: s.showNumstatInFilesView,
},
}

loader := &FileLoader{
GitCommon: buildGitCommon(commonDeps{appState: appState}),
GitCommon: buildGitCommon(commonDeps{appState: appState, userConfig: userConfig}),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
getFileType: func(string) string { return "file" },
Expand Down
2 changes: 2 additions & 0 deletions pkg/commands/models/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ type File struct {
HasInlineMergeConflicts bool
DisplayString string
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
LinesDeleted int
LinesAdded int

// If true, this must be a worktree folder
IsWorktree bool
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ type GuiConfig struct {
// If true, display the files in the file views as a tree. If false, display the files as a flat list.
// This can be toggled from within Lazygit with the '~' key, but that will not change the default.
ShowFileTree bool `yaml:"showFileTree"`
// If true, show the number of lines changed per file in the Files view
ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"`
// If true, show a random tip in the command log when Lazygit starts
ShowRandomTip bool `yaml:"showRandomTip"`
// If true, show the command log
Expand Down Expand Up @@ -714,6 +716,7 @@ func GetDefaultConfig() *UserConfig {
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowNumstatInFilesView: false,
ShowRandomTip: true,
ShowIcons: false,
NerdFontsVersion: "",
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/context/working_tree_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {

getDisplayStrings := func(_ int, _ int) [][]string {
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons)
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
Expand Down
27 changes: 26 additions & 1 deletion pkg/gui/presentation/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ func RenderFileTree(
tree filetree.IFileTree,
submoduleConfigs []*models.SubmoduleConfig,
showFileIcons bool,
showNumstat bool,
) []string {
collapsedPaths := tree.CollapsedPaths()
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
fileNode := filetree.NewFileNode(node)

return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showFileIcons, submoduleConfigs, node)
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node)
})
}

Expand Down Expand Up @@ -111,6 +112,7 @@ func getFileLine(
hasStagedChanges bool,
treeDepth int,
visualDepth int,
showNumstat,
showFileIcons bool,
submoduleConfigs []*models.SubmoduleConfig,
node *filetree.Node[models.File],
Expand Down Expand Up @@ -165,6 +167,12 @@ func getFileLine(
output += theme.DefaultTextColor.Sprint(" (submodule)")
}

if file != nil && showNumstat {
if lineChanges := formatLineChanges(file.LinesAdded, file.LinesDeleted); lineChanges != "" {
output += " " + lineChanges
}
}

return output
}

Expand All @@ -186,6 +194,23 @@ func formatFileStatus(file *models.File, restColor style.TextStyle) string {
return firstCharCl.Sprint(firstChar) + secondCharCl.Sprint(secondChar)
}

func formatLineChanges(linesAdded, linesDeleted int) string {
output := ""

if linesAdded != 0 {
output += style.FgGreen.Sprintf("+%d", linesAdded)
}

if linesDeleted != 0 {
if output != "" {
output += " "
}
output += style.FgRed.Sprintf("-%d", linesDeleted)
}

return output
}

func getCommitFileLine(
isCollapsed bool,
treeDepth int,
Expand Down
Loading

0 comments on commit 4a7cf60

Please sign in to comment.