1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-12 11:15:00 +02:00
lazygit/pkg/gui/merge_panel.go

367 lines
10 KiB
Go
Raw Normal View History

2018-08-14 11:05:26 +02:00
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
package gui
import (
"bufio"
"bytes"
"fmt"
2018-08-14 11:05:26 +02:00
"io/ioutil"
"math"
"os"
"strings"
"github.com/fatih/color"
2020-08-18 14:41:14 +02:00
"github.com/go-errors/errors"
2018-12-08 07:54:54 +02:00
"github.com/golang-collections/collections/stack"
2018-08-14 11:05:26 +02:00
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/theme"
2018-08-14 11:05:26 +02:00
"github.com/jesseduffield/lazygit/pkg/utils"
)
2020-08-18 14:23:36 +02:00
func (gui *Gui) findConflicts(content string) []commands.Conflict {
2018-08-14 11:05:26 +02:00
conflicts := make([]commands.Conflict, 0)
2020-08-18 14:23:36 +02:00
if content == "" {
return conflicts
}
2018-08-14 11:05:26 +02:00
var newConflict commands.Conflict
for i, line := range utils.SplitLines(content) {
trimmedLine := strings.TrimPrefix(line, "++")
2018-12-08 07:54:54 +02:00
gui.Log.Info(trimmedLine)
if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" {
2018-08-14 11:05:26 +02:00
newConflict = commands.Conflict{Start: i}
} else if trimmedLine == "=======" {
2018-08-14 11:05:26 +02:00
newConflict.Middle = i
} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
2018-08-14 11:05:26 +02:00
newConflict.End = i
conflicts = append(conflicts, newConflict)
}
}
2020-08-18 14:23:36 +02:00
return conflicts
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
return conflicts[0], conflicts[1:]
}
func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
}
func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
if len(conflicts) == 0 {
return content, nil
}
conflict, remainingConflicts := gui.shiftConflict(conflicts)
var outputBuffer bytes.Buffer
for i, line := range utils.SplitLines(content) {
colourAttr := theme.DefaultTextColor
2018-08-14 11:05:26 +02:00
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 && gui.shouldHighlightLine(i, conflict, conflictTop) {
colour.Add(color.Bold)
colour.Add(theme.SelectedRangeBgColor)
2018-08-14 11:05:26 +02:00
}
if i == conflict.End && len(remainingConflicts) > 0 {
conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}
func (gui *Gui) takeOverScrolling() {
gui.State.Panels.Merging.UserScrolling = false
}
2018-08-14 11:05:26 +02:00
func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
2018-08-14 11:05:26 +02:00
return nil
}
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
if gui.State.Panels.Merging.ConflictIndex <= 0 {
2018-08-14 11:05:26 +02:00
return nil
}
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
return i == conflict.Middle ||
i == conflict.Start ||
i == conflict.End ||
pick != "both" &&
(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
(pick == "top" && i > conflict.Middle && i < conflict.End)
}
2020-08-15 09:23:16 +02:00
func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error {
2020-08-16 09:45:12 +02:00
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
2018-08-14 11:05:26 +02:00
}
file, err := os.Open(gitFile.Name)
if err != nil {
return err
}
defer file.Close()
reader := bufio.NewReader(file)
output := ""
for i := 0; true; i++ {
line, err := reader.ReadString('\n')
if err != nil {
break
}
if !gui.isIndexToDelete(i, conflict, pick) {
output += line
}
}
gui.Log.Info(output)
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
2020-08-16 09:45:12 +02:00
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
2018-08-14 11:05:26 +02:00
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.EditHistory.Push(content)
2018-08-14 11:05:26 +02:00
return nil
}
func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
2018-12-08 07:54:54 +02:00
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
2018-08-14 11:05:26 +02:00
return nil
}
2018-12-08 07:54:54 +02:00
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
2020-08-16 09:45:12 +02:00
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
2018-08-14 11:05:26 +02:00
}
2020-03-09 02:34:10 +02:00
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
}
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
2020-03-09 02:34:10 +02:00
if err := gui.pushFileSnapshot(g); err != nil {
return err
}
2018-08-14 11:05:26 +02:00
pick := "bottom"
2018-12-08 07:54:54 +02:00
if gui.State.Panels.Merging.ConflictTop {
2018-08-14 11:05:26 +02:00
pick = "top"
}
2020-08-15 09:23:16 +02:00
err := gui.resolveConflict(conflict, pick)
2018-08-14 11:05:26 +02:00
if err != nil {
panic(err)
}
2018-12-08 07:54:54 +02:00
// if that was the last conflict, finish the merge for this file
if len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
2019-02-16 12:30:29 +02:00
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
2020-03-09 02:34:10 +02:00
if err := gui.pushFileSnapshot(g); err != nil {
return err
}
2020-08-15 09:23:16 +02:00
err := gui.resolveConflict(conflict, "both")
2018-08-14 11:05:26 +02:00
if err != nil {
panic(err)
}
return gui.refreshMergePanel()
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) refreshMergePanel() error {
2018-12-08 07:54:54 +02:00
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile(gui.g)
2018-08-14 11:05:26 +02:00
if err != nil {
2020-08-23 01:46:28 +02:00
return gui.refreshMainViews(refreshMainOpts{
2020-08-18 14:23:36 +02:00
main: &viewUpdateOpts{
title: "",
task: gui.createRenderStringTask(err.Error()),
},
})
2018-08-14 11:05:26 +02:00
}
2020-08-18 14:23:36 +02:00
panelState.Conflicts = gui.findConflicts(cat)
2018-12-08 07:54:54 +02:00
// handle potential fixes that the user made in their editor since we last refreshed
if len(panelState.Conflicts) == 0 {
return gui.handleCompleteMerge()
} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
panelState.ConflictIndex = len(panelState.Conflicts) - 1
2018-08-14 11:05:26 +02:00
}
2018-12-08 07:54:54 +02:00
2019-02-25 13:11:35 +02:00
hasFocus := gui.currentViewName() == "main"
2018-12-08 07:54:54 +02:00
content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
2018-08-14 11:05:26 +02:00
if err != nil {
return err
}
if err := gui.scrollToConflict(gui.g); err != nil {
2020-03-29 02:49:37 +02:00
return err
}
2020-08-23 01:46:28 +02:00
return gui.refreshMainViews(refreshMainOpts{
2020-08-18 14:02:35 +02:00
main: &viewUpdateOpts{
title: gui.Tr.SLocalize("MergeConflictsTitle"),
task: gui.createRenderStringWithoutScrollTask(content),
noWrap: true,
},
})
2018-08-14 11:05:26 +02:00
}
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
2020-08-16 09:45:12 +02:00
item := gui.getSelectedFile()
if item == nil {
2020-08-18 14:41:14 +02:00
return "", errors.New(gui.Tr.SLocalize("NoFilesDisplay"))
}
2020-08-16 09:45:12 +02:00
if item.Type != "file" {
2020-08-18 14:41:14 +02:00
return "", errors.New(gui.Tr.SLocalize("NotAFile"))
}
2020-08-16 09:45:12 +02:00
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
2020-08-18 14:41:14 +02:00
return "", err
}
return cat, nil
}
2018-08-14 11:05:26 +02:00
func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
if gui.State.Panels.Merging.UserScrolling {
return nil
}
2018-12-08 07:54:54 +02:00
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
2018-08-14 11:05:26 +02:00
return nil
}
mergingView := gui.getMainView()
2018-12-08 07:54:54 +02:00
conflict := panelState.Conflicts[panelState.ConflictIndex]
ox, _ := mergingView.Origin()
_, height := mergingView.Size()
2018-08-14 11:05:26 +02:00
conflictMiddle := (conflict.End + conflict.Start) / 2
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
2018-12-08 07:54:54 +02:00
gui.g.Update(func(g *gocui.Gui) error {
return mergingView.SetOrigin(ox, newOriginY)
})
return nil
2018-08-14 11:05:26 +02:00
}
2018-12-07 09:52:31 +02:00
func (gui *Gui) renderMergeOptions() error {
return gui.renderOptionsMap(map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevItem"), gui.getKeyDisplay("universal.nextItem")): gui.Tr.SLocalize("selectHunk"),
fmt.Sprintf("%s %s", gui.getKeyDisplay("universal.prevBlock"), gui.getKeyDisplay("universal.nextBlock")): gui.Tr.SLocalize("navigateConflicts"),
gui.getKeyDisplay("universal.select"): gui.Tr.SLocalize("pickHunk"),
gui.getKeyDisplay("main.pickBothHunks"): gui.Tr.SLocalize("pickBothHunks"),
2020-03-21 04:39:20 +02:00
gui.getKeyDisplay("universal.undo"): gui.Tr.SLocalize("undo"),
})
2018-08-14 11:05:26 +02:00
}
2020-08-15 09:23:16 +02:00
func (gui *Gui) handleEscapeMerge() error {
gui.takeOverScrolling()
2018-12-08 07:54:54 +02:00
gui.State.Panels.Merging.EditHistory = stack.New()
2020-03-28 00:57:36 +02:00
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
2019-02-16 12:30:29 +02:00
return err
}
2018-12-08 07:54:54 +02:00
// it's possible this method won't be called from the merging view so we need to
// ensure we only 'return' focus if we already have it
if gui.g.CurrentView() == gui.getMainView() {
2020-08-16 05:58:29 +02:00
return gui.switchContext(gui.Contexts.Files.Context)
2018-08-14 11:05:26 +02:00
}
2018-12-08 07:54:54 +02:00
return nil
2018-08-14 11:05:26 +02:00
}
2018-12-08 07:54:54 +02:00
func (gui *Gui) handleCompleteMerge() error {
2019-02-16 12:30:29 +02:00
if err := gui.stageSelectedFile(gui.g); err != nil {
return err
}
2020-03-28 00:57:36 +02:00
if err := gui.refreshSidePanels(refreshOptions{scope: []int{FILES}}); err != nil {
2019-02-16 12:30:29 +02:00
return err
}
2019-02-16 12:01:17 +02:00
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == "normal" {
2020-08-15 09:23:16 +02:00
return gui.handleEscapeMerge()
2019-02-16 12:01:17 +02:00
}
2018-12-08 07:54:54 +02:00
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
2019-03-03 07:11:20 +02:00
return gui.promptToContinue()
2018-12-02 18:34:15 +02:00
}
2020-08-15 09:23:16 +02:00
return gui.handleEscapeMerge()
2018-08-14 11:05:26 +02:00
}
2019-03-03 07:11:20 +02:00
// promptToContinue asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinue() error {
gui.takeOverScrolling()
2020-08-15 08:38:16 +02:00
return gui.ask(askOpts{
2020-08-15 08:36:39 +02:00
returnToView: gui.getFilesView(),
returnFocusOnClose: true,
title: "continue",
prompt: gui.Tr.SLocalize("ConflictsResolved"),
handleConfirm: func() error {
return gui.genericMergeCommand("continue")
},
})
2019-03-03 07:11:20 +02:00
}
func (gui *Gui) canScrollMergePanel() bool {
currentViewName := gui.currentViewName()
if currentViewName != "main" {
return false
}
2020-08-16 09:45:12 +02:00
file := gui.getSelectedFile()
if file == nil {
return false
}
return file.HasInlineMergeConflicts
}