diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-26 01:20:19 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-26 14:50:47 +1100 |
commit | c8cc18920f0b5ab54a19b9f7bdcf83db3210576f (patch) | |
tree | 60623fbe5504469b9a4f4ca25a9a538b4eee52ce /pkg/gui/mergeconflicts | |
parent | ce3bcfe37cf0c68f501fb09d543e9e212b0eaa61 (diff) |
improve merge conflict flow
Diffstat (limited to 'pkg/gui/mergeconflicts')
-rw-r--r-- | pkg/gui/mergeconflicts/find_conflicts.go | 48 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/find_conflicts_test.go | 40 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/rendering.go | 3 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/state.go | 83 |
4 files changed, 146 insertions, 28 deletions
diff --git a/pkg/gui/mergeconflicts/find_conflicts.go b/pkg/gui/mergeconflicts/find_conflicts.go index 80e487f85..14a08fd68 100644 --- a/pkg/gui/mergeconflicts/find_conflicts.go +++ b/pkg/gui/mergeconflicts/find_conflicts.go @@ -1,6 +1,10 @@ package mergeconflicts import ( + "bufio" + "bytes" + "io" + "os" "strings" "github.com/jesseduffield/lazygit/pkg/utils" @@ -53,19 +57,59 @@ func findConflicts(content string) []*mergeConflict { return conflicts } +var CONFLICT_START = "<<<<<<< " +var CONFLICT_END = ">>>>>>> " +var CONFLICT_START_BYTES = []byte(CONFLICT_START) +var CONFLICT_END_BYTES = []byte(CONFLICT_END) + func determineLineType(line string) LineType { + // TODO: find out whether we ever actually get this prefix trimmedLine := strings.TrimPrefix(line, "++") switch { - case strings.HasPrefix(trimmedLine, "<<<<<<< "): + case strings.HasPrefix(trimmedLine, CONFLICT_START): return START case strings.HasPrefix(trimmedLine, "||||||| "): return ANCESTOR case trimmedLine == "=======": return TARGET - case strings.HasPrefix(trimmedLine, ">>>>>>> "): + case strings.HasPrefix(trimmedLine, CONFLICT_END): return END default: return NOT_A_MARKER } } + +// 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) { + file, err := os.Open(path) + if err != nil { + return false, err + } + + defer file.Close() + + return fileHasConflictMarkersAux(file), nil +} + +// Efficiently scans through a file looking for merge conflict markers. Returns true if it does +func fileHasConflictMarkersAux(file io.Reader) bool { + scanner := bufio.NewScanner(file) + scanner.Split(bufio.ScanLines) + 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 + } + + if bytes.HasPrefix(line, CONFLICT_END_BYTES) { + return true + } + } + + return false +} diff --git a/pkg/gui/mergeconflicts/find_conflicts_test.go b/pkg/gui/mergeconflicts/find_conflicts_test.go index bcc70ce73..f4ab4d30c 100644 --- a/pkg/gui/mergeconflicts/find_conflicts_test.go +++ b/pkg/gui/mergeconflicts/find_conflicts_test.go @@ -1,6 +1,7 @@ package mergeconflicts import ( + "strings" "testing" "github.com/stretchr/testify/assert" @@ -59,3 +60,42 @@ func TestDetermineLineType(t *testing.T) { assert.EqualValues(t, s.expected, determineLineType(s.line)) } } + +func TestFindConflictsAux(t *testing.T) { + type scenario struct { + content string + expected bool + } + + scenarios := []scenario{ + { + content: "", + expected: false, + }, + { + content: "blah", + expected: false, + }, + { + content: ">>>>>>> ", + expected: true, + }, + { + content: "<<<<<<< ", + expected: true, + }, + { + content: " <<<<<<< ", + expected: false, + }, + { + content: "a\nb\nc\n<<<<<<< ", + expected: true, + }, + } + + for _, s := range scenarios { + reader := strings.NewReader(s.content) + assert.EqualValues(t, s.expected, fileHasConflictMarkersAux(reader)) + } +} diff --git a/pkg/gui/mergeconflicts/rendering.go b/pkg/gui/mergeconflicts/rendering.go index 5b44f2a14..54fc4e836 100644 --- a/pkg/gui/mergeconflicts/rendering.go +++ b/pkg/gui/mergeconflicts/rendering.go @@ -8,7 +8,8 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) -func ColoredConflictFile(content string, state *State, hasFocus bool) string { +func ColoredConflictFile(state *State, hasFocus bool) string { + content := state.GetContent() if len(state.conflicts) == 0 { return content } diff --git a/pkg/gui/mergeconflicts/state.go b/pkg/gui/mergeconflicts/state.go index f202cf772..0889aaf41 100644 --- a/pkg/gui/mergeconflicts/state.go +++ b/pkg/gui/mergeconflicts/state.go @@ -3,13 +3,19 @@ package mergeconflicts import ( "sync" - "github.com/golang-collections/collections/stack" "github.com/jesseduffield/lazygit/pkg/utils" ) type State struct { sync.Mutex + // path of the file with the conflicts + path string + + // This is a stack of the file content. It is used to undo changes. + // The last item is the current file content. + contents []string + conflicts []*mergeConflict // this is the index of the above `conflicts` field which is currently selected conflictIndex int @@ -17,9 +23,6 @@ type State struct { // this is the index of the selected conflict's available selections slice e.g. [TOP, MIDDLE, BOTTOM] // We use this to know which hunk of the conflict is selected. selectionIndex int - - // this allows us to undo actions - EditHistory *stack.Stack } func NewState() *State { @@ -28,7 +31,7 @@ func NewState() *State { conflictIndex: 0, selectionIndex: 0, conflicts: []*mergeConflict{}, - EditHistory: stack.New(), + contents: []string{}, } } @@ -63,28 +66,56 @@ func (s *State) SelectPrevConflict() { s.setConflictIndex(s.conflictIndex - 1) } -func (s *State) PushFileSnapshot(content string) { - s.EditHistory.Push(content) +func (s *State) currentConflict() *mergeConflict { + if len(s.conflicts) == 0 { + return nil + } + + return s.conflicts[s.conflictIndex] } -func (s *State) PopFileSnapshot() (string, bool) { - if s.EditHistory.Len() == 0 { - return "", false +// this is for starting a new merge conflict session +func (s *State) SetContent(content string, path string) { + if content == s.GetContent() && path == s.path { + return } - return s.EditHistory.Pop().(string), true + s.path = path + s.contents = []string{} + s.PushContent(content) } -func (s *State) currentConflict() *mergeConflict { - if len(s.conflicts) == 0 { - return nil +// this is for when you've resolved a conflict. This allows you to undo to a previous +// state +func (s *State) PushContent(content string) { + s.contents = append(s.contents, content) + s.setConflicts(findConflicts(content)) +} + +func (s *State) GetContent() string { + if len(s.contents) == 0 { + return "" } - return s.conflicts[s.conflictIndex] + return s.contents[len(s.contents)-1] } -func (s *State) SetConflictsFromCat(cat string) { - s.setConflicts(findConflicts(cat)) +func (s *State) GetPath() string { + return s.path +} + +func (s *State) Undo() bool { + if len(s.contents) <= 1 { + return false + } + + s.contents = s.contents[:len(s.contents)-1] + + newContent := s.GetContent() + // We could be storing the old conflicts and selected index on a stack too. + s.setConflicts(findConflicts(newContent)) + + return true } func (s *State) setConflicts(conflicts []*mergeConflict) { @@ -110,29 +141,31 @@ func (s *State) availableSelections() []Selection { return nil } -func (s *State) IsFinalConflict() bool { - return len(s.conflicts) == 1 +func (s *State) AllConflictsResolved() bool { + return len(s.conflicts) == 0 } func (s *State) Reset() { - s.EditHistory = stack.New() + s.contents = []string{} + s.path = "" +} + +func (s *State) Active() bool { + return s.path != "" } func (s *State) GetConflictMiddle() int { return s.currentConflict().target } -func (s *State) ContentAfterConflictResolve( - path string, - selection Selection, -) (bool, string, error) { +func (s *State) ContentAfterConflictResolve(selection Selection) (bool, string, error) { conflict := s.currentConflict() if conflict == nil { return false, "", nil } content := "" - err := utils.ForEachLineInFile(path, func(line string, i int) { + err := utils.ForEachLineInFile(s.path, func(line string, i int) { if selection.isIndexToKeep(conflict, i) { content += line } |