mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Make the conflict marker size a parameter of our marker matching
Git doesn't always write conflict markers of seven characters: the conflict-marker-size gitattribute overrides that per file, and it is set for good reasons — for file types whose regular content tends to contain marker-looking lines, such as documentation about merging, or test scripts. We hard-code seven characters everywhere we look for markers, so none of that works. Prepare for honoring the attribute by threading the marker size through everything that recognizes a marker, carried on the file model. Nothing fills it in yet, so we still use git's default size of seven everywhere, and matching is unchanged: a marker consists of exactly that many marker characters, and all but the "=======" one are followed by a space and a label.
This commit is contained in:
@@ -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,57 @@ 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".
|
||||
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 +130,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,62 @@ func TestDetermineLineType(t *testing.T) {
|
||||
line: "||||||| adf33b9",
|
||||
expected: ANCESTOR,
|
||||
},
|
||||
{
|
||||
line: "<<<<<<<<",
|
||||
expected: NOT_A_MARKER,
|
||||
},
|
||||
{
|
||||
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{
|
||||
@@ -92,12 +140,28 @@ func TestFindConflictsAux(t *testing.T) {
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user