mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 10:13:41 -05:00
Honor the conflict-marker-size gitattribute (#5902)
If the `conflict-marker-size` git attribute is used to set the marker size to a non-default value (!= 7), lazygit's handling of conflicted files was totally broken. Stopping at a commit with conflicts in a rebase would show the `UU` files for a moment, and then, a few seconds later, would stage all conflicted files and offer to continue the rebase (with the conflicts baked into the resulting commits if you confirmed). Even if you cancelled the continue prompt, it wasn't possible to use git's conflict panel to resolve the conflicts; it would only show the regular diff for those files, not its conflicts editor. Fix this by querying the `conflict-marker-size` git attribute for all conflicting files and use that to match the conflict markers. Fixes #4367.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
})
|
||||
@@ -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"))
|
||||
},
|
||||
})
|
||||
@@ -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.
|
||||
|
||||
@@ -165,6 +165,8 @@ var tests = []*components.IntegrationTest{
|
||||
config.NegativeRefspec,
|
||||
config.RemoteNamedStar,
|
||||
config.SidePanelsInPerRepoConfig,
|
||||
conflicts.ConflictMarkerSizeNotAutoStaged,
|
||||
conflicts.ConflictMarkerSizeResolve,
|
||||
conflicts.ContinuePromptDismissedWhenResolvedExternally,
|
||||
conflicts.Filter,
|
||||
conflicts.MergeFileBoth,
|
||||
|
||||
Reference in New Issue
Block a user