mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-31 14:24:25 +03:00
refactor merge panel
This commit is contained in:
@ -1,9 +0,0 @@
|
|||||||
package commands
|
|
||||||
|
|
||||||
// Conflict : A git conflict with a start middle and end corresponding to line
|
|
||||||
// numbers in the file where the conflict bars appear
|
|
||||||
type Conflict struct {
|
|
||||||
Start int
|
|
||||||
Middle int
|
|
||||||
End int
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/golang-collections/collections/stack"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
@ -21,6 +20,7 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
"github.com/jesseduffield/lazygit/pkg/gui/modes/filtering"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||||
@ -138,12 +138,8 @@ type LblPanelState struct {
|
|||||||
SecondaryFocused bool // this is for if we show the left or right panel
|
SecondaryFocused bool // this is for if we show the left or right panel
|
||||||
}
|
}
|
||||||
|
|
||||||
type mergingPanelState struct {
|
type MergingPanelState struct {
|
||||||
ConflictIndex int
|
*mergeconflicts.State
|
||||||
ConflictTop bool
|
|
||||||
Conflicts []commands.Conflict
|
|
||||||
ConflictsMutex sync.Mutex
|
|
||||||
EditHistory *stack.Stack
|
|
||||||
|
|
||||||
// UserScrolling tells us if the user has started scrolling through the file themselves
|
// UserScrolling tells us if the user has started scrolling through the file themselves
|
||||||
// in which case we won't auto-scroll to a conflict.
|
// in which case we won't auto-scroll to a conflict.
|
||||||
@ -226,7 +222,7 @@ type panelStates struct {
|
|||||||
Stash *stashPanelState
|
Stash *stashPanelState
|
||||||
Menu *menuPanelState
|
Menu *menuPanelState
|
||||||
LineByLine *LblPanelState
|
LineByLine *LblPanelState
|
||||||
Merging *mergingPanelState
|
Merging *MergingPanelState
|
||||||
CommitFiles *commitFilesPanelState
|
CommitFiles *commitFilesPanelState
|
||||||
Submodules *submodulePanelState
|
Submodules *submodulePanelState
|
||||||
Suggestions *suggestionsPanelState
|
Suggestions *suggestionsPanelState
|
||||||
@ -417,12 +413,9 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
|
|||||||
Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}},
|
Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}},
|
||||||
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil},
|
||||||
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}},
|
||||||
Merging: &mergingPanelState{
|
Merging: &MergingPanelState{
|
||||||
ConflictIndex: 0,
|
State: mergeconflicts.NewState(),
|
||||||
ConflictTop: true,
|
UserScrolling: false,
|
||||||
Conflicts: []commands.Conflict{},
|
|
||||||
EditHistory: stack.New(),
|
|
||||||
ConflictsMutex: sync.Mutex{},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Ptmx: nil,
|
Ptmx: nil,
|
||||||
|
@ -3,14 +3,11 @@
|
|||||||
package gui
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/golang-collections/collections/stack"
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||||
@ -20,7 +17,7 @@ import (
|
|||||||
func (gui *Gui) handleSelectTop() error {
|
func (gui *Gui) handleSelectTop() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
gui.State.Panels.Merging.ConflictTop = true
|
gui.State.Panels.Merging.SelectTopOption()
|
||||||
return gui.refreshMergePanel()
|
return gui.refreshMergePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -28,7 +25,7 @@ func (gui *Gui) handleSelectTop() error {
|
|||||||
func (gui *Gui) handleSelectBottom() error {
|
func (gui *Gui) handleSelectBottom() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
gui.State.Panels.Merging.ConflictTop = false
|
gui.State.Panels.Merging.SelectBottomOption()
|
||||||
return gui.refreshMergePanel()
|
return gui.refreshMergePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -36,10 +33,7 @@ func (gui *Gui) handleSelectBottom() error {
|
|||||||
func (gui *Gui) handleSelectNextConflict() error {
|
func (gui *Gui) handleSelectNextConflict() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
|
gui.State.Panels.Merging.SelectNextConflict()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gui.State.Panels.Merging.ConflictIndex++
|
|
||||||
return gui.refreshMergePanel()
|
return gui.refreshMergePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -47,32 +41,26 @@ func (gui *Gui) handleSelectNextConflict() error {
|
|||||||
func (gui *Gui) handleSelectPrevConflict() error {
|
func (gui *Gui) handleSelectPrevConflict() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
if gui.State.Panels.Merging.ConflictIndex <= 0 {
|
gui.State.Panels.Merging.SelectPrevConflict()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
gui.State.Panels.Merging.ConflictIndex--
|
|
||||||
return gui.refreshMergePanel()
|
return gui.refreshMergePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) pushFileSnapshot() error {
|
func (gui *Gui) pushFileSnapshot() error {
|
||||||
gitFile := gui.getSelectedFile()
|
content, err := gui.catSelectedFile()
|
||||||
if gitFile == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
content, err := gui.GitCommand.CatFile(gitFile.Name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
gui.State.Panels.Merging.EditHistory.Push(content)
|
gui.State.Panels.Merging.PushFileSnapshot(content)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) handlePopFileSnapshot() error {
|
func (gui *Gui) handlePopFileSnapshot() error {
|
||||||
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
|
prevContent, ok := gui.State.Panels.Merging.PopFileSnapshot()
|
||||||
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
|
|
||||||
gitFile := gui.getSelectedFile()
|
gitFile := gui.getSelectedFile()
|
||||||
if gitFile == nil {
|
if gitFile == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -87,28 +75,18 @@ func (gui *Gui) handlePopFileSnapshot() error {
|
|||||||
|
|
||||||
func (gui *Gui) handlePickHunk() error {
|
func (gui *Gui) handlePickHunk() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
conflict := gui.getCurrentConflict()
|
|
||||||
if conflict == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
|
|
||||||
if err := gui.pushFileSnapshot(); err != nil {
|
ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection())
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
selection := mergeconflicts.BOTTOM
|
if !ok {
|
||||||
if gui.State.Panels.Merging.ConflictTop {
|
return nil
|
||||||
selection = mergeconflicts.TOP
|
|
||||||
}
|
|
||||||
err := gui.resolveConflict(*conflict, selection)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if that was the last conflict, finish the merge for this file
|
if gui.State.Panels.Merging.IsFinalConflict() {
|
||||||
if len(gui.State.Panels.Merging.Conflicts) == 1 {
|
|
||||||
if err := gui.handleCompleteMerge(); err != nil {
|
if err := gui.handleCompleteMerge(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -119,53 +97,38 @@ func (gui *Gui) handlePickHunk() error {
|
|||||||
|
|
||||||
func (gui *Gui) handlePickBothHunks() error {
|
func (gui *Gui) handlePickBothHunks() error {
|
||||||
return gui.withMergeConflictLock(func() error {
|
return gui.withMergeConflictLock(func() error {
|
||||||
conflict := gui.getCurrentConflict()
|
gui.takeOverMergeConflictScrolling()
|
||||||
if conflict == nil {
|
|
||||||
|
ok, err := gui.resolveConflict(mergeconflicts.BOTH)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
gui.takeOverMergeConflictScrolling()
|
|
||||||
|
|
||||||
if err := gui.pushFileSnapshot(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err := gui.resolveConflict(*conflict, mergeconflicts.BOTH)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return gui.refreshMergePanel()
|
return gui.refreshMergePanel()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) getCurrentConflict() *commands.Conflict {
|
func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) {
|
||||||
if len(gui.State.Panels.Merging.Conflicts) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconflicts.Selection) error {
|
|
||||||
gitFile := gui.getSelectedFile()
|
gitFile := gui.getSelectedFile()
|
||||||
if gitFile == nil {
|
if gitFile == nil {
|
||||||
return nil
|
return false, nil
|
||||||
}
|
}
|
||||||
file, err := os.Open(gitFile.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
ok, output, err := gui.State.Panels.Merging.ContentAfterConflictResolve(gitFile.Name, selection)
|
||||||
output := ""
|
if err != nil {
|
||||||
for i := 0; true; i++ {
|
return false, err
|
||||||
line, err := reader.ReadString('\n')
|
}
|
||||||
if err != nil {
|
|
||||||
break
|
if !ok {
|
||||||
}
|
return false, nil
|
||||||
if !mergeconflicts.IsIndexToDelete(i, conflict, selection) {
|
}
|
||||||
output += line
|
|
||||||
}
|
if err := gui.pushFileSnapshot(); err != nil {
|
||||||
|
return false, gui.surfaceError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var logStr string
|
var logStr string
|
||||||
@ -178,7 +141,7 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconfl
|
|||||||
logStr = "Picking both hunks"
|
logStr = "Picking both hunks"
|
||||||
}
|
}
|
||||||
gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
|
gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
|
||||||
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) refreshMergePanelWithLock() error {
|
func (gui *Gui) refreshMergePanelWithLock() error {
|
||||||
@ -197,17 +160,14 @@ func (gui *Gui) refreshMergePanel() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
panelState.Conflicts = mergeconflicts.FindConflicts(cat)
|
panelState.SetConflictsFromCat(cat)
|
||||||
|
|
||||||
// handle potential fixes that the user made in their editor since we last refreshed
|
if panelState.NoConflicts() {
|
||||||
if len(panelState.Conflicts) == 0 {
|
|
||||||
return gui.handleCompleteMerge()
|
return gui.handleCompleteMerge()
|
||||||
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
|
|
||||||
panelState.ConflictIndex = len(panelState.Conflicts) - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasFocus := gui.currentViewName() == "main"
|
hasFocus := gui.currentViewName() == "main"
|
||||||
content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
|
content := mergeconflicts.ColoredConflictFile(cat, panelState.State, hasFocus)
|
||||||
|
|
||||||
if err := gui.scrollToConflict(); err != nil {
|
if err := gui.scrollToConflict(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -246,21 +206,24 @@ func (gui *Gui) scrollToConflict() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
panelState := gui.State.Panels.Merging
|
panelState := gui.State.Panels.Merging
|
||||||
if len(panelState.Conflicts) == 0 {
|
if panelState.NoConflicts() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mergingView := gui.Views.Main
|
gui.centerYPos(gui.Views.Main, panelState.GetConflictMiddle())
|
||||||
conflict := panelState.Conflicts[panelState.ConflictIndex]
|
|
||||||
ox, _ := mergingView.Origin()
|
|
||||||
_, height := mergingView.Size()
|
|
||||||
newOriginY := int(math.Max(0, float64(conflict.Middle-(height/2))))
|
|
||||||
gui.g.Update(func(g *gocui.Gui) error {
|
|
||||||
return mergingView.SetOrigin(ox, newOriginY)
|
|
||||||
})
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) centerYPos(view *gocui.View, y int) {
|
||||||
|
ox, _ := view.Origin()
|
||||||
|
_, height := view.Size()
|
||||||
|
newOriginY := int(math.Max(0, float64(y-(height/2))))
|
||||||
|
gui.g.Update(func(g *gocui.Gui) error {
|
||||||
|
return view.SetOrigin(ox, newOriginY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (gui *Gui) getMergingOptions() map[string]string {
|
func (gui *Gui) getMergingOptions() map[string]string {
|
||||||
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
keybindingConfig := gui.Config.GetUserConfig().Keybinding
|
||||||
|
|
||||||
@ -276,7 +239,7 @@ func (gui *Gui) getMergingOptions() map[string]string {
|
|||||||
func (gui *Gui) handleEscapeMerge() error {
|
func (gui *Gui) handleEscapeMerge() error {
|
||||||
gui.takeOverMergeConflictScrolling()
|
gui.takeOverMergeConflictScrolling()
|
||||||
|
|
||||||
gui.State.Panels.Merging.EditHistory = stack.New()
|
gui.State.Panels.Merging.Reset()
|
||||||
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -343,8 +306,8 @@ func (gui *Gui) canScrollMergePanel() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gui *Gui) withMergeConflictLock(f func() error) error {
|
func (gui *Gui) withMergeConflictLock(f func() error) error {
|
||||||
gui.State.Panels.Merging.ConflictsMutex.Lock()
|
gui.State.Panels.Merging.Lock()
|
||||||
defer gui.State.Panels.Merging.ConflictsMutex.Unlock()
|
defer gui.State.Panels.Merging.Unlock()
|
||||||
|
|
||||||
return f()
|
return f()
|
||||||
}
|
}
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
package mergeconflicts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Selection int
|
|
||||||
|
|
||||||
const (
|
|
||||||
TOP Selection = iota
|
|
||||||
BOTTOM
|
|
||||||
BOTH
|
|
||||||
)
|
|
||||||
|
|
||||||
func FindConflicts(content string) []commands.Conflict {
|
|
||||||
conflicts := make([]commands.Conflict, 0)
|
|
||||||
|
|
||||||
if content == "" {
|
|
||||||
return conflicts
|
|
||||||
}
|
|
||||||
|
|
||||||
var newConflict commands.Conflict
|
|
||||||
for i, line := range utils.SplitLines(content) {
|
|
||||||
trimmedLine := strings.TrimPrefix(line, "++")
|
|
||||||
switch trimmedLine {
|
|
||||||
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
|
|
||||||
newConflict = commands.Conflict{Start: i}
|
|
||||||
case "=======":
|
|
||||||
newConflict.Middle = i
|
|
||||||
default:
|
|
||||||
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
|
||||||
newConflict.End = i
|
|
||||||
conflicts = append(conflicts, newConflict)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return conflicts
|
|
||||||
}
|
|
||||||
|
|
||||||
func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string {
|
|
||||||
if len(conflicts) == 0 {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
conflict, remainingConflicts := shiftConflict(conflicts)
|
|
||||||
var outputBuffer bytes.Buffer
|
|
||||||
for i, line := range utils.SplitLines(content) {
|
|
||||||
colourAttr := theme.DefaultTextColor
|
|
||||||
if i == conflict.Start || i == conflict.Middle || i == conflict.End {
|
|
||||||
colourAttr = color.FgRed
|
|
||||||
}
|
|
||||||
colour := color.New(colourAttr)
|
|
||||||
if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && shouldHighlightLine(i, conflict, conflictTop) {
|
|
||||||
colour.Add(color.Bold)
|
|
||||||
colour.Add(theme.SelectedRangeBgColor)
|
|
||||||
}
|
|
||||||
if i == conflict.End && len(remainingConflicts) > 0 {
|
|
||||||
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
|
||||||
}
|
|
||||||
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
|
||||||
}
|
|
||||||
return outputBuffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool {
|
|
||||||
return i == conflict.Middle ||
|
|
||||||
i == conflict.Start ||
|
|
||||||
i == conflict.End ||
|
|
||||||
selection != BOTH &&
|
|
||||||
(selection == BOTTOM && i > conflict.Start && i < conflict.Middle) ||
|
|
||||||
(selection == TOP && i > conflict.Middle && i < conflict.End)
|
|
||||||
}
|
|
||||||
|
|
||||||
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
|
||||||
return conflicts[0], conflicts[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
|
||||||
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
|
|
||||||
}
|
|
41
pkg/gui/mergeconflicts/rendering.go
Normal file
41
pkg/gui/mergeconflicts/rendering.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package mergeconflicts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ColoredConflictFile(content string, state *State, hasFocus bool) string {
|
||||||
|
if len(state.conflicts) == 0 {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
conflict, remainingConflicts := shiftConflict(state.conflicts)
|
||||||
|
var outputBuffer bytes.Buffer
|
||||||
|
for i, line := range utils.SplitLines(content) {
|
||||||
|
colourAttr := theme.DefaultTextColor
|
||||||
|
if i == conflict.start || i == conflict.middle || i == conflict.end {
|
||||||
|
colourAttr = color.FgRed
|
||||||
|
}
|
||||||
|
colour := color.New(colourAttr)
|
||||||
|
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.conflictTop) {
|
||||||
|
colour.Add(color.Bold)
|
||||||
|
colour.Add(theme.SelectedRangeBgColor)
|
||||||
|
}
|
||||||
|
if i == conflict.end && len(remainingConflicts) > 0 {
|
||||||
|
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
||||||
|
}
|
||||||
|
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
|
||||||
|
}
|
||||||
|
return outputBuffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict) {
|
||||||
|
return conflicts[0], conflicts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool {
|
||||||
|
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
||||||
|
}
|
185
pkg/gui/mergeconflicts/state.go
Normal file
185
pkg/gui/mergeconflicts/state.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package mergeconflicts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/golang-collections/collections/stack"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Selection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TOP Selection = iota
|
||||||
|
BOTTOM
|
||||||
|
BOTH
|
||||||
|
)
|
||||||
|
|
||||||
|
// mergeConflict : A git conflict with a start middle and end corresponding to line
|
||||||
|
// numbers in the file where the conflict bars appear
|
||||||
|
type mergeConflict struct {
|
||||||
|
start int
|
||||||
|
middle int
|
||||||
|
end int
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
sync.Mutex
|
||||||
|
conflictIndex int
|
||||||
|
conflictTop bool
|
||||||
|
conflicts []*mergeConflict
|
||||||
|
EditHistory *stack.Stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState() *State {
|
||||||
|
return &State{
|
||||||
|
Mutex: sync.Mutex{},
|
||||||
|
conflictIndex: 0,
|
||||||
|
conflictTop: true,
|
||||||
|
conflicts: []*mergeConflict{},
|
||||||
|
EditHistory: stack.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SelectTopOption() {
|
||||||
|
s.conflictTop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SelectBottomOption() {
|
||||||
|
s.conflictTop = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SelectNextConflict() {
|
||||||
|
if s.conflictIndex < len(s.conflicts)-1 {
|
||||||
|
s.conflictIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SelectPrevConflict() {
|
||||||
|
if s.conflictIndex > 0 {
|
||||||
|
s.conflictIndex--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.conflicts[s.conflictIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) SetConflictsFromCat(cat string) {
|
||||||
|
s.setConflicts(findConflicts(cat))
|
||||||
|
}
|
||||||
|
|
||||||
|
func findConflicts(content string) []*mergeConflict {
|
||||||
|
conflicts := make([]*mergeConflict, 0)
|
||||||
|
|
||||||
|
if content == "" {
|
||||||
|
return conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
var newConflict *mergeConflict
|
||||||
|
for i, line := range utils.SplitLines(content) {
|
||||||
|
trimmedLine := strings.TrimPrefix(line, "++")
|
||||||
|
switch trimmedLine {
|
||||||
|
case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours":
|
||||||
|
newConflict = &mergeConflict{start: i}
|
||||||
|
case "=======":
|
||||||
|
newConflict.middle = i
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
|
||||||
|
newConflict.end = i
|
||||||
|
conflicts = append(conflicts, newConflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return conflicts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) setConflicts(conflicts []*mergeConflict) {
|
||||||
|
s.conflicts = conflicts
|
||||||
|
|
||||||
|
if s.conflictIndex > len(s.conflicts)-1 {
|
||||||
|
s.conflictIndex = len(s.conflicts) - 1
|
||||||
|
} else if s.conflictIndex < 0 {
|
||||||
|
s.conflictIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) NoConflicts() bool {
|
||||||
|
return len(s.conflicts) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Selection() Selection {
|
||||||
|
if s.conflictTop {
|
||||||
|
return TOP
|
||||||
|
} else {
|
||||||
|
return BOTTOM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) IsFinalConflict() bool {
|
||||||
|
return len(s.conflicts) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Reset() {
|
||||||
|
s.EditHistory = stack.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) GetConflictMiddle() int {
|
||||||
|
return s.currentConflict().middle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) ContentAfterConflictResolve(path string, selection Selection) (bool, string, error) {
|
||||||
|
conflict := s.currentConflict()
|
||||||
|
if conflict == nil {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
content := ""
|
||||||
|
for i := 0; true; i++ {
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !isIndexToDelete(i, conflict, selection) {
|
||||||
|
content += line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, content, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIndexToDelete(i int, conflict *mergeConflict, selection Selection) bool {
|
||||||
|
return i == conflict.middle ||
|
||||||
|
i == conflict.start ||
|
||||||
|
i == conflict.end ||
|
||||||
|
selection != BOTH &&
|
||||||
|
(selection == BOTTOM && i > conflict.start && i < conflict.middle) ||
|
||||||
|
(selection == TOP && i > conflict.middle && i < conflict.end)
|
||||||
|
}
|
Reference in New Issue
Block a user