From eeeef9ca863e529f4442dd780a462a577ff251cc Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 2 Apr 2021 00:48:13 +1100 Subject: [PATCH] refactor --- pkg/gui/keybindings.go | 26 +- pkg/gui/merge_panel.go | 309 +++++++++------------- pkg/gui/mergeconflicts/merge_conflicts.go | 86 ++++++ 3 files changed, 227 insertions(+), 194 deletions(-) create mode 100644 pkg/gui/mergeconflicts/merge_conflicts.go diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 493b94396..a2c2cd9b5 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -1410,42 +1410,42 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.Select), - Handler: gui.handlePickHunk, + Handler: gui.wrappedHandler(gui.handlePickHunk), Description: gui.Tr.PickHunk, }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Main.PickBothHunks), - Handler: gui.handlePickBothHunks, + Handler: gui.wrappedHandler(gui.handlePickBothHunks), Description: gui.Tr.PickBothHunks, }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.PrevBlock), - Handler: gui.handleSelectPrevConflict, + Handler: gui.wrappedHandler(gui.handleSelectPrevConflict), Description: gui.Tr.PrevConflict, }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.NextBlock), - Handler: gui.handleSelectNextConflict, + Handler: gui.wrappedHandler(gui.handleSelectNextConflict), Description: gui.Tr.NextConflict, }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.PrevItem), - Handler: gui.handleSelectTop, + Handler: gui.wrappedHandler(gui.handleSelectTop), Description: gui.Tr.SelectTop, }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.NextItem), - Handler: gui.handleSelectBottom, + Handler: gui.wrappedHandler(gui.handleSelectBottom), Description: gui.Tr.SelectBottom, }, { @@ -1453,48 +1453,48 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gocui.MouseWheelUp, Modifier: gocui.ModNone, - Handler: gui.handleSelectTop, + Handler: gui.wrappedHandler(gui.handleSelectTop), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gocui.MouseWheelDown, Modifier: gocui.ModNone, - Handler: gui.handleSelectBottom, + Handler: gui.wrappedHandler(gui.handleSelectBottom), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.PrevBlockAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectPrevConflict, + Handler: gui.wrappedHandler(gui.handleSelectPrevConflict), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.NextBlockAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectNextConflict, + Handler: gui.wrappedHandler(gui.handleSelectNextConflict), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.PrevItemAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectTop, + Handler: gui.wrappedHandler(gui.handleSelectTop), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.NextItemAlt), Modifier: gocui.ModNone, - Handler: gui.handleSelectBottom, + Handler: gui.wrappedHandler(gui.handleSelectBottom), }, { ViewName: "main", Contexts: []string{MAIN_MERGING_CONTEXT_KEY}, Key: gui.getKey(config.Universal.Undo), - Handler: gui.handlePopFileSnapshot, + Handler: gui.wrappedHandler(gui.handlePopFileSnapshot), Description: gui.Tr.LcUndo, }, { diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 2bd1eb6e3..a73ec1727 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -4,132 +4,146 @@ package gui import ( "bufio" - "bytes" "fmt" "io/ioutil" "math" "os" - "strings" - "github.com/fatih/color" "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/theme" - "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" ) -func (gui *Gui) findConflicts(content string) []commands.Conflict { - conflicts := make([]commands.Conflict, 0) +func (gui *Gui) handleSelectTop() error { + return gui.withMergeConflictLock(func() error { + gui.takeOverScrolling() + gui.State.Panels.Merging.ConflictTop = true + return gui.refreshMergePanel() + }) +} - if content == "" { - return conflicts - } +func (gui *Gui) handleSelectBottom() error { + return gui.withMergeConflictLock(func() error { + gui.takeOverScrolling() + gui.State.Panels.Merging.ConflictTop = false + return gui.refreshMergePanel() + }) +} - var newConflict commands.Conflict - for i, line := range utils.SplitLines(content) { - trimmedLine := strings.TrimPrefix(line, "++") - if trimmedLine == "<<<<<<< HEAD" || trimmedLine == "<<<<<<< MERGE_HEAD" || trimmedLine == "<<<<<<< Updated upstream" || trimmedLine == "<<<<<<< ours" { - newConflict = commands.Conflict{Start: i} - } else if trimmedLine == "=======" { - newConflict.Middle = i - } else if strings.HasPrefix(trimmedLine, ">>>>>>> ") { - newConflict.End = i - conflicts = append(conflicts, newConflict) +func (gui *Gui) handleSelectNextConflict() error { + return gui.withMergeConflictLock(func() error { + gui.takeOverScrolling() + if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 { + return nil } - } - return conflicts + gui.State.Panels.Merging.ConflictIndex++ + return gui.refreshMergePanel() + }) } -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 { - if len(conflicts) == 0 { - return content - } - conflict, remainingConflicts := gui.shiftConflict(conflicts) - var outputBuffer bytes.Buffer - for i, line := range utils.SplitLines(content) { - colourAttr := theme.DefaultTextColor - if i == conflict.Start || i == conflict.Middle || i == conflict.End { - colourAttr = color.FgRed +func (gui *Gui) handleSelectPrevConflict() error { + return gui.withMergeConflictLock(func() error { + gui.takeOverScrolling() + if gui.State.Panels.Merging.ConflictIndex <= 0 { + return nil } - 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) - } - if i == conflict.End && len(remainingConflicts) > 0 { - conflict, remainingConflicts = gui.shiftConflict(remainingConflicts) - } - outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n") - } - return outputBuffer.String() + gui.State.Panels.Merging.ConflictIndex-- + return gui.refreshMergePanel() + }) } -func (gui *Gui) takeOverScrolling() { - gui.State.Panels.Merging.UserScrolling = false -} - -func (gui *Gui) handleSelectTop(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - gui.State.Panels.Merging.ConflictTop = true - return gui.refreshMergePanel() -} - -func (gui *Gui) handleSelectBottom(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - gui.State.Panels.Merging.ConflictTop = false - return gui.refreshMergePanel() -} - -func (gui *Gui) handleSelectNextConflict(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - if gui.State.Panels.Merging.ConflictIndex >= len(gui.State.Panels.Merging.Conflicts)-1 { +func (gui *Gui) pushFileSnapshot() error { + gitFile := gui.getSelectedFile() + if gitFile == nil { return nil } - gui.State.Panels.Merging.ConflictIndex++ - return gui.refreshMergePanel() + content, err := gui.GitCommand.CatFile(gitFile.Name) + if err != nil { + return err + } + gui.State.Panels.Merging.EditHistory.Push(content) + return nil } -func (gui *Gui) handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - if gui.State.Panels.Merging.ConflictIndex <= 0 { +func (gui *Gui) handlePopFileSnapshot() error { + if gui.State.Panels.Merging.EditHistory.Len() == 0 { return nil } - gui.State.Panels.Merging.ConflictIndex-- + prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string) + gitFile := gui.getSelectedFile() + if gitFile == nil { + return nil + } + if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil { + return err + } + return gui.refreshMergePanel() } -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) handlePickHunk() error { + return gui.withMergeConflictLock(func() error { + conflict := gui.getCurrentConflict() + if conflict == nil { + return nil + } + + gui.takeOverScrolling() + + 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) resolveConflict(conflict commands.Conflict, pick string) error { +func (gui *Gui) handlePickBothHunks() error { + return gui.withMergeConflictLock(func() error { + conflict := gui.getCurrentConflict() + if conflict == nil { + return nil + } + + gui.takeOverScrolling() + + 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 @@ -147,93 +161,15 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, pick string) error { if err != nil { break } - if !gui.isIndexToDelete(i, conflict, pick) { + if !mergeconflicts.IsIndexToDelete(i, conflict, selection) { output += line } } return ioutil.WriteFile(gitFile.Name, []byte(output), 0644) } -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(g *gocui.Gui, v *gocui.View) 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 - } - if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil { - return err - } - - return gui.refreshMergePanel() -} - -func (gui *Gui) handlePickHunk(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - - conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex] - if err := gui.pushFileSnapshot(); err != nil { - return err - } - - pick := "bottom" - if gui.State.Panels.Merging.ConflictTop { - pick = "top" - } - err := gui.resolveConflict(conflict, pick) - 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(g *gocui.Gui, v *gocui.View) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - gui.takeOverScrolling() - - conflict := gui.State.Panels.Merging.Conflicts[gui.State.Panels.Merging.ConflictIndex] - if err := gui.pushFileSnapshot(); err != nil { - return err - } - err := gui.resolveConflict(conflict, "both") - if err != nil { - panic(err) - } - return gui.refreshMergePanel() -} - func (gui *Gui) refreshMergePanelWithLock() error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() - - return gui.refreshMergePanel() + return gui.withMergeConflictLock(gui.refreshMergePanel) } func (gui *Gui) refreshMergePanel() error { @@ -248,7 +184,7 @@ func (gui *Gui) refreshMergePanel() error { }) } - panelState.Conflicts = gui.findConflicts(cat) + panelState.Conflicts = mergeconflicts.FindConflicts(cat) // handle potential fixes that the user made in their editor since we last refreshed if len(panelState.Conflicts) == 0 { @@ -258,7 +194,7 @@ func (gui *Gui) refreshMergePanel() error { } hasFocus := gui.currentViewName() == "main" - content := gui.coloredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus) + content := mergeconflicts.ColoredConflictFile(cat, panelState.Conflicts, panelState.ConflictIndex, panelState.ConflictTop, hasFocus) if err := gui.scrollToConflict(); err != nil { return err @@ -353,13 +289,13 @@ func (gui *Gui) handleCompleteMerge() error { } // if there are no more files with merge conflicts, we should ask whether the user wants to continue if !gui.anyFilesWithMergeConflicts() { - return gui.promptToContinue() + return gui.promptToContinueRebase() } return gui.handleEscapeMerge() } -// promptToContinue asks the user if they want to continue the rebase/merge that's in progress -func (gui *Gui) promptToContinue() error { +// promptToContinueRebase asks the user if they want to continue the rebase/merge that's in progress +func (gui *Gui) promptToContinueRebase() error { gui.takeOverScrolling() return gui.ask(askOpts{ @@ -392,3 +328,14 @@ func (gui *Gui) canScrollMergePanel() bool { 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) takeOverScrolling() { + gui.State.Panels.Merging.UserScrolling = false +} diff --git a/pkg/gui/mergeconflicts/merge_conflicts.go b/pkg/gui/mergeconflicts/merge_conflicts.go new file mode 100644 index 000000000..fe3bdfbe3 --- /dev/null +++ b/pkg/gui/mergeconflicts/merge_conflicts.go @@ -0,0 +1,86 @@ +package mergeconflicts + +import ( + "bytes" + "strings" + + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/theme" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type Selection int + +const ( + TOP Selection = iota + BOTTOM + BOTH +) + +func FindConflicts(content string) []commands.Conflict { + conflicts := make([]commands.Conflict, 0) + + if content == "" { + return conflicts + } + + var newConflict commands.Conflict + for i, line := range utils.SplitLines(content) { + trimmedLine := strings.TrimPrefix(line, "++") + switch trimmedLine { + case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours": + newConflict = commands.Conflict{Start: i} + case "=======": + newConflict.Middle = i + default: + if strings.HasPrefix(trimmedLine, ">>>>>>> ") { + newConflict.End = i + conflicts = append(conflicts, newConflict) + } + } + + } + return conflicts +} + +func ColoredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) string { + if len(conflicts) == 0 { + return content + } + conflict, remainingConflicts := shiftConflict(conflicts) + var outputBuffer bytes.Buffer + for i, line := range utils.SplitLines(content) { + colourAttr := theme.DefaultTextColor + 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) + colour.Add(theme.SelectedRangeBgColor) + } + if i == conflict.End && len(remainingConflicts) > 0 { + conflict, remainingConflicts = shiftConflict(remainingConflicts) + } + outputBuffer.WriteString(utils.ColoredStringDirect(line, colour) + "\n") + } + return outputBuffer.String() +} + +func IsIndexToDelete(i int, conflict commands.Conflict, selection Selection) bool { + return i == conflict.Middle || + i == conflict.Start || + i == conflict.End || + selection != BOTH && + (selection == BOTTOM && i > conflict.Start && i < conflict.Middle) || + (selection == TOP && i > conflict.Middle && i < conflict.End) +} + +func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { + return conflicts[0], conflicts[1:] +} + +func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool { + return (index >= conflict.Start && index <= conflict.Middle && top) || (index >= conflict.Middle && index <= conflict.End && !top) +}