2018-06-09 11:06:33 +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 main
|
|
|
|
|
|
|
|
import (
|
2018-07-21 07:53:52 +02:00
|
|
|
"bufio"
|
|
|
|
"bytes"
|
|
|
|
"io/ioutil"
|
2018-07-21 09:48:27 +02:00
|
|
|
"math"
|
2018-07-21 07:53:52 +02:00
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
|
"github.com/jesseduffield/gocui"
|
2018-06-09 11:06:33 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
func findConflicts(content string) ([]conflict, error) {
|
2018-07-21 07:53:52 +02:00
|
|
|
conflicts := make([]conflict, 0)
|
|
|
|
var newConflict conflict
|
|
|
|
for i, line := range splitLines(content) {
|
2018-08-07 02:41:14 +02:00
|
|
|
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
2018-07-21 07:53:52 +02:00
|
|
|
newConflict = conflict{start: i}
|
|
|
|
} else if line == "=======" {
|
|
|
|
newConflict.middle = i
|
|
|
|
} else if strings.HasPrefix(line, ">>>>>>> ") {
|
|
|
|
newConflict.end = i
|
|
|
|
conflicts = append(conflicts, newConflict)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return conflicts, nil
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
|
2018-07-21 07:53:52 +02:00
|
|
|
return conflicts[0], conflicts[1:]
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
|
2018-07-21 07:53:52 +02:00
|
|
|
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
2018-07-21 07:53:52 +02:00
|
|
|
if len(conflicts) == 0 {
|
|
|
|
return content, nil
|
|
|
|
}
|
|
|
|
conflict, remainingConflicts := shiftConflict(conflicts)
|
|
|
|
var outputBuffer bytes.Buffer
|
|
|
|
for i, line := range splitLines(content) {
|
|
|
|
colourAttr := color.FgWhite
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
if i == conflict.end && len(remainingConflicts) > 0 {
|
|
|
|
conflict, remainingConflicts = shiftConflict(remainingConflicts)
|
|
|
|
}
|
2018-08-10 13:33:49 +02:00
|
|
|
outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n")
|
2018-07-21 07:53:52 +02:00
|
|
|
}
|
|
|
|
return outputBuffer.String(), nil
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleSelectTop(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
state.ConflictTop = true
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
state.ConflictTop = false
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
if state.ConflictIndex >= len(state.Conflicts)-1 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
state.ConflictIndex++
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
if state.ConflictIndex <= 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
state.ConflictIndex--
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
2018-07-21 09:48:27 +02:00
|
|
|
func isIndexToDelete(i int, conflict conflict, pick string) bool {
|
2018-07-21 07:53:52 +02:00
|
|
|
return i == conflict.middle ||
|
|
|
|
i == conflict.start ||
|
|
|
|
i == conflict.end ||
|
2018-07-21 09:48:27 +02:00
|
|
|
pick != "both" &&
|
|
|
|
(pick == "bottom" && i > conflict.start && i < conflict.middle) ||
|
|
|
|
(pick == "top" && i > conflict.middle && i < conflict.end)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
2018-07-21 09:48:27 +02:00
|
|
|
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
|
|
|
|
gitFile, err := getSelectedFile(g)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
file, err := os.Open(gitFile.Name)
|
2018-07-21 07:53:52 +02:00
|
|
|
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
|
|
|
|
}
|
2018-07-21 09:48:27 +02:00
|
|
|
if !isIndexToDelete(i, conflict, pick) {
|
2018-07-21 07:53:52 +02:00
|
|
|
output += line
|
|
|
|
}
|
|
|
|
}
|
|
|
|
devLog(output)
|
2018-07-21 09:48:27 +02:00
|
|
|
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
2018-07-21 09:48:27 +02:00
|
|
|
func pushFileSnapshot(g *gocui.Gui) error {
|
|
|
|
gitFile, err := getSelectedFile(g)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
content, err := catFile(gitFile.Name)
|
2018-07-21 07:53:52 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
state.EditHistory.Push(content)
|
|
|
|
return nil
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
if state.EditHistory.Len() == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
prevContent := state.EditHistory.Pop().(string)
|
|
|
|
gitFile, err := getSelectedFile(g)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
2018-07-21 09:48:27 +02:00
|
|
|
func handlePickHunk(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
conflict := state.Conflicts[state.ConflictIndex]
|
2018-07-21 09:48:27 +02:00
|
|
|
pushFileSnapshot(g)
|
|
|
|
pick := "bottom"
|
|
|
|
if state.ConflictTop {
|
|
|
|
pick = "top"
|
|
|
|
}
|
|
|
|
err := resolveConflict(g, conflict, pick)
|
2018-07-21 07:53:52 +02:00
|
|
|
if err != nil {
|
2018-07-21 09:48:27 +02:00
|
|
|
panic(err)
|
2018-07-21 07:53:52 +02:00
|
|
|
}
|
2018-07-21 09:48:27 +02:00
|
|
|
refreshMergePanel(g)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
|
|
|
|
conflict := state.Conflicts[state.ConflictIndex]
|
|
|
|
pushFileSnapshot(g)
|
|
|
|
err := resolveConflict(g, conflict, "both")
|
2018-07-21 07:53:52 +02:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func currentViewName(g *gocui.Gui) string {
|
2018-07-21 07:53:52 +02:00
|
|
|
currentView := g.CurrentView()
|
|
|
|
return currentView.Name()
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func refreshMergePanel(g *gocui.Gui) error {
|
2018-07-21 09:48:27 +02:00
|
|
|
cat, err := catSelectedFile(g)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
state.Conflicts, err = findConflicts(cat)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(state.Conflicts) == 0 {
|
|
|
|
return handleCompleteMerge(g)
|
|
|
|
} else if state.ConflictIndex > len(state.Conflicts)-1 {
|
|
|
|
state.ConflictIndex = len(state.Conflicts) - 1
|
|
|
|
}
|
|
|
|
hasFocus := currentViewName(g) == "main"
|
|
|
|
if hasFocus {
|
|
|
|
renderMergeOptions(g)
|
|
|
|
}
|
|
|
|
content, err := coloredConflictFile(cat, state.Conflicts, state.ConflictIndex, state.ConflictTop, hasFocus)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := scrollToConflict(g); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return renderString(g, "main", content)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
2018-06-10 03:37:10 +02:00
|
|
|
func scrollToConflict(g *gocui.Gui) error {
|
2018-07-21 09:48:27 +02:00
|
|
|
mainView, err := g.View("main")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(state.Conflicts) == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
conflict := state.Conflicts[state.ConflictIndex]
|
|
|
|
ox, _ := mainView.Origin()
|
|
|
|
_, height := mainView.Size()
|
|
|
|
conflictMiddle := (conflict.end + conflict.start) / 2
|
|
|
|
newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
|
|
|
|
return mainView.SetOrigin(ox, newOriginY)
|
2018-06-10 03:37:10 +02:00
|
|
|
}
|
|
|
|
|
2018-06-09 11:06:33 +02:00
|
|
|
func switchToMerging(g *gocui.Gui) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
state.ConflictIndex = 0
|
|
|
|
state.ConflictTop = true
|
|
|
|
_, err := g.SetCurrentView("main")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return refreshMergePanel(g)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func renderMergeOptions(g *gocui.Gui) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
return renderOptionsMap(g, map[string]string{
|
2018-07-22 04:58:09 +02:00
|
|
|
"↑ ↓": "select hunk",
|
|
|
|
"← →": "navigate conflicts",
|
|
|
|
"space": "pick hunk",
|
|
|
|
"b": "pick both hunks",
|
|
|
|
"z": "undo",
|
2018-07-21 07:53:52 +02:00
|
|
|
})
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
2018-07-21 07:53:52 +02:00
|
|
|
filesView, err := g.View("files")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
refreshFiles(g)
|
|
|
|
return switchFocus(g, v, filesView)
|
2018-06-09 11:06:33 +02:00
|
|
|
}
|
2018-07-21 09:48:27 +02:00
|
|
|
|
|
|
|
func handleCompleteMerge(g *gocui.Gui) error {
|
|
|
|
filesView, err := g.View("files")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
stageSelectedFile(g)
|
|
|
|
refreshFiles(g)
|
|
|
|
return switchFocus(g, nil, filesView)
|
|
|
|
}
|