Skip to content

Commit

Permalink
Add user config gui.showNumstatInFilesView
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
johannaschwarz authored and stefanhaller committed Dec 8, 2024
1 parent f3a5c18 commit f455f99
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 f455f99

Please sign in to comment.