Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions pkg/commands/git_commands/file_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/samber/lo"
)

type FileLoaderConfig interface {
Expand Down Expand Up @@ -88,6 +89,8 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
files = append(files, file)
}

self.setConflictMarkerSizes(files)

// Go through the files to see if any of these files are actually worktrees
// so that we can render them correctly
worktreePaths := linkedWortkreePaths(self.Fs, self.repoPaths.RepoGitDirPath())
Expand All @@ -111,6 +114,63 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
return files
}

// Looks up how long the conflict markers in the conflicted files are. We ask
// git for all of them at once, because spawning a process per file would be
// painfully slow when hundreds of files are conflicted (especially on Windows).
func (self *FileLoader) setConflictMarkerSizes(files []*models.File) {
conflictedFiles := lo.Filter(files, func(file *models.File, _ int) bool {
return file.HasInlineMergeConflicts
})
if len(conflictedFiles) == 0 {
return
}

paths := lo.Map(conflictedFiles, func(file *models.File, _ int) string {
return file.Path
})

markerSizes, err := self.getConflictMarkerSizes(paths)
if err != nil {
self.Log.Error(err)
return
}

for _, file := range conflictedFiles {
file.ConflictMarkerSize = markerSizes[file.Path]
}
}

func (self *FileLoader) getConflictMarkerSizes(paths []string) (map[string]int, error) {
cmdArgs := NewGitCmd("check-attr").
Arg("-z").
Arg("--stdin").
Arg("conflict-marker-size").
ToArgv()

// -z makes git both read the paths and write its output NUL-separated, so
// that paths containing newlines don't throw us off.
output, _, err := self.cmd.New(cmdArgs).
SetStdin(strings.Join(paths, "\x00")).
DontLog().
RunWithOutputs()
if err != nil {
return nil, err
}

markerSizes := map[string]int{}
fields := strings.Split(output, "\x00")
// Each path yields a path/attribute/value triple; the value is either a
// number or something like "unspecified", in which case we leave the marker
// size at 0 to say that git's default applies.
for i := 0; i+2 < len(fields); i += 3 {
if markerSize, err := strconv.Atoi(fields[i+2]); err == nil && markerSize > 0 {
markerSizes[fields[i]] = markerSize
}
}

return markerSizes, nil
}

type FileDiff struct {
LinesAdded int
LinesDeleted int
Expand Down
56 changes: 56 additions & 0 deletions pkg/commands/git_commands/file_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func TestFileGetStatusFiles(t *testing.T) {
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,
).
ExpectGitArgs([]string{"check-attr", "-z", "--stdin", "conflict-marker-size"},
"file5.txt\x00conflict-marker-size\x00unspecified\x00",
nil,
),
showNumstatInFilesView: true,
expectedFiles: []*models.File{
Expand Down Expand Up @@ -112,6 +116,58 @@ func TestFileGetStatusFiles(t *testing.T) {
},
},
},
{
testName: "Conflicted files with a conflict-marker-size attribute",
similarityThreshold: 50,
runner: oscommands.NewFakeRunner(t).
ExpectGitArgs([]string{"status", "--untracked-files=yes", "--porcelain", "-z", "--find-renames=50%"},
"UU file1.txt\x00UU file2.txt\x00UU file3.txt\x00 M file4.txt",
nil,
).
ExpectGitArgs([]string{"check-attr", "-z", "--stdin", "conflict-marker-size"},
"file1.txt\x00conflict-marker-size\x0032\x00"+
"file2.txt\x00conflict-marker-size\x00unspecified\x00"+
"file3.txt\x00conflict-marker-size\x00nonsense\x00",
nil,
),
expectedFiles: []*models.File{
{
Path: "file1.txt",
HasUnstagedChanges: true,
Tracked: true,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
ConflictMarkerSize: 32,
DisplayString: "UU file1.txt",
ShortStatus: "UU",
},
{
Path: "file2.txt",
HasUnstagedChanges: true,
Tracked: true,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file2.txt",
ShortStatus: "UU",
},
{
Path: "file3.txt",
HasUnstagedChanges: true,
Tracked: true,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file3.txt",
ShortStatus: "UU",
},
{
Path: "file4.txt",
HasUnstagedChanges: true,
Tracked: true,
DisplayString: " M file4.txt",
ShortStatus: " M",
},
},
},
{
testName: "File with new line char",
similarityThreshold: 50,
Expand Down
12 changes: 8 additions & 4 deletions pkg/commands/models/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ type File struct {
Deleted bool
HasMergeConflicts bool
HasInlineMergeConflicts bool
DisplayString string
ShortStatus string // e.g. 'AD', ' A', 'M ', '??'
LinesDeleted int
LinesAdded int
// How long the conflict markers in this file are, taken from its
// conflict-marker-size gitattribute; 0 if it doesn't have that attribute. We
// only look this up for files that have inline merge conflicts.
ConflictMarkerSize int
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
4 changes: 2 additions & 2 deletions pkg/gui/controllers/files_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ func (self *FilesController) renderSubmoduleConflict(node *filetree.FileNode) {
// (it was resolved in an editor), in which case the caller should fall back to
// showing the file's diff.
func (self *FilesController) renderInlineMergeConflict(node *filetree.FileNode) bool {
hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.GetPath())
hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.File)
if err != nil {
return true
}
Expand Down Expand Up @@ -1264,7 +1264,7 @@ func (self *FilesController) switchToMerge() error {
return nil
}

return self.c.Helpers().MergeConflicts.SwitchToMerge(file.Path)
return self.c.Helpers().MergeConflicts.SwitchToMerge(file)
}

func (self *FilesController) createStashMenu() error {
Expand Down
18 changes: 10 additions & 8 deletions pkg/gui/controllers/helpers/merge_conflicts_helper.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package helpers

import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
Expand All @@ -17,14 +18,14 @@ func NewMergeConflictsHelper(
}
}

func (self *MergeConflictsHelper) SetMergeState(path string) (bool, error) {
func (self *MergeConflictsHelper) SetMergeState(file *models.File) (bool, error) {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()

return self.setMergeStateWithoutLock(path)
return self.setMergeStateWithoutLock(file.Path, file.ConflictMarkerSize)
}

func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string) (bool, error) {
func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string, markerSize int) (bool, error) {
content, err := self.c.Git().File.Cat(path)
if err != nil {
return false, err
Expand All @@ -34,7 +35,7 @@ func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string) (bool, e
self.context().SetUserScrolling(false)
}

self.context().GetState().SetContent(content, path)
self.context().GetState().SetContent(content, path, markerSize)

return !self.context().GetState().NoConflicts(), nil
}
Expand Down Expand Up @@ -72,7 +73,8 @@ func (self *MergeConflictsHelper) SetConflictsAndRender() (bool, error) {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()

hasConflicts, err := self.setMergeStateWithoutLock(self.context().GetState().GetPath())
state := self.context().GetState()
hasConflicts, err := self.setMergeStateWithoutLock(state.GetPath(), state.GetMarkerSize())
if err != nil {
return false, err
}
Expand All @@ -84,9 +86,9 @@ func (self *MergeConflictsHelper) SetConflictsAndRender() (bool, error) {
return false, nil
}

func (self *MergeConflictsHelper) SwitchToMerge(path string) error {
if self.context().GetState().GetPath() != path {
hasConflicts, err := self.SetMergeState(path)
func (self *MergeConflictsHelper) SwitchToMerge(file *models.File) error {
if self.context().GetState().GetPath() != file.Path {
hasConflicts, err := self.SetMergeState(file)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/gui/controllers/helpers/refresh_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,8 @@ func (self *RefreshHelper) refreshStateFiles(captured capturedFilesState, env re
// process working directory, which may already point at another
// repo if the user switched while this refresh was in flight.
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(
filepath.Join(env.git.RepoPaths.WorktreePath(), file.Path))
filepath.Join(env.git.RepoPaths.WorktreePath(), file.Path),
file.ConflictMarkerSize)
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {
Expand Down
2 changes: 1 addition & 1 deletion pkg/gui/filetree/file_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (self *FileNode) GetHasInlineMergeConflicts() bool {
if !file.HasInlineMergeConflicts {
return false
}
hasConflicts, _ := mergeconflicts.FileHasConflictMarkers(file.Path)
hasConflicts, _ := mergeconflicts.FileHasConflictMarkers(file.Path, file.ConflictMarkerSize)
return hasConflicts
})
}
Expand Down
Loading
Loading