summaryrefslogtreecommitdiffstats
path: root/pkg/gui/mergeconflicts
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-01-26 01:20:19 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-01-26 14:50:47 +1100
commitc8cc18920f0b5ab54a19b9f7bdcf83db3210576f (patch)
tree60623fbe5504469b9a4f4ca25a9a538b4eee52ce /pkg/gui/mergeconflicts
parentce3bcfe37cf0c68f501fb09d543e9e212b0eaa61 (diff)
improve merge conflict flow
Diffstat (limited to 'pkg/gui/mergeconflicts')
-rw-r--r--pkg/gui/mergeconflicts/find_conflicts.go48
-rw-r--r--pkg/gui/mergeconflicts/find_conflicts_test.go40
-rw-r--r--pkg/gui/mergeconflicts/rendering.go3
-rw-r--r--pkg/gui/mergeconflicts/state.go83
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
}