Skip to content

Commit f455f99

Browse files
johannaschwarzstefanhaller
authored andcommitted
Add user config gui.showNumstatInFilesView
When enabled, it adds "+n -m" after each file in the Files panel to show how many lines were added and deleted, as with `git diff --numstat` on the command line.
1 parent f3a5c18 commit f455f99

File tree

9 files changed

+174
-33
lines changed

9 files changed

+174
-33
lines changed

docs/Config.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ gui:
164164
# This can be toggled from within Lazygit with the '~' key, but that will not change the default.
165165
showFileTree: true
166166

167+
# If true, show the number of lines changed per file in the Files view
168+
showNumstatInFilesView: false
169+
167170
# If true, show a random tip in the command log when Lazygit starts
168171
showRandomTip: true
169172

pkg/commands/git_commands/file_loader.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package git_commands
33
import (
44
"fmt"
55
"path/filepath"
6+
"strconv"
67
"strings"
78

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

52+
fileDiffs := map[string]FileDiff{}
53+
if self.GitCommon.Common.UserConfig().Gui.ShowNumstatInFilesView {
54+
fileDiffs, err = self.getFileDiffs()
55+
if err != nil {
56+
self.Log.Error(err)
57+
}
58+
}
59+
5160
for _, status := range statuses {
5261
if strings.HasPrefix(status.StatusString, "warning") {
5362
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
@@ -60,6 +69,11 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
6069
DisplayString: status.StatusString,
6170
}
6271

72+
if diff, ok := fileDiffs[status.Name]; ok {
73+
file.LinesAdded = diff.LinesAdded
74+
file.LinesDeleted = diff.LinesDeleted
75+
}
76+
6377
models.SetStatusFields(file, status.Change)
6478
files = append(files, file)
6579
}
@@ -87,6 +101,45 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
87101
return files
88102
}
89103

104+
type FileDiff struct {
105+
LinesAdded int
106+
LinesDeleted int
107+
}
108+
109+
func (fileLoader *FileLoader) getFileDiffs() (map[string]FileDiff, error) {
110+
diffs, err := fileLoader.gitDiffNumStat()
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
splitLines := strings.Split(diffs, "\x00")
116+
117+
fileDiffs := map[string]FileDiff{}
118+
for _, line := range splitLines {
119+
splitLine := strings.Split(line, "\t")
120+
if len(splitLine) != 3 {
121+
continue
122+
}
123+
124+
linesAdded, err := strconv.Atoi(splitLine[0])
125+
if err != nil {
126+
continue
127+
}
128+
linesDeleted, err := strconv.Atoi(splitLine[1])
129+
if err != nil {
130+
continue
131+
}
132+
133+
fileName := splitLine[2]
134+
fileDiffs[fileName] = FileDiff{
135+
LinesAdded: linesAdded,
136+
LinesDeleted: linesDeleted,
137+
}
138+
}
139+
140+
return fileDiffs, nil
141+
}
142+
90143
// GitStatus returns the file status of the repo
91144
type GitStatusOptions struct {
92145
NoRenames bool
@@ -100,6 +153,16 @@ type FileStatus struct {
100153
PreviousName string
101154
}
102155

156+
func (fileLoader *FileLoader) gitDiffNumStat() (string, error) {
157+
return fileLoader.cmd.New(
158+
NewGitCmd("diff").
159+
Arg("--numstat").
160+
Arg("-z").
161+
Arg("HEAD").
162+
ToArgv(),
163+
).DontLog().RunWithOutput()
164+
}
165+
103166
func (self *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
104167
cmdArgs := NewGitCmd("status").
105168
Arg(opts.UntrackedFilesArg).

pkg/commands/git_commands/file_loader_test.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,35 @@ import (
1111

1212
func TestFileGetStatusFiles(t *testing.T) {
1313
type scenario struct {
14-
testName string
15-
similarityThreshold int
16-
runner oscommands.ICmdObjRunner
17-
expectedFiles []*models.File
14+
testName string
15+
similarityThreshold int
16+
runner oscommands.ICmdObjRunner
17+
showNumstatInFilesView bool
18+
expectedFiles []*models.File
1819
}
1920

2021
scenarios := []scenario{
2122
{
22-
"No files found",
23-
50,
24-
oscommands.NewFakeRunner(t).
23+
testName: "No files found",
24+
similarityThreshold: 50,
25+
runner: oscommands.NewFakeRunner(t).
2526
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "", nil),
26-
[]*models.File{},
27+
expectedFiles: []*models.File{},
2728
},
2829
{
29-
"Several files found",
30-
50,
31-
oscommands.NewFakeRunner(t).
30+
testName: "Several files found",
31+
similarityThreshold: 50,
32+
runner: oscommands.NewFakeRunner(t).
3233
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
3334
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
3435
nil,
36+
).
37+
ExpectGitArgs([]string{"diff", "--numstat", "-z", "HEAD"},
38+
"4\t1\tfile1.txt\x001\t0\tfile2.txt\x002\t2\tfile3.txt\x000\t2\tfile4.txt\x002\t2\tfile5.txt",
39+
nil,
3540
),
36-
[]*models.File{
41+
showNumstatInFilesView: true,
42+
expectedFiles: []*models.File{
3743
{
3844
Name: "file1.txt",
3945
HasStagedChanges: true,
@@ -45,6 +51,8 @@ func TestFileGetStatusFiles(t *testing.T) {
4551
HasInlineMergeConflicts: false,
4652
DisplayString: "MM file1.txt",
4753
ShortStatus: "MM",
54+
LinesAdded: 4,
55+
LinesDeleted: 1,
4856
},
4957
{
5058
Name: "file3.txt",
@@ -57,6 +65,8 @@ func TestFileGetStatusFiles(t *testing.T) {
5765
HasInlineMergeConflicts: false,
5866
DisplayString: "A file3.txt",
5967
ShortStatus: "A ",
68+
LinesAdded: 2,
69+
LinesDeleted: 2,
6070
},
6171
{
6272
Name: "file2.txt",
@@ -69,6 +79,8 @@ func TestFileGetStatusFiles(t *testing.T) {
6979
HasInlineMergeConflicts: false,
7080
DisplayString: "AM file2.txt",
7181
ShortStatus: "AM",
82+
LinesAdded: 1,
83+
LinesDeleted: 0,
7284
},
7385
{
7486
Name: "file4.txt",
@@ -81,6 +93,8 @@ func TestFileGetStatusFiles(t *testing.T) {
8193
HasInlineMergeConflicts: false,
8294
DisplayString: "?? file4.txt",
8395
ShortStatus: "??",
96+
LinesAdded: 0,
97+
LinesDeleted: 2,
8498
},
8599
{
86100
Name: "file5.txt",
@@ -93,15 +107,17 @@ func TestFileGetStatusFiles(t *testing.T) {
93107
HasInlineMergeConflicts: true,
94108
DisplayString: "UU file5.txt",
95109
ShortStatus: "UU",
110+
LinesAdded: 2,
111+
LinesDeleted: 2,
96112
},
97113
},
98114
},
99115
{
100-
"File with new line char",
101-
50,
102-
oscommands.NewFakeRunner(t).
116+
testName: "File with new line char",
117+
similarityThreshold: 50,
118+
runner: oscommands.NewFakeRunner(t).
103119
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"}, "MM a\nb.txt", nil),
104-
[]*models.File{
120+
expectedFiles: []*models.File{
105121
{
106122
Name: "a\nb.txt",
107123
HasStagedChanges: true,
@@ -117,14 +133,14 @@ func TestFileGetStatusFiles(t *testing.T) {
117133
},
118134
},
119135
{
120-
"Renamed files",
121-
50,
122-
oscommands.NewFakeRunner(t).
136+
testName: "Renamed files",
137+
similarityThreshold: 50,
138+
runner: oscommands.NewFakeRunner(t).
123139
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
124140
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
125141
nil,
126142
),
127-
[]*models.File{
143+
expectedFiles: []*models.File{
128144
{
129145
Name: "after1.txt",
130146
PreviousName: "before1.txt",
@@ -154,14 +170,14 @@ func TestFileGetStatusFiles(t *testing.T) {
154170
},
155171
},
156172
{
157-
"File with arrow in name",
158-
50,
159-
oscommands.NewFakeRunner(t).
173+
testName: "File with arrow in name",
174+
similarityThreshold: 50,
175+
runner: oscommands.NewFakeRunner(t).
160176
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
161177
`?? a -> b.txt`,
162178
nil,
163179
),
164-
[]*models.File{
180+
expectedFiles: []*models.File{
165181
{
166182
Name: "a -> b.txt",
167183
HasStagedChanges: false,
@@ -185,8 +201,14 @@ func TestFileGetStatusFiles(t *testing.T) {
185201
appState := &config.AppState{}
186202
appState.RenameSimilarityThreshold = s.similarityThreshold
187203

204+
userConfig := &config.UserConfig{
205+
Gui: config.GuiConfig{
206+
ShowNumstatInFilesView: s.showNumstatInFilesView,
207+
},
208+
}
209+
188210
loader := &FileLoader{
189-
GitCommon: buildGitCommon(commonDeps{appState: appState}),
211+
GitCommon: buildGitCommon(commonDeps{appState: appState, userConfig: userConfig}),
190212
cmd: cmd,
191213
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
192214
getFileType: func(string) string { return "file" },

pkg/commands/models/file.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type File struct {
1919
HasInlineMergeConflicts bool
2020
DisplayString string
2121
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
22+
LinesDeleted int
23+
LinesAdded int
2224

2325
// If true, this must be a worktree folder
2426
IsWorktree bool

pkg/config/user_config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ type GuiConfig struct {
109109
// If true, display the files in the file views as a tree. If false, display the files as a flat list.
110110
// This can be toggled from within Lazygit with the '~' key, but that will not change the default.
111111
ShowFileTree bool `yaml:"showFileTree"`
112+
// If true, show the number of lines changed per file in the Files view
113+
ShowNumstatInFilesView bool `yaml:"showNumstatInFilesView"`
112114
// If true, show a random tip in the command log when Lazygit starts
113115
ShowRandomTip bool `yaml:"showRandomTip"`
114116
// If true, show the command log
@@ -714,6 +716,7 @@ func GetDefaultConfig() *UserConfig {
714716
ShowBottomLine: true,
715717
ShowPanelJumps: true,
716718
ShowFileTree: true,
719+
ShowNumstatInFilesView: false,
717720
ShowRandomTip: true,
718721
ShowIcons: false,
719722
NerdFontsVersion: "",

pkg/gui/context/working_tree_context.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
3030

3131
getDisplayStrings := func(_ int, _ int) [][]string {
3232
showFileIcons := icons.IsIconEnabled() && c.UserConfig().Gui.ShowFileIcons
33-
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons)
33+
showNumstat := c.UserConfig().Gui.ShowNumstatInFilesView
34+
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumstat)
3435
return lo.Map(lines, func(line string, _ int) []string {
3536
return []string{line}
3637
})

pkg/gui/presentation/files.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ func RenderFileTree(
2222
tree filetree.IFileTree,
2323
submoduleConfigs []*models.SubmoduleConfig,
2424
showFileIcons bool,
25+
showNumstat bool,
2526
) []string {
2627
collapsedPaths := tree.CollapsedPaths()
2728
return renderAux(tree.GetRoot().Raw(), collapsedPaths, -1, -1, func(node *filetree.Node[models.File], treeDepth int, visualDepth int, isCollapsed bool) string {
2829
fileNode := filetree.NewFileNode(node)
2930

30-
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showFileIcons, submoduleConfigs, node)
31+
return getFileLine(isCollapsed, fileNode.GetHasUnstagedChanges(), fileNode.GetHasStagedChanges(), treeDepth, visualDepth, showNumstat, showFileIcons, submoduleConfigs, node)
3132
})
3233
}
3334

@@ -111,6 +112,7 @@ func getFileLine(
111112
hasStagedChanges bool,
112113
treeDepth int,
113114
visualDepth int,
115+
showNumstat,
114116
showFileIcons bool,
115117
submoduleConfigs []*models.SubmoduleConfig,
116118
node *filetree.Node[models.File],
@@ -165,6 +167,12 @@ func getFileLine(
165167
output += theme.DefaultTextColor.Sprint(" (submodule)")
166168
}
167169

170+
if file != nil && showNumstat {
171+
if lineChanges := formatLineChanges(file.LinesAdded, file.LinesDeleted); lineChanges != "" {
172+
output += " " + lineChanges
173+
}
174+
}
175+
168176
return output
169177
}
170178

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

197+
func formatLineChanges(linesAdded, linesDeleted int) string {
198+
output := ""
199+
200+
if linesAdded != 0 {
201+
output += style.FgGreen.Sprintf("+%d", linesAdded)
202+
}
203+
204+
if linesDeleted != 0 {
205+
if output != "" {
206+
output += " "
207+
}
208+
output += style.FgRed.Sprintf("-%d", linesDeleted)
209+
}
210+
211+
return output
212+
}
213+
189214
func getCommitFileLine(
190215
isCollapsed bool,
191216
treeDepth int,

0 commit comments

Comments
 (0)