diff --git a/pkg/commands/git_commands/file_loader.go b/pkg/commands/git_commands/file_loader.go index 7e2bdf0f309..3f960cb3071 100644 --- a/pkg/commands/git_commands/file_loader.go +++ b/pkg/commands/git_commands/file_loader.go @@ -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 { @@ -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()) @@ -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 diff --git a/pkg/commands/git_commands/file_loader_test.go b/pkg/commands/git_commands/file_loader_test.go index ec1f502f1a8..23602ff9ea6 100644 --- a/pkg/commands/git_commands/file_loader_test.go +++ b/pkg/commands/git_commands/file_loader_test.go @@ -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{ @@ -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, diff --git a/pkg/commands/models/file.go b/pkg/commands/models/file.go index e48696a4f0c..9eedfb1fc49 100644 --- a/pkg/commands/models/file.go +++ b/pkg/commands/models/file.go @@ -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 diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 567b0b6e5b6..d9ef55a715a 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -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 } @@ -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 { diff --git a/pkg/gui/controllers/helpers/merge_conflicts_helper.go b/pkg/gui/controllers/helpers/merge_conflicts_helper.go index 34ae285f094..3928ecebfcb 100644 --- a/pkg/gui/controllers/helpers/merge_conflicts_helper.go +++ b/pkg/gui/controllers/helpers/merge_conflicts_helper.go @@ -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" ) @@ -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 @@ -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 } @@ -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 } @@ -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 } diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index d0327216c8f..53505ba6c8c 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -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 { diff --git a/pkg/gui/filetree/file_node.go b/pkg/gui/filetree/file_node.go index 0836eaf02d9..04c98f1fafc 100644 --- a/pkg/gui/filetree/file_node.go +++ b/pkg/gui/filetree/file_node.go @@ -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 }) } diff --git a/pkg/gui/mergeconflicts/find_conflicts.go b/pkg/gui/mergeconflicts/find_conflicts.go index 5fe45624e7a..e57c166353a 100644 --- a/pkg/gui/mergeconflicts/find_conflicts.go +++ b/pkg/gui/mergeconflicts/find_conflicts.go @@ -2,7 +2,6 @@ package mergeconflicts import ( "bufio" - "bytes" "io" "os" "strings" @@ -22,7 +21,23 @@ const ( NOT_A_MARKER ) -func findConflicts(content string) []*mergeConflict { +// The number of characters a conflict marker consists of, unless the file's +// conflict-marker-size gitattribute says otherwise. +const defaultConflictMarkerSize = 7 + +// The marker size that everything in here takes is the conflict-marker-size +// gitattribute of the file being examined, which is 0 for a file that doesn't +// have that attribute. Git falls back to its default size in that case, so we +// do the same. +func effectiveMarkerSize(markerSize int) int { + if markerSize < 1 { + return defaultConflictMarkerSize + } + + return markerSize +} + +func findConflicts(content string, markerSize int) []*mergeConflict { conflicts := make([]*mergeConflict, 0) if content == "" { @@ -31,7 +46,7 @@ func findConflicts(content string) []*mergeConflict { var newConflict *mergeConflict for i, line := range utils.SplitLines(content) { - switch determineLineType(line) { + switch determineLineType(line, markerSize) { case START: newConflict = &mergeConflict{start: i, ancestor: -1} case ANCESTOR: @@ -57,35 +72,59 @@ func findConflicts(content string) []*mergeConflict { return conflicts } -var ( - CONFLICT_START = "<<<<<<< " - CONFLICT_END = ">>>>>>> " - CONFLICT_START_BYTES = []byte(CONFLICT_START) - CONFLICT_END_BYTES = []byte(CONFLICT_END) -) +func determineLineType(line string, markerSize int) LineType { + markerSize = effectiveMarkerSize(markerSize) -func determineLineType(line string) LineType { // TODO: find out whether we ever actually get this prefix trimmedLine := strings.TrimPrefix(line, "++") switch { - case strings.HasPrefix(trimmedLine, CONFLICT_START): + case isConflictMarker(trimmedLine, '<', markerSize): return START - case strings.HasPrefix(trimmedLine, "||||||| "): + case isConflictMarker(trimmedLine, '|', markerSize): return ANCESTOR - case trimmedLine == "=======": + case isTargetMarker(trimmedLine, markerSize): return TARGET - case strings.HasPrefix(trimmedLine, CONFLICT_END): + case isConflictMarker(trimmedLine, '>', markerSize): return END default: return NOT_A_MARKER } } +// Tells us whether the line begins with markerSize repetitions of markerChar. +func hasMarkerPrefix[T string | []byte](line T, markerChar byte, markerSize int) bool { + if len(line) < markerSize { + return false + } + + for i := range markerSize { + if line[i] != markerChar { + return false + } + } + + return true +} + +// A start, ancestor or end marker is followed by a space and a label, e.g. +// "<<<<<<< HEAD". The label can be missing though, in which case git doesn't +// write the space either; `git checkout -m` with the diff3 conflict style does +// that for the ancestor marker, for example. +func isConflictMarker[T string | []byte](line T, markerChar byte, markerSize int) bool { + return hasMarkerPrefix(line, markerChar, markerSize) && + (len(line) == markerSize || line[markerSize] == ' ') +} + +// The marker separating the two sides of a conflict never has a label after it. +func isTargetMarker(line string, markerSize int) bool { + return hasMarkerPrefix(line, '=', markerSize) && len(line) == markerSize +} + // tells us whether a file actually has inline merge conflicts. We need to run this // because git will continue showing a status of 'UU' even after the conflicts have // been resolved in the user's editor -func FileHasConflictMarkers(path string) (bool, error) { +func FileHasConflictMarkers(path string, markerSize int) (bool, error) { file, err := os.Open(path) if err != nil { return false, err @@ -93,22 +132,20 @@ func FileHasConflictMarkers(path string) (bool, error) { defer file.Close() - return fileHasConflictMarkersAux(file) + return fileHasConflictMarkersAux(file, markerSize) } // Efficiently scans through a file looking for merge conflict markers. Returns true if it does -func fileHasConflictMarkersAux(file io.Reader) (bool, error) { +func fileHasConflictMarkersAux(file io.Reader, markerSize int) (bool, error) { + markerSize = effectiveMarkerSize(markerSize) + scanner := bufio.NewScanner(file) scanner.Split(utils.ScanLinesAndTruncateWhenLongerThanBuffer(bufio.MaxScanTokenSize)) for scanner.Scan() { line := scanner.Bytes() // only searching for start/end markers because the others are more ambiguous - if bytes.HasPrefix(line, CONFLICT_START_BYTES) { - return true, nil - } - - if bytes.HasPrefix(line, CONFLICT_END_BYTES) { + if isConflictMarker(line, '<', markerSize) || isConflictMarker(line, '>', markerSize) { return true, nil } } diff --git a/pkg/gui/mergeconflicts/find_conflicts_test.go b/pkg/gui/mergeconflicts/find_conflicts_test.go index c763aa51f4f..28839126fb0 100644 --- a/pkg/gui/mergeconflicts/find_conflicts_test.go +++ b/pkg/gui/mergeconflicts/find_conflicts_test.go @@ -8,9 +8,12 @@ import ( ) func TestDetermineLineType(t *testing.T) { + // A markerSize of 0 means the file has no conflict-marker-size gitattribute, + // so git's default size applies. type scenario struct { - line string - expected LineType + line string + markerSize int + expected LineType } scenarios := []scenario{ @@ -54,17 +57,75 @@ func TestDetermineLineType(t *testing.T) { line: "||||||| adf33b9", expected: ANCESTOR, }, + { + line: "<<<<<<<<", + expected: NOT_A_MARKER, + }, + // Markers without a label + { + line: "<<<<<<<", + expected: START, + }, + { + line: "|||||||", + expected: ANCESTOR, + }, + { + line: ">>>>>>>", + expected: END, + }, + { + line: strings.Repeat("<", 32) + " HEAD", + markerSize: 32, + expected: START, + }, + { + line: strings.Repeat("|", 32) + " adf33b9", + markerSize: 32, + expected: ANCESTOR, + }, + { + line: strings.Repeat("=", 32), + markerSize: 32, + expected: TARGET, + }, + { + line: strings.Repeat(">", 32) + " blah", + markerSize: 32, + expected: END, + }, + // A file gets a bigger marker size precisely because its regular content + // tends to contain marker-looking lines, so lines with the default size + // must not be mistaken for markers + { + line: "<<<<<<< HEAD", + markerSize: 32, + expected: NOT_A_MARKER, + }, + { + line: "=======", + markerSize: 32, + expected: NOT_A_MARKER, + }, + { + line: strings.Repeat("=", 33), + markerSize: 32, + expected: NOT_A_MARKER, + }, } for _, s := range scenarios { - assert.EqualValues(t, s.expected, determineLineType(s.line)) + assert.EqualValues(t, s.expected, determineLineType(s.line, s.markerSize), s.line) } } func TestFindConflictsAux(t *testing.T) { + // A markerSize of 0 means the file has no conflict-marker-size gitattribute, + // so git's default size applies. type scenario struct { - content string - expected bool + content string + markerSize int + expected bool } scenarios := []scenario{ @@ -88,16 +149,36 @@ func TestFindConflictsAux(t *testing.T) { content: " <<<<<<< ", expected: false, }, + { + content: ">>>>>>>", + expected: true, + }, { content: "a\nb\nc\n<<<<<<< ", expected: true, }, + { + content: "a\nb\nc\n" + strings.Repeat("<", 32) + " HEAD", + markerSize: 32, + expected: true, + }, + { + content: "a\nb\nc\n" + strings.Repeat(">", 32) + " blah", + markerSize: 32, + expected: true, + }, + // Marker-looking lines of the default size are the file's regular content + { + content: "a\nb\nc\n<<<<<<< HEAD\n=======\n>>>>>>> blah", + markerSize: 32, + expected: false, + }, } for _, s := range scenarios { reader := strings.NewReader(s.content) - result, err := fileHasConflictMarkersAux(reader) + result, err := fileHasConflictMarkersAux(reader, s.markerSize) assert.NoError(t, err) - assert.EqualValues(t, s.expected, result) + assert.EqualValues(t, s.expected, result, s.content) } } diff --git a/pkg/gui/mergeconflicts/state.go b/pkg/gui/mergeconflicts/state.go index 047241353e4..d38e0c75400 100644 --- a/pkg/gui/mergeconflicts/state.go +++ b/pkg/gui/mergeconflicts/state.go @@ -12,6 +12,9 @@ type State struct { // path of the file with the conflicts path string + // the file's conflict-marker-size gitattribute, or 0 if it doesn't have one + markerSize int + // This is a stack of the file content. It is used to undo changes. // The last item is the current file content. contents []string @@ -74,12 +77,13 @@ func (s *State) currentConflict() *mergeConflict { } // this is for starting a new merge conflict session -func (s *State) SetContent(content string, path string) { - if content == s.GetContent() && path == s.path { +func (s *State) SetContent(content string, path string, markerSize int) { + if content == s.GetContent() && path == s.path && markerSize == s.markerSize { return } s.path = path + s.markerSize = markerSize s.contents = []string{} s.PushContent(content) } @@ -88,7 +92,7 @@ func (s *State) SetContent(content string, path string) { // state func (s *State) PushContent(content string) { s.contents = append(s.contents, content) - s.setConflicts(findConflicts(content)) + s.setConflicts(findConflicts(content, s.markerSize)) } func (s *State) GetContent() string { @@ -103,6 +107,10 @@ func (s *State) GetPath() string { return s.path } +func (s *State) GetMarkerSize() int { + return s.markerSize +} + func (s *State) Undo() bool { if len(s.contents) <= 1 { return false @@ -112,7 +120,7 @@ func (s *State) Undo() bool { newContent := s.GetContent() // We could be storing the old conflicts and selected index on a stack too. - s.setConflicts(findConflicts(newContent)) + s.setConflicts(findConflicts(newContent, s.markerSize)) return true } @@ -147,6 +155,7 @@ func (s *State) AllConflictsResolved() bool { func (s *State) Reset() { s.contents = []string{} s.path = "" + s.markerSize = 0 } // we're not resetting selectedIndex here because the user typically would want diff --git a/pkg/gui/mergeconflicts/state_test.go b/pkg/gui/mergeconflicts/state_test.go index 7a9ee8c2691..06f8fa6bb12 100644 --- a/pkg/gui/mergeconflicts/state_test.go +++ b/pkg/gui/mergeconflicts/state_test.go @@ -116,7 +116,7 @@ baz for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - assert.EqualValues(t, s.expected, findConflicts(s.content)) + assert.EqualValues(t, s.expected, findConflicts(s.content, defaultConflictMarkerSize)) }) } } diff --git a/pkg/integration/tests/conflicts/conflict_marker_size_not_auto_staged.go b/pkg/integration/tests/conflicts/conflict_marker_size_not_auto_staged.go new file mode 100644 index 00000000000..52931c19275 --- /dev/null +++ b/pkg/integration/tests/conflicts/conflict_marker_size_not_auto_staged.go @@ -0,0 +1,43 @@ +package conflicts + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var ConflictMarkerSizeNotAutoStaged = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Doesn't auto-stage an unresolved file whose conflict-marker-size gitattribute makes its markers longer than usual", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shared.SetCustomConflictMarkerSize(shell) + shared.CreateMergeConflictFile(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Common().PretendMergeOrRebaseStartedInLazygit() + + t.Views().Files(). + IsFocused(). + Lines( + Contains("UU file").IsSelected(), + ). + // Each refresh checks whether the conflicts are still there + Press(keys.Universal.Refresh). + // They are, so the file doesn't get staged and we don't get asked to + // continue the merge + Lines( + Contains("UU file").IsSelected(), + ). + // Once they really are resolved, we do + Tap(func() { + t.Shell().UpdateFile("file", "resolved content") + }). + Press(keys.Universal.Refresh). + Tap(func() { + t.Common().ContinueOnConflictsResolved("merge") + }). + IsEmpty() + }, +}) diff --git a/pkg/integration/tests/conflicts/conflict_marker_size_resolve.go b/pkg/integration/tests/conflicts/conflict_marker_size_resolve.go new file mode 100644 index 00000000000..13a64793162 --- /dev/null +++ b/pkg/integration/tests/conflicts/conflict_marker_size_resolve.go @@ -0,0 +1,40 @@ +package conflicts + +import ( + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" + "github.com/jesseduffield/lazygit/pkg/integration/tests/shared" +) + +var ConflictMarkerSizeResolve = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Resolves a conflict in a file whose conflict-marker-size gitattribute makes its markers longer than usual", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shared.SetCustomConflictMarkerSize(shell) + shared.CreateMergeConflictFileMultiple(shell) + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + startMarker := strings.Repeat("<", shared.CustomConflictMarkerSize) + + t.Views().Files(). + IsFocused(). + Lines( + Contains("UU file").IsSelected(), + ). + PressEnter() + + t.Views().MergeConflicts(). + IsFocused(). + SelectedLines( + Contains(startMarker+" HEAD"), + Contains("First Change"), + Contains(strings.Repeat("=", shared.CustomConflictMarkerSize)), + ). + PressPrimaryAction(). + Content(DoesNotContain(startMarker + " HEAD\nFirst Change")) + }, +}) diff --git a/pkg/integration/tests/shared/conflicts.go b/pkg/integration/tests/shared/conflicts.go index b84c8c7add0..c8319acf485 100644 --- a/pkg/integration/tests/shared/conflicts.go +++ b/pkg/integration/tests/shared/conflicts.go @@ -1,6 +1,8 @@ package shared import ( + "fmt" + . "github.com/jesseduffield/lazygit/pkg/integration/components" ) @@ -28,6 +30,20 @@ Second Change File ` +// A conflict-marker-size that isn't git's default of 7. It's set for file types +// whose regular content tends to contain marker-looking lines, e.g. +// documentation about merging, or test scripts. +const CustomConflictMarkerSize = 32 + +// Makes git write conflict markers of CustomConflictMarkerSize characters into +// the file that the setups below create conflicts in. Call this before one of +// them. +var SetCustomConflictMarkerSize = func(shell *Shell) { + shell.CreateFileAndAdd(".gitattributes", + fmt.Sprintf("file conflict-marker-size=%d\n", CustomConflictMarkerSize)). + Commit("set a custom conflict marker size") +} + // prepares us for a rebase/merge that has conflicts var MergeConflictsSetup = func(shell *Shell) { shell. diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a6b5aeafdf0..e8511fcf8d3 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -165,6 +165,8 @@ var tests = []*components.IntegrationTest{ config.NegativeRefspec, config.RemoteNamedStar, config.SidePanelsInPerRepoConfig, + conflicts.ConflictMarkerSizeNotAutoStaged, + conflicts.ConflictMarkerSizeResolve, conflicts.ContinuePromptDismissedWhenResolvedExternally, conflicts.Filter, conflicts.MergeFileBoth,