mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
improve merge conflict flow
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,18 +66,6 @@ func (s *State) SelectPrevConflict() {
|
||||
s.setConflictIndex(s.conflictIndex - 1)
|
||||
}
|
||||
|
||||
func (s *State) PushFileSnapshot(content string) {
|
||||
s.EditHistory.Push(content)
|
||||
}
|
||||
|
||||
func (s *State) PopFileSnapshot() (string, bool) {
|
||||
if s.EditHistory.Len() == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return s.EditHistory.Pop().(string), true
|
||||
}
|
||||
|
||||
func (s *State) currentConflict() *mergeConflict {
|
||||
if len(s.conflicts) == 0 {
|
||||
return nil
|
||||
@ -83,8 +74,48 @@ func (s *State) currentConflict() *mergeConflict {
|
||||
return s.conflicts[s.conflictIndex]
|
||||
}
|
||||
|
||||
func (s *State) SetConflictsFromCat(cat string) {
|
||||
s.setConflicts(findConflicts(cat))
|
||||
// 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
|
||||
}
|
||||
|
||||
s.path = path
|
||||
s.contents = []string{}
|
||||
s.PushContent(content)
|
||||
}
|
||||
|
||||
// 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.contents[len(s.contents)-1]
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Reference in New Issue
Block a user