diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-26 01:20:19 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2022-01-26 14:50:47 +1100 |
commit | c8cc18920f0b5ab54a19b9f7bdcf83db3210576f (patch) | |
tree | 60623fbe5504469b9a4f4ca25a9a538b4eee52ce | |
parent | ce3bcfe37cf0c68f501fb09d543e9e212b0eaa61 (diff) |
improve merge conflict flow
-rw-r--r-- | pkg/commands/git_commands/working_tree.go | 13 | ||||
-rw-r--r-- | pkg/commands/git_commands/working_tree_test.go | 10 | ||||
-rw-r--r-- | pkg/commands/loaders/files.go | 2 | ||||
-rw-r--r-- | pkg/gui/context_config.go | 2 | ||||
-rw-r--r-- | pkg/gui/files_panel.go | 133 | ||||
-rw-r--r-- | pkg/gui/filetree/file_node_test.go | 29 | ||||
-rw-r--r-- | pkg/gui/filetree/file_tree_view_model.go | 10 | ||||
-rw-r--r-- | pkg/gui/global_handlers.go | 4 | ||||
-rw-r--r-- | pkg/gui/gui.go | 24 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 16 | ||||
-rw-r--r-- | pkg/gui/merge_panel.go | 200 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/find_conflicts.go | 48 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/find_conflicts_test.go | 40 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/rendering.go | 3 | ||||
-rw-r--r-- | pkg/gui/mergeconflicts/state.go | 83 | ||||
-rw-r--r-- | pkg/i18n/english.go | 2 | ||||
-rw-r--r-- | test/integration/mergeConflicts/config/config.yml | 5 |
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 |