github.com/andrewrech/lazygit@v0.8.1/pkg/gui/merge_panel.go (about)

     1  // though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
     2  
     3  package gui
     4  
     5  import (
     6  	"bufio"
     7  	"bytes"
     8  	"io/ioutil"
     9  	"math"
    10  	"os"
    11  	"strings"
    12  
    13  	"github.com/fatih/color"
    14  	"github.com/golang-collections/collections/stack"
    15  	"github.com/jesseduffield/gocui"
    16  	"github.com/jesseduffield/lazygit/pkg/commands"
    17  	"github.com/jesseduffield/lazygit/pkg/utils"
    18  )
    19  
    20  func (gui *Gui) findConflicts(content string) ([]commands.Conflict, error) {
    21  	conflicts := make([]commands.Conflict, 0)
    22  	var newConflict commands.Conflict
    23  	for i, line := range utils.SplitLines(content) {
    24  		trimmedLine := strings.TrimPrefix(line, "++")
    25  		gui.Log.Info(trimmedLine)
    26  		if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" {
    27  			newConflict = commands.Conflict{Start: i}
    28  		} else if trimmedLine == "=======" {
    29  			newConflict.Middle = i
    30  		} else if strings.HasPrefix(trimmedLine, ">>>>>>> ") {
    31  			newConflict.End = i
    32  			conflicts = append(conflicts, newConflict)
    33  		}
    34  	}
    35  	return conflicts, nil
    36  }
    37  
    38  func (gui *Gui) shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
    39  	return conflicts[0], conflicts[1:]
    40  }
    41  
    42  func (gui *Gui) shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
    43  	return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top)
    44  }
    45  
    46  func (gui *Gui) coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
    47  	if len(conflicts) == 0 {
    48  		return content, nil
    49  	}
    50  	conflict, remainingConflicts := gui.shiftConflict(conflicts)
    51  	var outputBuffer bytes.Buffer
    52  	for i, line := range utils.SplitLines(content) {
    53  		colourAttr := color.FgWhite
    54  		if i == conflict.Start || i == conflict.Middle || i == conflict.End {
    55  			colourAttr = color.FgRed
    56  		}
    57  		colour := color.New(colourAttr)
    58  		if hasFocus && conflictIndex < len(conflicts) && conflicts[conflictIndex] == conflict && gui.shouldHighlightLine(i, conflict, conflictTop) {
    59  			colour.Add(color.Bold)
    60  		}
    61  		if i == conflict.End && len(remainingConflicts) > 0 {
    62  			conflict, remainingConflicts = gui.shiftConflict(remainingConflicts)
    63  		}
    64  		outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n")
    65  	}
    66  	return outputBuffer.String(), nil
    67  }
    68  
    69  func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error {
    70  	gui.State.Panels.Merging.ConflictTop = true
    71  	return gui.refreshMergePanel()
    72  }
    73  
    74  func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error {
    75  	gui.State.Panels.Merging.ConflictTop = false
    76  	return gui.refreshMergePanel()
    77  }
    78  
    79  func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error {
    80  	if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
    81  		return nil
    82  	}
    83  	gui.State.Panels.Merging.ConflictIndex++
    84  	return gui.refreshMergePanel()
    85  }
    86  
    87  func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
    88  	if gui.State.Panels.Merging.ConflictIndex <= 0 {
    89  		return nil
    90  	}
    91  	gui.State.Panels.Merging.ConflictIndex--
    92  	return gui.refreshMergePanel()
    93  }
    94  
    95  func (gui *Gui) isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
    96  	return i == conflict.Middle ||
    97  		i == conflict.Start ||
    98  		i == conflict.End ||
    99  		pick != "both" &&
   100  			(pick == "bottom" && i > conflict.Start && i < conflict.Middle) ||
   101  		(pick == "top" && i > conflict.Middle && i < conflict.End)
   102  }
   103  
   104  func (gui *Gui) resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
   105  	gitFile, err := gui.getSelectedFile(g)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	file, err := os.Open(gitFile.Name)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer file.Close()
   114  
   115  	reader := bufio.NewReader(file)
   116  	output := ""
   117  	for i := 0; true; i++ {
   118  		line, err := reader.ReadString('\n')
   119  		if err != nil {
   120  			break
   121  		}
   122  		if !gui.isIndexToDelete(i, conflict, pick) {
   123  			output += line
   124  		}
   125  	}
   126  	gui.Log.Info(output)
   127  	return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
   128  }
   129  
   130  func (gui *Gui) pushFileSnapshot(g *gocui.Gui) error {
   131  	gitFile, err := gui.getSelectedFile(g)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	content, err := gui.GitCommand.CatFile(gitFile.Name)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	gui.State.Panels.Merging.EditHistory.Push(content)
   140  	return nil
   141  }
   142  
   143  func (gui *Gui) handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
   144  	if gui.State.Panels.Merging.EditHistory.Len() == 0 {
   145  		return nil
   146  	}
   147  	prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
   148  	gitFile, err := gui.getSelectedFile(g)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644)
   153  	return gui.refreshMergePanel()
   154  }
   155  
   156  func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error {
   157  	conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
   158  	gui.pushFileSnapshot(g)
   159  	pick := "bottom"
   160  	if gui.State.Panels.Merging.ConflictTop {
   161  		pick = "top"
   162  	}
   163  	err := gui.resolveConflict(g, conflict, pick)
   164  	if err != nil {
   165  		panic(err)
   166  	}
   167  
   168  	// if that was the last conflict, finish the merge for this file
   169  	if len(gui.State.Panels.Merging.Conflicts) == 1 {
   170  		if err := gui.handleCompleteMerge(); err != nil {
   171  			return err
   172  		}
   173  	}
   174  	return gui.refreshMergePanel()
   175  }
   176  
   177  func (gui *Gui) handlePickBothHunks(g *gocui.Gui, v *gocui.View) error {
   178  	conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex]
   179  	gui.pushFileSnapshot(g)
   180  	err := gui.resolveConflict(g, conflict, "both")
   181  	if err != nil {
   182  		panic(err)
   183  	}
   184  	return gui.refreshMergePanel()
   185  }
   186  
   187  func (gui *Gui) refreshMergePanel() error {
   188  	panelState := gui.State.Panels.Merging
   189  	cat, err := gui.catSelectedFile(gui.g)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if cat == "" {
   194  		return nil
   195  	}
   196  	panelState.Conflicts, err = gui.findConflicts(cat)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	// handle potential fixes that the user made in their editor since we last refreshed
   202  	if len(panelState.Conflicts) == 0 {
   203  		return gui.handleCompleteMerge()
   204  	} else if panelState.ConflictIndex > len(panelState.Conflicts)-1 {
   205  		panelState.ConflictIndex = len(panelState.Conflicts) - 1
   206  	}
   207  
   208  	hasFocus := gui.currentViewName() == "main"
   209  	content, err := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
   210  	if err != nil {
   211  		return err
   212  	}
   213  	if err := gui.renderString(gui.g, "main", content); err != nil {
   214  		return err
   215  	}
   216  	if err := gui.scrollToConflict(gui.g); err != nil {
   217  		return err
   218  	}
   219  
   220  	mainView := gui.getMainView()
   221  	mainView.Wrap = false
   222  
   223  	return nil
   224  }
   225  
   226  func (gui *Gui) scrollToConflict(g *gocui.Gui) error {
   227  	panelState := gui.State.Panels.Merging
   228  	if len(panelState.Conflicts) == 0 {
   229  		return nil
   230  	}
   231  	mergingView := gui.getMainView()
   232  	conflict := panelState.Conflicts[panelState.ConflictIndex]
   233  	ox, _ := mergingView.Origin()
   234  	_, height := mergingView.Size()
   235  	conflictMiddle := (conflict.End + conflict.Start) / 2
   236  	newOriginY := int(math.Max(0, float64(conflictMiddle-(height/2))))
   237  	gui.g.Update(func(g *gocui.Gui) error {
   238  		return mergingView.SetOrigin(ox, newOriginY)
   239  	})
   240  	return nil
   241  }
   242  
   243  func (gui *Gui) renderMergeOptions() error {
   244  	return gui.renderOptionsMap(map[string]string{
   245  		"↑ ↓":   gui.Tr.SLocalize("selectHunk"),
   246  		"← →":   gui.Tr.SLocalize("navigateConflicts"),
   247  		"space": gui.Tr.SLocalize("pickHunk"),
   248  		"b":     gui.Tr.SLocalize("pickBothHunks"),
   249  		"z":     gui.Tr.SLocalize("undo"),
   250  	})
   251  }
   252  
   253  func (gui *Gui) handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
   254  	gui.State.Panels.Merging.EditHistory = stack.New()
   255  	if err := gui.refreshFiles(); err != nil {
   256  		return err
   257  	}
   258  	// it's possible this method won't be called from the merging view so we need to
   259  	// ensure we only 'return' focus if we already have it
   260  	if gui.g.CurrentView() == gui.getMainView() {
   261  		return gui.switchFocus(g, v, gui.getFilesView())
   262  	}
   263  	return nil
   264  }
   265  
   266  func (gui *Gui) handleCompleteMerge() error {
   267  	if err := gui.stageSelectedFile(gui.g); err != nil {
   268  		return err
   269  	}
   270  	if err := gui.refreshFiles(); err != nil {
   271  		return err
   272  	}
   273  	// if we got conflicts after unstashing, we don't want to call any git
   274  	// commands to continue rebasing/merging here
   275  	if gui.State.WorkingTreeState == "normal" {
   276  		return gui.handleEscapeMerge(gui.g, gui.getMainView())
   277  	}
   278  	// if there are no more files with merge conflicts, we should ask whether the user wants to continue
   279  	if !gui.anyFilesWithMergeConflicts() {
   280  		return gui.promptToContinue()
   281  	}
   282  	return gui.handleEscapeMerge(gui.g, gui.getMainView())
   283  }
   284  
   285  // promptToContinue asks the user if they want to continue the rebase/merge that's in progress
   286  func (gui *Gui) promptToContinue() error {
   287  	return gui.createConfirmationPanel(gui.g, gui.getFilesView(), "continue", gui.Tr.SLocalize("ConflictsResolved"), func(g *gocui.Gui, v *gocui.View) error {
   288  		return gui.genericMergeCommand("continue")
   289  	}, nil)
   290  }