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
2021-04-11 17:07:49 +10:00

355 lines
9.4 KiB
Go

// 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"
"fmt"
"io/ioutil"
"math"
"os"
"github.com/go-errors/errors"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)
func (gui *Gui) handleSelectTop() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.ConflictTop = true
return gui.refreshMergePanel()
})
}
func (gui *Gui) handleSelectBottom() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.ConflictTop = false
return gui.refreshMergePanel()
})
}
func (gui *Gui) handleSelectNextConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 {
return nil
}
gui.State.Panels.Merging.ConflictIndex++
return gui.refreshMergePanel()
})
}
func (gui *Gui) handleSelectPrevConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
if gui.State.Panels.Merging.ConflictIndex <= 0 {
return nil
}
gui.State.Panels.Merging.ConflictIndex--
return gui.refreshMergePanel()
})
}
func (gui *Gui) pushFileSnapshot() error {
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
content, err := gui.GitCommand.CatFile(gitFile.Name)
if err != nil {
return err
}
gui.State.Panels.Merging.EditHistory.Push(content)
return nil
}
func (gui *Gui) handlePopFileSnapshot() error {
if gui.State.Panels.Merging.EditHistory.Len() == 0 {
return nil
}
prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string)
gitFile := gui.getSelectedFile()
if gitFile == nil {
return nil
}
gui.OnRunCommand(oscommands.NewCmdLogEntry("Undoing last conflict resolution", "Undo merge conflict resolution", false))
if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
return err
}
return gui.refreshMergePanel()
}
func (gui *Gui) handlePickHunk() error {
return gui.withMergeConflictLock(func() error {
conflict := gui.getCurrentConflict()
if conflict == nil {
return nil
}
gui.takeOverMergeConflictScrolling()
if err := gui.pushFileSnapshot(); err != nil {
return err
}
selection := mergeconflicts.BOTTOM
if gui.State.Panels.Merging.ConflictTop {
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 len(gui.State.Panels.Merging.Conflicts) == 1 {
if err := gui.handleCompleteMerge(); err != nil {
return err
}
}
return gui.refreshMergePanel()
})
}
func (gui *Gui) handlePickBothHunks() error {
return gui.withMergeConflictLock(func() error {
conflict := gui.getCurrentConflict()
if conflict == 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()
})
}
func (gui *Gui) getCurrentConflict() *commands.Conflict {
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()
if gitFile == nil {
return nil
}
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 !mergeconflicts.IsIndexToDelete(i, conflict, selection) {
output += line
}
}
var logStr string
switch selection {
case mergeconflicts.TOP:
logStr = "Picking top hunk"
case mergeconflicts.BOTTOM:
logStr = "Picking bottom hunk"
case mergeconflicts.BOTH:
logStr = "Picking both hunks"
}
gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false))
return ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
}
func (gui *Gui) refreshMergePanelWithLock() error {
return gui.withMergeConflictLock(gui.refreshMergePanel)
}
func (gui *Gui) refreshMergePanel() error {
panelState := gui.State.Panels.Merging
cat, err := gui.catSelectedFile()
if err != nil {
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: "",
task: NewRenderStringTask(err.Error()),
},
})
}
panelState.Conflicts = mergeconflicts.FindConflicts(cat)
// 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
}
hasFocus := gui.currentViewName() == "main"
content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus)
if err := gui.scrollToConflict(); err != nil {
return err
}
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: gui.Tr.MergeConflictsTitle,
task: NewRenderStringWithoutScrollTask(content),
noWrap: true,
},
})
}
func (gui *Gui) catSelectedFile() (string, error) {
item := gui.getSelectedFile()
if item == nil {
return "", errors.New(gui.Tr.NoFilesDisplay)
}
if item.Type != "file" {
return "", errors.New(gui.Tr.NotAFile)
}
cat, err := gui.GitCommand.CatFile(item.Name)
if err != nil {
gui.Log.Error(err)
return "", err
}
return cat, nil
}
func (gui *Gui) scrollToConflict() error {
if gui.State.Panels.Merging.UserScrolling {
return nil
}
panelState := gui.State.Panels.Merging
if len(panelState.Conflicts) == 0 {
return nil
}
mergingView := gui.Views.Main
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
}
func (gui *Gui) getMergingOptions() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
return map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcSelectHunk,
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevBlock), gui.getKeyDisplay(keybindingConfig.Universal.NextBlock)): gui.Tr.LcNavigateConflicts,
gui.getKeyDisplay(keybindingConfig.Universal.Select): gui.Tr.LcPickHunk,
gui.getKeyDisplay(keybindingConfig.Main.PickBothHunks): gui.Tr.LcPickBothHunks,
gui.getKeyDisplay(keybindingConfig.Universal.Undo): gui.Tr.LcUndo,
}
}
func (gui *Gui) handleEscapeMerge() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.EditHistory = stack.New()
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
// 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.Views.Main {
return gui.pushContext(gui.State.Contexts.Files)
}
return nil
}
func (gui *Gui) handleCompleteMerge() error {
if err := gui.stageSelectedFile(); err != nil {
return err
}
if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil {
return err
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue
if !gui.anyFilesWithMergeConflicts() {
return gui.promptToContinueRebase()
}
return gui.handleEscapeMerge()
}
// promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress
func (gui *Gui) promptToContinueRebase() error {
gui.takeOverMergeConflictScrolling()
return gui.ask(askOpts{
title: "continue",
prompt: gui.Tr.ConflictsResolved,
handlersManageFocus: true,
handleConfirm: func() error {
if err := gui.pushContext(gui.State.Contexts.Files); err != nil {
return err
}
return gui.genericMergeCommand("continue")
},
handleClose: func() error {
return gui.pushContext(gui.State.Contexts.Files)
},
})
}
func (gui *Gui) canScrollMergePanel() bool {
currentView := gui.g.CurrentView()
if currentView != gui.Views.Main && currentView != gui.Views.Files {
return false
}
file := gui.getSelectedFile()
if file == nil {
return false
}
return file.HasInlineMergeConflicts
}
func (gui *Gui) withMergeConflictLock(f func() error) error {
gui.State.Panels.Merging.ConflictsMutex.Lock()
defer gui.State.Panels.Merging.ConflictsMutex.Unlock()
return f()
}
func (gui *Gui) takeOverMergeConflictScrolling() {
gui.State.Panels.Merging.UserScrolling = false
}