diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go deleted file mode 100644 index 1590ce32e..000000000 --- a/pkg/commands/git_structs.go +++ /dev/null @@ -1,9 +0,0 @@ -package commands - -// Conflict : A git conflict with a start middle and end corresponding to line -// numbers in the file where the conflict bars appear -type Conflict struct { - Start int - Middle int - End int -} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index de3b319c0..d17384fa4 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -13,7 +13,6 @@ import ( "time" "github.com/fatih/color" - "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/models" @@ -21,6 +20,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/filetree" "github.com/jesseduffield/lazygit/pkg/gui/lbl" + "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/modes/filtering" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" @@ -138,12 +138,8 @@ type LblPanelState struct { SecondaryFocused bool // this is for if we show the left or right panel } -type mergingPanelState struct { - ConflictIndex int - ConflictTop bool - Conflicts []commands.Conflict - ConflictsMutex sync.Mutex - EditHistory *stack.Stack +type MergingPanelState struct { + *mergeconflicts.State // UserScrolling tells us if the user has started scrolling through the file themselves // in which case we won't auto-scroll to a conflict. @@ -226,7 +222,7 @@ type panelStates struct { Stash *stashPanelState Menu *menuPanelState LineByLine *LblPanelState - Merging *mergingPanelState + Merging *MergingPanelState CommitFiles *commitFilesPanelState Submodules *submodulePanelState Suggestions *suggestionsPanelState @@ -417,12 +413,9 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) { Stash: &stashPanelState{listPanelState{SelectedLineIdx: -1}}, Menu: &menuPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}, OnPress: nil}, Suggestions: &suggestionsPanelState{listPanelState: listPanelState{SelectedLineIdx: 0}}, - Merging: &mergingPanelState{ - ConflictIndex: 0, - ConflictTop: true, - Conflicts: []commands.Conflict{}, - EditHistory: stack.New(), - ConflictsMutex: sync.Mutex{}, + Merging: &MergingPanelState{ + State: mergeconflicts.NewState(), + UserScrolling: false, }, }, Ptmx: nil, diff --git a/pkg/gui/lbl/line_by_line.go b/pkg/gui/lbl/state.go similarity index 100% rename from pkg/gui/lbl/line_by_line.go rename to pkg/gui/lbl/state.go diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go index 0b4e30903..51376ccf8 100644 --- a/pkg/gui/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -3,14 +3,11 @@ 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" @@ -20,7 +17,7 @@ import ( func (gui *Gui) handleSelectTop() error { return gui.withMergeConflictLock(func() error { gui.takeOverMergeConflictScrolling() - gui.State.Panels.Merging.ConflictTop = true + gui.State.Panels.Merging.SelectTopOption() return gui.refreshMergePanel() }) } @@ -28,7 +25,7 @@ func (gui *Gui) handleSelectTop() error { func (gui *Gui) handleSelectBottom() error { return gui.withMergeConflictLock(func() error { gui.takeOverMergeConflictScrolling() - gui.State.Panels.Merging.ConflictTop = false + gui.State.Panels.Merging.SelectBottomOption() return gui.refreshMergePanel() }) } @@ -36,10 +33,7 @@ func (gui *Gui) handleSelectBottom() error { 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++ + gui.State.Panels.Merging.SelectNextConflict() return gui.refreshMergePanel() }) } @@ -47,32 +41,26 @@ func (gui *Gui) handleSelectNextConflict() error { 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-- + gui.State.Panels.Merging.SelectPrevConflict() return gui.refreshMergePanel() }) } func (gui *Gui) pushFileSnapshot() error { - gitFile := gui.getSelectedFile() - if gitFile == nil { - return nil - } - content, err := gui.GitCommand.CatFile(gitFile.Name) + content, err := gui.catSelectedFile() if err != nil { return err } - gui.State.Panels.Merging.EditHistory.Push(content) + gui.State.Panels.Merging.PushFileSnapshot(content) return nil } func (gui *Gui) handlePopFileSnapshot() error { - if gui.State.Panels.Merging.EditHistory.Len() == 0 { + prevContent, ok := gui.State.Panels.Merging.PopFileSnapshot() + if !ok { return nil } - prevContent := gui.State.Panels.Merging.EditHistory.Pop().(string) + gitFile := gui.getSelectedFile() if gitFile == nil { return nil @@ -87,28 +75,18 @@ func (gui *Gui) handlePopFileSnapshot() error { 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 { + ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection()) + if 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 !ok { + return nil } - // if that was the last conflict, finish the merge for this file - if len(gui.State.Panels.Merging.Conflicts) == 1 { + if gui.State.Panels.Merging.IsFinalConflict() { if err := gui.handleCompleteMerge(); err != nil { return err } @@ -119,53 +97,38 @@ func (gui *Gui) handlePickHunk() error { func (gui *Gui) handlePickBothHunks() error { return gui.withMergeConflictLock(func() error { - conflict := gui.getCurrentConflict() - if conflict == nil { + gui.takeOverMergeConflictScrolling() + + ok, err := gui.resolveConflict(mergeconflicts.BOTH) + if err != nil { + return err + } + + if !ok { 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 { +func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) { gitFile := gui.getSelectedFile() if gitFile == nil { - return nil + return false, 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 - } + ok, output, err := gui.State.Panels.Merging.ContentAfterConflictResolve(gitFile.Name, selection) + if err != nil { + return false, err + } + + if !ok { + return false, nil + } + + if err := gui.pushFileSnapshot(); err != nil { + return false, gui.surfaceError(err) } var logStr string @@ -178,7 +141,7 @@ func (gui *Gui) resolveConflict(conflict commands.Conflict, selection mergeconfl logStr = "Picking both hunks" } gui.OnRunCommand(oscommands.NewCmdLogEntry(logStr, "Resolve merge conflict", false)) - return ioutil.WriteFile(gitFile.Name, []byte(output), 0644) + return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644) } func (gui *Gui) refreshMergePanelWithLock() error { @@ -197,17 +160,14 @@ func (gui *Gui) refreshMergePanel() error { }) } - panelState.Conflicts = mergeconflicts.FindConflicts(cat) + panelState.SetConflictsFromCat(cat) - // handle potential fixes that the user made in their editor since we last refreshed - if len(panelState.Conflicts) == 0 { + if panelState.NoConflicts() { 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) + content := mergeconflicts.ColoredConflictFile(cat, panelState.State, hasFocus) if err := gui.scrollToConflict(); err != nil { return err @@ -246,21 +206,24 @@ func (gui *Gui) scrollToConflict() error { } panelState := gui.State.Panels.Merging - if len(panelState.Conflicts) == 0 { + if panelState.NoConflicts() { 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) - }) + gui.centerYPos(gui.Views.Main, panelState.GetConflictMiddle()) + return nil } +func (gui *Gui) centerYPos(view *gocui.View, y int) { + ox, _ := view.Origin() + _, height := view.Size() + newOriginY := int(math.Max(0, float64(y-(height/2)))) + gui.g.Update(func(g *gocui.Gui) error { + return view.SetOrigin(ox, newOriginY) + }) +} + func (gui *Gui) getMergingOptions() map[string]string { keybindingConfig := gui.Config.GetUserConfig().Keybinding @@ -276,7 +239,7 @@ func (gui *Gui) getMergingOptions() map[string]string { func (gui *Gui) handleEscapeMerge() error { gui.takeOverMergeConflictScrolling() - gui.State.Panels.Merging.EditHistory = stack.New() + gui.State.Panels.Merging.Reset() if err := gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{FILES}}); err != nil { return err } @@ -343,8 +306,8 @@ func (gui *Gui) canScrollMergePanel() bool { } func (gui *Gui) withMergeConflictLock(f func() error) error { - gui.State.Panels.Merging.ConflictsMutex.Lock() - defer gui.State.Panels.Merging.ConflictsMutex.Unlock() + gui.State.Panels.Merging.Lock() + defer gui.State.Panels.Merging.Unlock() return f() } diff --git a/pkg/gui/mergeconflicts/merge_conflicts.go b/pkg/gui/mergeconflicts/merge_conflicts.go deleted file mode 100644 index fe3bdfbe3..000000000 --- a/pkg/gui/mergeconflicts/merge_conflicts.go +++ /dev/null @@ -1,86 +0,0 @@ -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) -} diff --git a/pkg/gui/mergeconflicts/rendering.go b/pkg/gui/mergeconflicts/rendering.go new file mode 100644 index 000000000..5de6eda93 --- /dev/null +++ b/pkg/gui/mergeconflicts/rendering.go @@ -0,0 +1,41 @@ +package mergeconflicts + +import ( + "bytes" + + "github.com/fatih/color" + "github.com/jesseduffield/lazygit/pkg/theme" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +func ColoredConflictFile(content string, state *State, hasFocus bool) string { + if len(state.conflicts) == 0 { + return content + } + conflict, remainingConflicts := shiftConflict(state.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 && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.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 shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict) { + return conflicts[0], conflicts[1:] +} + +func shouldHighlightLine(index int, conflict *mergeConflict, top bool) bool { + return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top) +} diff --git a/pkg/gui/mergeconflicts/state.go b/pkg/gui/mergeconflicts/state.go new file mode 100644 index 000000000..17ac02a0b --- /dev/null +++ b/pkg/gui/mergeconflicts/state.go @@ -0,0 +1,185 @@ +package mergeconflicts + +import ( + "bufio" + "os" + "strings" + "sync" + + "github.com/golang-collections/collections/stack" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type Selection int + +const ( + TOP Selection = iota + BOTTOM + BOTH +) + +// mergeConflict : A git conflict with a start middle and end corresponding to line +// numbers in the file where the conflict bars appear +type mergeConflict struct { + start int + middle int + end int +} + +type State struct { + sync.Mutex + conflictIndex int + conflictTop bool + conflicts []*mergeConflict + EditHistory *stack.Stack +} + +func NewState() *State { + return &State{ + Mutex: sync.Mutex{}, + conflictIndex: 0, + conflictTop: true, + conflicts: []*mergeConflict{}, + EditHistory: stack.New(), + } +} + +func (s *State) SelectTopOption() { + s.conflictTop = true +} + +func (s *State) SelectBottomOption() { + s.conflictTop = false +} + +func (s *State) SelectNextConflict() { + if s.conflictIndex < len(s.conflicts)-1 { + s.conflictIndex++ + } +} + +func (s *State) SelectPrevConflict() { + if s.conflictIndex > 0 { + s.conflictIndex-- + } +} + +func (s *State) PushFileSnapshot(content string) { + s.EditHistory.Push(content) +} + +func (s *State) PopFileSnapshot() (string, bool) { + if s.EditHistory.Len() == 0 { + return "", false + } + + return s.EditHistory.Pop().(string), true +} + +func (s *State) currentConflict() *mergeConflict { + if len(s.conflicts) == 0 { + return nil + } + + return s.conflicts[s.conflictIndex] +} + +func (s *State) SetConflictsFromCat(cat string) { + s.setConflicts(findConflicts(cat)) +} + +func findConflicts(content string) []*mergeConflict { + conflicts := make([]*mergeConflict, 0) + + if content == "" { + return conflicts + } + + var newConflict *mergeConflict + for i, line := range utils.SplitLines(content) { + trimmedLine := strings.TrimPrefix(line, "++") + switch trimmedLine { + case "<<<<<<< HEAD", "<<<<<<< MERGE_HEAD", "<<<<<<< Updated upstream", "<<<<<<< ours": + newConflict = &mergeConflict{start: i} + case "=======": + newConflict.middle = i + default: + if strings.HasPrefix(trimmedLine, ">>>>>>> ") { + newConflict.end = i + conflicts = append(conflicts, newConflict) + } + } + + } + return conflicts +} + +func (s *State) setConflicts(conflicts []*mergeConflict) { + s.conflicts = conflicts + + if s.conflictIndex > len(s.conflicts)-1 { + s.conflictIndex = len(s.conflicts) - 1 + } else if s.conflictIndex < 0 { + s.conflictIndex = 0 + } +} + +func (s *State) NoConflicts() bool { + return len(s.conflicts) == 0 +} + +func (s *State) Selection() Selection { + if s.conflictTop { + return TOP + } else { + return BOTTOM + } +} + +func (s *State) IsFinalConflict() bool { + return len(s.conflicts) == 1 +} + +func (s *State) Reset() { + s.EditHistory = stack.New() +} + +func (s *State) GetConflictMiddle() int { + return s.currentConflict().middle +} + +func (s *State) ContentAfterConflictResolve(path string, selection Selection) (bool, string, error) { + conflict := s.currentConflict() + if conflict == nil { + return false, "", nil + } + + file, err := os.Open(path) + if err != nil { + return false, "", err + } + defer file.Close() + + reader := bufio.NewReader(file) + content := "" + for i := 0; true; i++ { + line, err := reader.ReadString('\n') + if err != nil { + break + } + if !isIndexToDelete(i, conflict, selection) { + content += line + } + } + + return true, content, nil +} + +func isIndexToDelete(i int, conflict *mergeConflict, 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) +}