summaryrefslogtreecommitdiffstats
path: root/pkg/gui/merge_panel.go
diff options
context:
space:
mode:
authorMark Kopenga <mkopenga@gmail.com>2018-08-14 11:05:26 +0200
committerMark Kopenga <mkopenga@gmail.com>2018-08-14 11:05:26 +0200
commitdfafb988713a79664139d15ad471736a5a4b1b90 (patch)
treeebde4bd5c64ce8e1fe5e8670657c708984b0e8ff /pkg/gui/merge_panel.go
parentf2dfcb6e12d78f3e7b890d5bd43be7b032e1df88 (diff)
tried to update to latest master
Diffstat (limited to 'pkg/gui/merge_panel.go')
-rw-r--r--pkg/gui/merge_panel.go260
1 files changed, 260 insertions, 0 deletions
diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go
new file mode 100644
index 000000000..81e37f593
--- /dev/null
+++ b/pkg/gui/merge_panel.go
@@ -0,0 +1,260 @@
+// 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"
+ "io/ioutil"
+ "math"
+ "os"
+ "strings"
+
+ "github.com/fatih/color"
+ "github.com/jesseduffield/gocui"
+ "github.com/jesseduffield/lazygit/pkg/commands"
+ "github.com/jesseduffield/lazygit/pkg/utils"
+)
+
+func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
+ conflicts := make([]commands.Conflict, 0)
+ var newConflict commands.Conflict
+ for i, line := range utils.SplitLines(content) {
+ if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
+ newConflict = commands.Conflict{Start: i}
+ } else if line == "=======" {
+ newConflict.Middle = i
+ } else if strings.HasPrefix(line, ">>>>>>> ") {
+ newConflict.End = i
+ conflicts = append(conflicts, newConflict)
+ }
+ }
+ return conflicts, nil
+}
+
+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 := 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 && gui.shouldHighlightLine(i, conflict, conflictTop) {
+ colour.Add(color.Bold)
+ }
+ 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) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
+ gui.State.ConflictTop = true
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
+ gui.State.ConflictTop = false
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
+ if gui.State.ConflictIndex >= len(gui.State.Conflicts)-1 {
+ return nil
+ }
+ gui.State.ConflictIndex++
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
+ if gui.State.ConflictIndex <= 0 {
+ return nil
+ }
+ gui.State.ConflictIndex--
+ return gui.refreshMergePanel(g)
+}
+
+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)
+}
+
+func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
+ gitFile, err := gui.getSelectedFile(g)
+ if err != nil {
+ return err
+ }
+ 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 {
+ gitFile, err := gui.getSelectedFile(g)
+ if err != nil {
+ return err
+ }
+ content, err := gui.GitCommand.CatFile(gitFile.Name)
+ if err != nil {
+ return err
+ }
+ gui.State.EditHistory.Push(content)
+ return nil
+}
+
+func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
+ if gui.State.EditHistory.Len() == 0 {
+ return nil
+ }
+ prevContent := gui.State.EditHistory.Pop().(string)
+ gitFile, err := gui.getSelectedFile(g)
+ if err != nil {
+ return err
+ }
+ ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
+ conflict := gui.State.Conflicts[gui.State.ConflictIndex]
+ gui.pushFileSnapshot(g)
+ pick := "bottom"
+ if gui.State.ConflictTop {
+ pick = "top"
+ }
+ err := gui.resolveConflict(g, conflict, pick)
+ if err != nil {
+ panic(err)
+ }
+ gui.refreshMergePanel(g)
+ return nil
+}
+
+func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
+ conflict := gui.State.Conflicts[gui.State.ConflictIndex]
+ gui.pushFileSnapshot(g)
+ err := gui.resolveConflict(g, conflict, "both")
+ if err != nil {
+ panic(err)
+ }
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) refreshMergePanel(g *gocui.Gui) error {
+ cat, err := gui.catSelectedFile(g)
+ if err != nil {
+ return err
+ }
+ gui.State.Conflicts, err = gui.findConflicts(cat)
+ if err != nil {
+ return err
+ }
+
+ if len(gui.State.Conflicts) == 0 {
+ return gui.handleCompleteMerge(g)
+ } else if gui.State.ConflictIndex > len(gui.State.Conflicts)-1 {
+ gui.State.ConflictIndex = len(gui.State.Conflicts) - 1
+ }
+ hasFocus := gui.currentViewName(g) == "main"
+ if hasFocus {
+ gui.renderMergeOptions(g)
+ }
+ content, err := gui.coloredConflictFile(cat, gui.State.Conflicts, gui.State.ConflictIndex, gui.State.ConflictTop, hasFocus)
+ if err != nil {
+ return err
+ }
+ if err := gui.scrollToConflict(g); err != nil {
+ return err
+ }
+ return gui.renderString(g, "main", content)
+}
+
+func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
+ mainView, err := g.View("main")
+ if err != nil {
+ return err
+ }
+ if len(gui.State.Conflicts) == 0 {
+ return nil
+ }
+ conflict := gui.State.Conflicts[gui.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)
+}
+
+func (gui *Gui) switchToMerging(g *gocui.Gui) error {
+ gui.State.ConflictIndex = 0
+ gui.State.ConflictTop = true
+ _, err := g.SetCurrentView("main")
+ if err != nil {
+ return err
+ }
+ return gui.refreshMergePanel(g)
+}
+
+func (gui *Gui) renderMergeOptions(g *gocui.Gui) error {
+ return gui.renderOptionsMap(g, map[string]string{
+ "↑ ↓": "select hunk",
+ "← →": "navigate conflicts",
+ "space": "pick hunk",
+ "b": "pick both hunks",
+ "z": "undo",
+ })
+}
+
+func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
+ filesView, err := g.View("files")
+ if err != nil {
+ return err
+ }
+ gui.refreshFiles(g)
+ return gui.switchFocus(g, v, filesView)
+}
+
+func (gui *Gui) handleCompleteMerge(g *gocui.Gui) error {
+ filesView, err := g.View("files")
+ if err != nil {
+ return err
+ }
+ gui.stageSelectedFile(g)
+ gui.refreshFiles(g)
+ return gui.switchFocus(g, nil, filesView)
+}