summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2022-01-26 01:20:19 +1100
committerJesse Duffield <jessedduffield@gmail.com>2022-01-26 14:50:47 +1100
commitc8cc18920f0b5ab54a19b9f7bdcf83db3210576f (patch)
tree60623fbe5504469b9a4f4ca25a9a538b4eee52ce
parentce3bcfe37cf0c68f501fb09d543e9e212b0eaa61 (diff)
improve merge conflict flow
-rw-r--r--pkg/commands/git_commands/working_tree.go13
-rw-r--r--pkg/commands/git_commands/working_tree_test.go10
-rw-r--r--pkg/commands/loaders/files.go2
-rw-r--r--pkg/gui/context_config.go2
-rw-r--r--pkg/gui/files_panel.go133
-rw-r--r--pkg/gui/filetree/file_node_test.go29
-rw-r--r--pkg/gui/filetree/file_tree_view_model.go10
-rw-r--r--pkg/gui/global_handlers.go4
-rw-r--r--pkg/gui/gui.go24
-rw-r--r--pkg/gui/keybindings.go16
-rw-r--r--pkg/gui/merge_panel.go200
-rw-r--r--pkg/gui/mergeconflicts/find_conflicts.go48
-rw-r--r--pkg/gui/mergeconflicts/find_conflicts_test.go40
-rw-r--r--pkg/gui/mergeconflicts/rendering.go3
-rw-r--r--pkg/gui/mergeconflicts/state.go83
-rw-r--r--pkg/i18n/english.go2
-rw-r--r--test/integration/mergeConflicts/config/config.yml5
17 files changed, 394 insertions, 230 deletions
diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go
index 4e97913fb..f594a639b 100644
--- a/pkg/commands/git_commands/working_tree.go
+++ b/pkg/commands/git_commands/working_tree.go
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"time"
"github.com/go-errors/errors"
@@ -40,8 +41,16 @@ func (self *WorkingTreeCommands) OpenMergeTool() error {
}
// StageFile stages a file
-func (self *WorkingTreeCommands) StageFile(fileName string) error {
- return self.cmd.New("git add -- " + self.cmd.Quote(fileName)).Run()
+func (self *WorkingTreeCommands) StageFile(path string) error {
+ return self.StageFiles([]string{path})
+}
+
+func (self *WorkingTreeCommands) StageFiles(paths []string) error {
+ quotedPaths := make([]string, len(paths))
+ for i, path := range paths {
+ quotedPaths[i] = self.cmd.Quote(path)
+ }
+ return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
}
// StageAll stages all files
diff --git a/pkg/commands/git_commands/working_tree_test.go b/pkg/commands/git_commands/working_tree_test.go
index 8b2c70714..e4e884ce4 100644
--- a/pkg/commands/git_commands/working_tree_test.go
+++ b/pkg/commands/git_commands/working_tree_test.go
@@ -23,6 +23,16 @@ func TestWorkingTreeStageFile(t *testing.T) {
runner.CheckForMissingCalls()
}
+func TestWorkingTreeStageFiles(t *testing.T) {
+ runner := oscommands.NewFakeRunner(t).
+ Expect(`git add -- "test.txt" "test2.txt"`, "", nil)
+
+ instance := buildWorkingTreeCommands(commonDeps{runner: runner})
+
+ assert.NoError(t, instance.StageFiles([]string{"test.txt", "test2.txt"}))
+ runner.CheckForMissingCalls()
+}
+
func TestWorkingTreeUnstageFile(t *testing.T) {
type scenario struct {
testName string
diff --git a/pkg/commands/loaders/files.go b/pkg/commands/loaders/files.go
index 1e056cb98..f5becdb92 100644
--- a/pkg/commands/loaders/files.go
+++ b/pkg/commands/loaders/files.go
@@ -59,8 +59,8 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
unstagedChange := change[1:2]
untracked := utils.IncludesString([]string{"??", "A ", "AM"}, change)
hasNoStagedChanges := utils.IncludesString([]string{" ", "U", "?"}, stagedChange)
- hasMergeConflicts := utils.IncludesString([]string{"DD", "AA", "UU", "AU", "UA", "UD", "DU"}, change)
hasInlineMergeConflicts := utils.IncludesString([]string{"UU", "AA"}, change)
+ hasMergeConflicts := hasInlineMergeConflicts || utils.IncludesString([]string{"DD", "AU", "UA", "UD", "DU"}, change)
file := &models.File{
Name: status.Name,
diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go
index 2b587a62e..2a152e517 100644
--- a/pkg/gui/context_config.go
+++ b/pkg/gui/context_config.go
@@ -167,7 +167,7 @@ func (gui *Gui) contextTree() ContextTree {
Key: MAIN_PATCH_BUILDING_CONTEXT_KEY,
},
Merging: &BasicContext{
- OnFocus: OnFocusWrapper(gui.refreshMergePanelWithLock),
+ OnFocus: OnFocusWrapper(gui.renderConflictsWithFocus),
Kind: MAIN_CONTEXT,
ViewName: "main",
Key: MAIN_MERGING_CONTEXT_KEY,
diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go
index c62da8e21..7b8e8d3de 100644
--- a/pkg/gui/files_panel.go
+++ b/pkg/gui/files_panel.go
@@ -8,8 +8,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
+ "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
+ "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -54,9 +56,19 @@ func (gui *Gui) filesRenderToMain() error {
}
if node.File != nil && node.File.HasInlineMergeConflicts {
- return gui.renderConflictsFromFilesPanel()
+ hasConflicts, err := gui.setMergeState(node.File.Name)
+ if err != nil {
+ return err
+ }
+
+ // if we don't have conflicts we'll fall through and show the diff
+ if hasConflicts {
+ return gui.renderConflicts(false)
+ }
}
+ gui.resetMergeState()
+
cmdObj := gui.Git.WorkingTree.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
@@ -88,11 +100,16 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
gui.Mutexes.RefreshingFilesMutex.Unlock()
}()
- selectedPath := gui.getSelectedPath()
+ prevSelectedPath := gui.getSelectedPath()
if err := gui.refreshStateSubmoduleConfigs(); err != nil {
return err
}
+
+ if err := gui.refreshMergeState(); err != nil {
+ return err
+ }
+
if err := gui.refreshStateFiles(); err != nil {
return err
}
@@ -109,9 +126,9 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
}
}
- if gui.currentContext().GetKey() == FILES_CONTEXT_KEY || (gui.g.CurrentView() == gui.Views.Main && ContextKey(gui.g.CurrentView().Context) == MAIN_MERGING_CONTEXT_KEY) {
- newSelectedPath := gui.getSelectedPath()
- alreadySelected := selectedPath != "" && newSelectedPath == selectedPath
+ if gui.currentContext().GetKey() == FILES_CONTEXT_KEY {
+ currentSelectedPath := gui.getSelectedPath()
+ alreadySelected := prevSelectedPath != "" && currentSelectedPath == prevSelectedPath
if !alreadySelected {
gui.takeOverMergeConflictScrolling()
}
@@ -126,6 +143,28 @@ func (gui *Gui) refreshFilesAndSubmodules() error {
return nil
}
+func (gui *Gui) refreshMergeState() error {
+ gui.State.Panels.Merging.Lock()
+ defer gui.State.Panels.Merging.Unlock()
+
+ if gui.currentContext().GetKey() != MAIN_MERGING_CONTEXT_KEY {
+ return nil
+ }
+
+ hasConflicts, err := gui.setMergeState(gui.State.Panels.Merging.GetPath())
+ if err != nil {
+ return gui.surfaceError(err)
+ }
+ if hasConflicts {
+ _ = gui.renderConflicts(true)
+ } else {
+ _ = gui.escapeMerge()
+ return nil
+ }
+
+ return nil
+}
+
// specific functions
func (gui *Gui) stagedFiles() []*models.File {
@@ -150,15 +189,6 @@ func (gui *Gui) trackedFiles() []*models.File {
return result
}
-func (gui *Gui) stageSelectedFile() error {
- file := gui.getSelectedFile()
- if file == nil {
- return nil
- }
-
- return gui.Git.WorkingTree.StageFile(file.Name)
-}
-
func (gui *Gui) handleEnterFile() error {
return gui.enterFile(OnFocusOpts{ClickedViewName: "", ClickedViewLineIdx: -1})
}
@@ -558,9 +588,50 @@ func (gui *Gui) refreshStateFiles() error {
prevNodes := gui.State.FileTreeViewModel.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
+ // If git thinks any of our files have inline merge conflicts, but they actually don't,
+ // we stage them.
+ // Note that if files with merge conflicts have both arisen and have been resolved
+ // between refreshes, we won't stage them here. This is super unlikely though,
+ // and this approach spares us from having to call `git status` twice in a row.
+ // Although this also means that at startup we won't be staging anything until
+ // we call git status again.
+ pathsToStage := []string{}
+ prevConflictFileCount := 0
+ for _, file := range state.FileTreeViewModel.GetAllFiles() {
+ if file.HasMergeConflicts {
+ prevConflictFileCount++
+ }
+ if file.HasInlineMergeConflicts {
+ hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name)
+ if err != nil {
+ gui.Log.Error(err)
+ } else if !hasConflicts {
+ pathsToStage = append(pathsToStage, file.Name)
+ }
+ }
+ }
+
+ if len(pathsToStage) > 0 {
+ gui.logAction(gui.Tr.Actions.StageResolvedFiles)
+ if err := gui.Git.WorkingTree.StageFiles(pathsToStage); err != nil {
+ return gui.surfaceError(err)
+ }
+ }
+
files := gui.Git.Loaders.Files.
GetStatusFiles(loaders.GetStatusFileOptions{})
+ conflictFileCount := 0
+ for _, file := range files {
+ if file.HasMergeConflicts {
+ conflictFileCount++
+ }
+ }
+
+ if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 {
+ gui.OnUIThread(func() error { return gui.promptToContinueRebase() })
+ }
+
// for when you stage the old file of a rename and the new file is in a collapsed dir
state.FileTreeViewModel.RWMutex.Lock()
for _, file := range files {
@@ -602,6 +673,19 @@ func (gui *Gui) refreshStateFiles() error {
return nil
}
+// 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,
+ handleConfirm: func() error {
+ return gui.genericMergeCommand(REBASE_OPTION_CONTINUE)
+ },
+ })
+}
+
// Let's try to find our file again and move the cursor to that.
// If we can't find our file, it was probably just removed by the user. In that
// case, we go looking for where the next file has been moved to. Given that the
@@ -859,6 +943,18 @@ func (gui *Gui) switchToMerge() error {
return nil
}
+ gui.takeOverMergeConflictScrolling()
+
+ if gui.State.Panels.Merging.GetPath() != file.Name {
+ hasConflicts, err := gui.setMergeState(file.Name)
+ if err != nil {
+ return err
+ }
+ if !hasConflicts {
+ return nil
+ }
+ }
+
return gui.pushContext(gui.State.Contexts.Merging)
}
@@ -870,15 +966,6 @@ func (gui *Gui) openFile(filename string) error {
return nil
}
-func (gui *Gui) anyFilesWithMergeConflicts() bool {
- for _, file := range gui.State.FileTreeViewModel.GetAllFiles() {
- if file.HasMergeConflicts {
- return true
- }
- }
- return false
-}
-
func (gui *Gui) handleCustomCommand() error {
return gui.prompt(promptOpts{
title: gui.Tr.CustomCommand,
diff --git a/pkg/gui/filetree/file_node_test.go b/pkg/gui/filetree/file_node_test.go
index 228b2036a..8961015ac 100644
--- a/pkg/gui/filetree/file_node_test.go
+++ b/pkg/gui/filetree/file_node_test.go
@@ -132,3 +132,32 @@ func TestCompress(t *testing.T) {
})
}
}
+
+func TestGetFile(t *testing.T) {
+ scenarios := []struct {
+ name string
+ viewModel *FileTreeViewModel
+ path string
+ expected *models.File
+ }{
+ {
+ name: "valid case",
+ viewModel: NewFileTreeViewModel([]*models.File{{Name: "blah/one"}, {Name: "blah/two"}}, nil, false),
+ path: "blah/two",
+ expected: &models.File{Name: "blah/two"},
+ },
+ {
+ name: "not found",
+ viewModel: NewFileTreeViewModel([]*models.File{{Name: "blah/one"}, {Name: "blah/two"}}, nil, false),
+ path: "blah/three",
+ expected: nil,
+ },
+ }
+
+ for _, s := range scenarios {
+ s := s
+ t.Run(s.name, func(t *testing.T) {
+ assert.EqualValues(t, s.expected, s.viewModel.GetFile(s.path))
+ })
+ }
+}
diff --git a/pkg/gui/filetree/file_tree_view_model.go b/pkg/gui/filetree/file_tree_view_model.go
index d12814976..7e8497c96 100644
--- a/pkg/gui/filetree/file_tree_view_model.go
+++ b/pkg/gui/filetree/file_tree_view_model.go
@@ -86,6 +86,16 @@ func (self *FileTreeViewModel) GetItemAtIndex(index int) *FileNode {
return self.tree.GetNodeAtIndex(index+1, self.collapsedPaths) // ignoring root
}
+func (self *FileTreeViewModel) GetFile(path string) *models.File {
+ for _, file := range self.files {
+ if file.Name == path {
+ return file
+ }
+ }
+
+ return nil
+}
+
func (self *FileTreeViewModel) GetIndexForPath(path string) (int, bool) {
index, found := self.tree.GetIndexForPath(path, self.collapsedPaths)
return index - 1, found
diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go
index f2ef52986..22b43b6b7 100644
--- a/pkg/gui/global_handlers.go
+++ b/pkg/gui/global_handlers.go
@@ -115,7 +115,7 @@ func (gui *Gui) linesToScrollDown(view *gocui.View) int {
}
func (gui *Gui) scrollUpMain() error {
- if gui.canScrollMergePanel() {
+ if gui.renderingConflicts() {
gui.State.Panels.Merging.UserVerticalScrolling = true
}
@@ -123,7 +123,7 @@ func (gui *Gui) scrollUpMain() error {
}
func (gui *Gui) scrollDownMain() error {
- if gui.canScrollMergePanel() {
+ if gui.renderingConflicts() {
gui.State.Panels.Merging.UserVerticalScrolling = true
}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index f46d3c756..3549e186f 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -292,10 +292,11 @@ type guiState struct {
// managers for them which handle rendering a flat list of files in tree form
FileTreeViewModel *filetree.FileTreeViewModel
CommitFileTreeViewModel *filetree.CommitFileTreeViewModel
- Submodules []*models.SubmoduleConfig
- Branches []*models.Branch
- Commits []*models.Commit
- StashEntries []*models.StashEntry
+
+ Submodules []*models.SubmoduleConfig
+ Branches []*models.Branch
+ Commits []*models.Commit
+ StashEntries []*models.StashEntry
// Suggestions will sometimes appear when typing into a prompt
Suggestions []*types.Suggestion
// FilteredReflogCommits are the ones that appear in the reflog panel.
@@ -304,13 +305,14 @@ type guiState struct {
// ReflogCommits are the ones used by the branches panel to obtain recency values
// if we're not in filtering mode, CommitFiles and FilteredReflogCommits will be
// one and the same
- ReflogCommits []*models.Commit
- SubCommits []*models.Commit
- Remotes []*models.Remote
- RemoteBranches []*models.RemoteBranch
- Tags []*models.Tag
- MenuItems []*menuItem
- BisectInfo *git_commands.BisectInfo
+ ReflogCommits []*models.Commit
+ SubCommits []*models.Commit
+ Remotes []*models.Remote
+ RemoteBranches []*models.RemoteBranch
+ Tags []*models.Tag
+ MenuItems []*menuItem
+ BisectInfo *git_commands.BisectInfo
+
Updating bool
Panels *panelStates
SplitMainPanel bool
diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go
index 584a276bb..464bb4f5d 100644
--- a/pkg/gui/keybindings.go
+++ b/pkg/gui/keybindings.go
@@ -1543,20 +1543,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
{
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
- Key: gocui.MouseWheelUp,
- Modifier: gocui.ModNone,
- Handler: gui.handleSelectPrevConflictHunk,
- },
- {
- ViewName: "main",
- Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
- Key: gocui.MouseWheelDown,
- Modifier: gocui.ModNone,
- Handler: gui.handleSelectNextConflictHunk,
- },
- {
- ViewName: "main",
- Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.PrevBlockAlt),
Modifier: gocui.ModNone,
Handler: gui.handleSelectPrevConflict,
@@ -1586,7 +1572,7 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
ViewName: "main",
Contexts: []string{string(MAIN_MERGING_CONTEXT_KEY)},
Key: gui.getKey(config.Universal.Undo),
- Handler: gui.handlePopFileSnapshot,
+ Handler: gui.handleMergeConflictUndo,
Description: gui.Tr.LcUndo,
},
{
diff --git a/pkg/gui/merge_panel.go b/pkg/gui/merge_panel.go
index 917fd3f1f..7700f193c 100644
--- a/pkg/gui/merge_panel.go
+++ b/pkg/gui/merge_panel.go
@@ -7,9 +7,7 @@ import (
"io/ioutil"
"math"
- "github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
- "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)
@@ -17,7 +15,7 @@ func (gui *Gui) handleSelectPrevConflictHunk() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.SelectPrevConflictHunk()
- return gui.refreshMergePanel()
+ return gui.renderConflictsWithFocus()
})
}
@@ -25,7 +23,7 @@ func (gui *Gui) handleSelectNextConflictHunk() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.SelectNextConflictHunk()
- return gui.refreshMergePanel()
+ return gui.renderConflictsWithFocus()
})
}
@@ -33,7 +31,7 @@ func (gui *Gui) handleSelectNextConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.SelectNextConflict()
- return gui.refreshMergePanel()
+ return gui.renderConflictsWithFocus()
})
}
@@ -41,42 +39,29 @@ func (gui *Gui) handleSelectPrevConflict() error {
return gui.withMergeConflictLock(func() error {
gui.takeOverMergeConflictScrolling()
gui.State.Panels.Merging.SelectPrevConflict()
- return gui.refreshMergePanel()
+ return gui.renderConflictsWithFocus()
})
}
-func (gui *Gui) pushFileSnapshot() error {
- content, err := gui.catSelectedFile()
- if err != nil {
- return err
- }
- gui.State.Panels.Merging.PushFileSnapshot(content)
- return nil
-}
+func (gui *Gui) handleMergeConflictUndo() error {
+ state := gui.State.Panels.Merging
-func (gui *Gui) handlePopFileSnapshot() error {
- prevContent, ok := gui.State.Panels.Merging.PopFileSnapshot()
+ ok := state.Undo()
if !ok {
return nil
}
- gitFile := gui.getSelectedFile()
- if gitFile == nil {
- return nil
- }
gui.logAction("Restoring file to previous state")
gui.logCommand("Undoing last conflict resolution", false)
- if err := ioutil.WriteFile(gitFile.Name, []byte(prevContent), 0644); err != nil {
+ if err := ioutil.WriteFile(state.GetPath(), []byte(state.GetContent()), 0644); err != nil {
return err
}
- return gui.refreshMergePanel()
+ return gui.renderConflictsWithFocus()
}
func (gui *Gui) handlePickHunk() error {
return gui.withMergeConflictLock(func() error {
- gui.takeOverMergeConflictScrolling()
-
ok, err := gui.resolveConflict(gui.State.Panels.Merging.Selection())
if err != nil {
return err
@@ -86,20 +71,16 @@ func (gui *Gui) handlePickHunk() error {
return nil
}
- if gui.State.Panels.Merging.IsFinalConflict() {
- if err := gui.handleCompleteMerge(); err != nil {
- return err
- }
- return nil
+ if gui.State.Panels.Merging.AllConflictsResolved() {
+ return gui.onLastConflictResolved()
}
- return gui.refreshMergePanel()
+
+ return gui.renderConflictsWithFocus()
})
}
func (gui *Gui) handlePickAllHunks() error {
return gui.withMergeConflictLock(func() error {
- gui.takeOverMergeConflictScrolling()
-
ok, err := gui.resolveConflict(mergeconflicts.ALL)
if err != nil {
return err
@@ -109,17 +90,20 @@ func (gui *Gui) handlePickAllHunks() error {
return nil
}
- return gui.refreshMergePanel()
+ if gui.State.Panels.Merging.AllConflictsResolved() {
+ return gui.onLastConflictResolved()
+ }
+
+ return gui.renderConflictsWithFocus()
})
}
func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error) {
- gitFile := gui.getSelectedFile()
- if gitFile == nil {
- return false, nil
- }
+ gui.takeOverMergeConflictScrolling()
- ok, output, err := gui.State.Panels.Merging.ContentAfterConflictResolve(gitFile.Name, selection)
+ state := gui.State.Panels.Merging
+
+ ok, content, err := state.ContentAfterConflictResolve(selection)
if err != nil {
return false, err
}
@@ -128,10 +112,6 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error
return false, nil
}
- if err := gui.pushFileSnapshot(); err != nil {
- return false, gui.surfaceError(err)
- }
-
var logStr string
switch selection {
case mergeconflicts.TOP:
@@ -145,40 +125,14 @@ func (gui *Gui) resolveConflict(selection mergeconflicts.Selection) (bool, error
}
gui.logAction("Resolve merge conflict")
gui.logCommand(logStr, false)
- return true, ioutil.WriteFile(gitFile.Name, []byte(output), 0644)
-}
-
-func (gui *Gui) refreshMergePanelWithLock() error {
- return gui.withMergeConflictLock(gui.refreshMergePanel)
-}
-
-// not re-using state here because we can run into issues with mutexes when
-// doing that.
-func (gui *Gui) renderConflictsFromFilesPanel() error {
- state := mergeconflicts.NewState()
- _, err := gui.renderConflicts(state, false)
-
- return err
+ state.PushContent(content)
+ return true, ioutil.WriteFile(state.GetPath(), []byte(content), 0644)
}
-func (gui *Gui) renderConflicts(state *mergeconflicts.State, hasFocus bool) (bool, error) {
- cat, err := gui.catSelectedFile()
- if err != nil {
- return false, gui.refreshMainViews(refreshMainOpts{
- main: &viewUpdateOpts{
- title: "",
- task: NewRenderStringTask(err.Error()),
- },
- })
- }
-
- state.SetConflictsFromCat(cat)
-
- if state.NoConflicts() {
- return false, gui.handleCompleteMerge()
- }
-
- content := mergeconflicts.ColoredConflictFile(cat, state, hasFocus)
+// precondition: we actually have conflicts to render
+func (gui *Gui) renderConflicts(hasFocus bool) error {
+ state := gui.State.Panels.Merging.State
+ content := mergeconflicts.ColoredConflictFile(state, hasFocus)
if !gui.State.Panels.Merging.UserVerticalScrolling {
// TODO: find a way to not have to do this OnUIThread thing. Why doesn't it work
@@ -189,7 +143,7 @@ func (gui *Gui) renderConflicts(state *mergeconflicts.State, hasFocus bool) (boo
})
}
- return true, gui.refreshMainViews(refreshMainOpts{
+ return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
title: gui.Tr.MergeConflictsTitle,
task: NewRenderStringWithoutScrollTask(content),
@@ -198,35 +152,8 @@ func (gui *Gui) renderConflicts(state *mergeconflicts.State, hasFocus bool) (boo
})
}
-func (gui *Gui) refreshMergePanel() error {
- conflictsFound, err := gui.renderConflicts(gui.State.Panels.Merging.State, true)
- if err != nil {
- return err
- }
-
- if !conflictsFound {
- return gui.handleCompleteMerge()
- }
-
- return nil
-}
-
-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.Git.File.Cat(item.Name)
- if err != nil {
- gui.Log.Error(err)
- return "", err
- }
- return cat, nil
+func (gui *Gui) renderConflictsWithFocus() error {
+ return gui.renderConflicts(true)
}
func (gui *Gui) centerYPos(view *gocui.View, y int) {
@@ -256,68 +183,47 @@ func (gui *Gui) handleEscapeMerge() error {
return gui.escapeMerge()
}
-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
- }
+func (gui *Gui) onLastConflictResolved() error {
+ // as part of refreshing files, we handle the situation where a file has had
+ // its merge conflicts resolved.
+ return gui.refreshSidePanels(refreshOptions{mode: ASYNC, scope: []RefreshableView{FILES}})
+}
+
+func (gui *Gui) resetMergeState() {
+ gui.takeOverMergeConflictScrolling()
+ gui.State.Panels.Merging.Reset()
+}
- // if there are no more files with merge conflicts, we should ask whether the user wants to continue
- if gui.Git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !gui.anyFilesWithMergeConflicts() {
- return gui.promptToContinueRebase()
+func (gui *Gui) setMergeState(path string) (bool, error) {
+ content, err := gui.Git.File.Cat(path)
+ if err != nil {
+ return false, err
}
- return gui.escapeMerge()
+ gui.State.Panels.Merging.SetContent(content, path)
+
+ return !gui.State.Panels.Merging.NoConflicts(), nil
}
func (gui *Gui) escapeMerge() error {
- gui.takeOverMergeConflictScrolling()
-
- gui.State.Panels.Merging.Reset()
+ gui.resetMergeState()
// 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 {
+
+ if gui.currentContext().GetKey() == MAIN_MERGING_CONTEXT_KEY {
return gui.pushContext(gui.State.Contexts.Files)
}
return nil
}
-// 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(REBASE_OPTION_CONTINUE)
- },
- handleClose: func() error {
- return gui.pushContext(gui.State.Contexts.Files)
- },
- })
-}
-
-func (gui *Gui) canScrollMergePanel() bool {
+func (gui *Gui) renderingConflicts() 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
+ return gui.State.Panels.Merging.Active()
}
func (gui *Gui) withMergeConflictLock(f func() error) error {
diff --git a/pkg/gui/mergeconflicts/find_conflicts.go b/pkg/gui/mergeconflicts/find_conflicts.go
index 80e487f85..14a08fd68 100644
--- a/pkg/gui/mergeconflicts/find_conflicts.go
+++ b/pkg/gui/mergeconflicts/find_conflicts.go
@@ -1,6 +1,10 @@
package mergeconflicts
import (
+ "bufio"
+ "bytes"
+ "io"
+ "os"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -53,19 +57,59 @@ func findConflicts(content string) []*mergeConflict {
return conflicts
}
+var CONFLICT_START = "<<<<<<< "
+var CONFLICT_END = ">>>>>>> "
+var CONFLICT_START_BYTES = []byte(CONFLICT_START)
+var CONFLICT_END_BYTES = []byte(CONFLICT_END)
+
func determineLineType(line string) LineType {
+ // TODO: find out whether we ever actually get this prefix
trimmedLine := strings.TrimPrefix(line, "++")
switch {
- case strings.HasPrefix(trimmedLine, "<<<<<<< "):
+ case strings.HasPrefix(trimmedLine, CONFLICT_START):
return START
case strings.HasPrefix(trimmedLine, "||||||| "):
return ANCESTOR
case trimmedLine == "=======":
return TARGET
- case strings.HasPrefix(trimmedLine, ">>>>>>> "):
+ case strings.HasPrefix(trimmedLine, CONFLICT_END):
return END
default:
return NOT_A_MARKER
}
}
+
+// tells us whether a file actually has inline merge conflicts. We need to run this
+// because git will continue showing a status of 'UU' even after the conflicts have
+// been resolved in the user's editor
+func FileHasConflictMarkers(path string) (bool, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ return false, err
+ }
+
+ defer file.Close()
+
+ return fileHasConflictMarkersAux(file), nil
+}
+
+// Efficiently scans through a file looking for merge conflict markers. Returns true if it does
+func fileHasConflictMarkersAux(file io.Reader) bool {
+ scanner := bufio.NewScanner(file)
+ scann