Skip to content

Commit

Permalink
Make auto-staging resolved conflicts optional (#3870)
Browse files Browse the repository at this point in the history
- **PR Description**

Add user config `git.autoStageResolvedConflicts` (default true). When
set to false, users need to stage their conflicted files manually after
resolving conflicts, and also continue a merge/rebase manually when all
conflicted files are resolved.

Fixes #3111.
  • Loading branch information
stefanhaller authored Sep 2, 2024
2 parents 2f01af4 + 90b8fd2 commit 3cffed9
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 25 deletions.
6 changes: 6 additions & 0 deletions docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ git:
# If true, pass the --all arg to git fetch
fetchAll: true

# If true, lazygit will automatically stage files that used to have merge
# conflicts but no longer do; and it will also ask you if you want to
# continue a merge or rebase if you've resolved all conflicts. If false, it
# won't do either of these things.
autoStageResolvedConflicts: true

# Command used when displaying the current branch git log in the main window
branchLogCmd: git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --

Expand Down
6 changes: 6 additions & 0 deletions pkg/config/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,11 @@ type GitConfig struct {
AutoRefresh bool `yaml:"autoRefresh"`
// If true, pass the --all arg to git fetch
FetchAll bool `yaml:"fetchAll"`
// If true, lazygit will automatically stage files that used to have merge
// conflicts but no longer do; and it will also ask you if you want to
// continue a merge or rebase if you've resolved all conflicts. If false, it
// won't do either of these things.
AutoStageResolvedConflicts bool `yaml:"autoStageResolvedConflicts"`
// Command used when displaying the current branch git log in the main window
BranchLogCmd string `yaml:"branchLogCmd"`
// Command used to display git log of all branches in the main window.
Expand Down Expand Up @@ -753,6 +758,7 @@ func GetDefaultConfig() *UserConfig {
AutoFetch: true,
AutoRefresh: true,
FetchAll: true,
AutoStageResolvedConflicts: true,
BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --",
AllBranchesLogCmd: "git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium",
DisableForcePushing: false,
Expand Down
48 changes: 25 additions & 23 deletions pkg/gui/controllers/helpers/refresh_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,33 +546,35 @@ func (self *RefreshHelper) refreshFilesAndSubmodules() error {
func (self *RefreshHelper) refreshStateFiles() error {
fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel

// 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 self.c.Model().Files {
if file.HasMergeConflicts {
prevConflictFileCount++
}
if file.HasInlineMergeConflicts {
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name)
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {
pathsToStage = append(pathsToStage, file.Name)
if self.c.UserConfig().Git.AutoStageResolvedConflicts {
// 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{}
for _, file := range self.c.Model().Files {
if file.HasMergeConflicts {
prevConflictFileCount++
}
if file.HasInlineMergeConflicts {
hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name)
if err != nil {
self.c.Log.Error(err)
} else if !hasConflicts {
pathsToStage = append(pathsToStage, file.Name)
}
}
}
}

if len(pathsToStage) > 0 {
self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles)
if err := self.c.Git().WorkingTree.StageFiles(pathsToStage); err != nil {
return err
if len(pathsToStage) > 0 {
self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles)
if err := self.c.Git().WorkingTree.StageFiles(pathsToStage); err != nil {
return err
}
}
}

Expand Down
13 changes: 11 additions & 2 deletions pkg/gui/filetree/file_node.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package filetree

import "github.com/jesseduffield/lazygit/pkg/commands/models"
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)

// FileNode wraps a node and provides some file-specific methods for it.
type FileNode struct {
Expand Down Expand Up @@ -44,7 +47,13 @@ func (self *FileNode) GetHasStagedChanges() bool {
}

func (self *FileNode) GetHasInlineMergeConflicts() bool {
return self.SomeFile(func(file *models.File) bool { return file.HasInlineMergeConflicts })
return self.SomeFile(func(file *models.File) bool {
if !file.HasInlineMergeConflicts {
return false
}
hasConflicts, _ := mergeconflicts.FileHasConflictMarkers(file.Name)
return hasConflicts
})
}

func (self *FileNode) GetIsTracked() bool {
Expand Down
81 changes: 81 additions & 0 deletions pkg/integration/tests/conflicts/resolve_no_auto_stage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package conflicts

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests/shared"
)

var ResolveNoAutoStage = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Resolving conflicts without auto-staging",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().Git.AutoStageResolvedConflicts = false
},
SetupRepo: func(shell *Shell) {
shared.CreateMergeConflictFiles(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("UU").Contains("file1").IsSelected(),
Contains("UU").Contains("file2"),
).
PressEnter()

t.Views().MergeConflicts().
IsFocused().
SelectedLines(
Contains("<<<<<<< HEAD"),
Contains("First Change"),
Contains("======="),
).
PressPrimaryAction()

t.Views().Files().
IsFocused().
// Resolving the conflict didn't auto-stage it
Lines(
Contains("UU").Contains("file1").IsSelected(),
Contains("UU").Contains("file2"),
).
// So do that manually
PressPrimaryAction().
Lines(
Contains("UU").Contains("file2").IsSelected(),
).
// Trying to stage a file that still has conflicts is not allowed:
PressPrimaryAction().
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("Cannot stage/unstage directory containing files with inline merge conflicts.")).
Confirm()
}).
PressEnter()

// coincidentally these files have the same conflict
t.Views().MergeConflicts().
IsFocused().
SelectedLines(
Contains("<<<<<<< HEAD"),
Contains("First Change"),
Contains("======="),
).
PressPrimaryAction()

t.Views().Files().
IsFocused().
// Again, resolving the conflict didn't auto-stage it
Lines(
Contains("UU").Contains("file2").IsSelected(),
).
// Doing that manually now works:
PressPrimaryAction().
Lines(
Contains("A").Contains("file3").IsSelected(),
)
},
})
1 change: 1 addition & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ var tests = []*components.IntegrationTest{
conflicts.Filter,
conflicts.ResolveExternally,
conflicts.ResolveMultipleFiles,
conflicts.ResolveNoAutoStage,
conflicts.UndoChooseHunk,
custom_commands.AccessCommitProperties,
custom_commands.BasicCommand,
Expand Down
5 changes: 5 additions & 0 deletions schema/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,11 @@
"description": "If true, pass the --all arg to git fetch",
"default": true
},
"autoStageResolvedConflicts": {
"type": "boolean",
"description": "If true, lazygit will automatically stage files that used to have merge\nconflicts but no longer do; and it will also ask you if you want to\ncontinue a merge or rebase if you've resolved all conflicts. If false, it\nwon't do either of these things.",
"default": true
},
"branchLogCmd": {
"type": "string",
"description": "Command used when displaying the current branch git log in the main window",
Expand Down

0 comments on commit 3cffed9

Please sign in to comment.