Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show the number of lines changed per file in working file tree view #4015

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
showNumberOfLineChanges: true

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

Expand Down
4 changes: 4 additions & 0 deletions pkg/commands/git_commands/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ func (self *ConfigCommands) GetCoreCommentChar() byte {
func (self *ConfigCommands) GetRebaseUpdateRefs() bool {
return self.gitConfig.GetBool("rebase.updateRefs")
}

func (cfgCommands *ConfigCommands) GetShowNumberOfLineChanges() bool {
johannaschwarz marked this conversation as resolved.
Show resolved Hide resolved
return cfgCommands.UserConfig().Gui.ShowNumberOfLineChanges
}
67 changes: 66 additions & 1 deletion 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 All @@ -11,6 +12,7 @@ import (

type FileLoaderConfig interface {
GetShowUntrackedFiles() string
GetShowNumberOfLineChanges() bool
}

type FileLoader struct {
Expand All @@ -30,7 +32,8 @@ func NewFileLoader(gitCommon *GitCommon, cmd oscommands.ICmdObjBuilder, config F
}

type GetStatusFileOptions struct {
NoRenames bool
NoRenames bool
GetFileDiffs bool
}

func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
Expand All @@ -48,6 +51,14 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
}
files := []*models.File{}

fileDiffs := map[string]FileDiff{}
if self.config.GetShowNumberOfLineChanges() {
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 +71,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 +103,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 +155,16 @@ type FileStatus struct {
PreviousName string
}

func (fileLoader *FileLoader) gitDiffNumStat() (string, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three things:

  1. Just confirming that this can't be bundled into the git status command?
  2. How expensive is this command to run? If it's expensive, we may need to run it asynchronously
  3. I've noticed that this doesn't include untracked files. Is there a way to include those?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I did research on this and could not find any way to use git status for this.

  2. I tested this for 18 files changed with about 6000 insertions and 3000 deletions:

    • on my M1 Pro the git diff --numstat command takes 62 milliseconds.
    • in comparison: the git status command takes 46 milliseconds.

    What do you think performance-wise? Is it fast enough to run synchronously?

  3. According to my research it is not possible to include untracked files in a git diff command. I don't think it is super important to include these, but it would probably be possible to determine the number of added lines another way for untracked files.

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
73 changes: 47 additions & 26 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
showNumberOfLineChanges 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{
showNumberOfLineChanges: 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 @@ -188,7 +204,7 @@ func TestFileGetStatusFiles(t *testing.T) {
loader := &FileLoader{
GitCommon: buildGitCommon(commonDeps{appState: appState}),
cmd: cmd,
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes"},
config: &FakeFileLoaderConfig{showUntrackedFiles: "yes", showNumberOfLineChanges: s.showNumberOfLineChanges},
getFileType: func(string) string { return "file" },
}

Expand All @@ -198,9 +214,14 @@ func TestFileGetStatusFiles(t *testing.T) {
}

type FakeFileLoaderConfig struct {
showUntrackedFiles string
showUntrackedFiles string
showNumberOfLineChanges bool
}

func (self *FakeFileLoaderConfig) GetShowUntrackedFiles() string {
return self.showUntrackedFiles
}

func (self *FakeFileLoaderConfig) GetShowNumberOfLineChanges() bool {
return self.showNumberOfLineChanges
}
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
ShowNumberOfLineChanges bool `yaml:"showNumberOfLineChanges"`
// 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 @@ -712,6 +714,7 @@ func GetDefaultConfig() *UserConfig {
ShowBottomLine: true,
ShowPanelJumps: true,
ShowFileTree: true,
ShowNumberOfLineChanges: true,
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)
showNumberOfLineChanges := c.UserConfig().Gui.ShowNumberOfLineChanges
lines := presentation.RenderFileTree(viewModel, c.Model().Submodules, showFileIcons, showNumberOfLineChanges)
return lo.Map(lines, func(line string, _ int) []string {
return []string{line}
})
Expand Down
Loading
Loading