Skip to content

Commit ebb576f

Browse files
committed
Provide conflict resolution dialogs for non-textual conflicts
1 parent efcd71b commit ebb576f

File tree

5 files changed

+168
-1
lines changed

5 files changed

+168
-1
lines changed

pkg/commands/git_commands/working_tree.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,13 @@ func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
341341
return self.cmd.New(cmdArgs).Run()
342342
}
343343

344+
func (self *WorkingTreeCommands) RemoveConflictedFile(name string) error {
345+
cmdArgs := NewGitCmd("rm").Arg("--", name).
346+
ToArgv()
347+
348+
return self.cmd.New(cmdArgs).Run()
349+
}
350+
344351
// RemoveUntrackedFiles runs `git clean -fd`
345352
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
346353
cmdArgs := NewGitCmd("clean").Arg("-fd").ToArgv()

pkg/gui/controllers/files_controller.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,13 +571,59 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
571571
return self.switchToMerge()
572572
}
573573
if file.HasMergeConflicts {
574-
return errors.New(self.c.Tr.FileStagingRequirements)
574+
return self.handleNonInlineConflict(file)
575575
}
576576

577577
self.c.Context().Push(self.c.Contexts().Staging, opts)
578578
return nil
579579
}
580580

581+
func (self *FilesController) handleNonInlineConflict(file *models.File) error {
582+
handle := func(command func(command string) error, logText string) error {
583+
self.c.LogAction(logText)
584+
if err := command(file.GetPath()); err != nil {
585+
return err
586+
}
587+
return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}})
588+
}
589+
keepItem := &types.MenuItem{
590+
Label: self.c.Tr.MergeConflictKeepFile,
591+
OnPress: func() error {
592+
return handle(self.c.Git().WorkingTree.StageFile, self.c.Tr.Actions.ResolveConflictByKeepingFile)
593+
},
594+
Key: 'k',
595+
}
596+
deleteItem := &types.MenuItem{
597+
Label: self.c.Tr.MergeConflictDeleteFile,
598+
OnPress: func() error {
599+
return handle(self.c.Git().WorkingTree.RemoveConflictedFile, self.c.Tr.Actions.ResolveConflictByDeletingFile)
600+
},
601+
Key: 'd',
602+
}
603+
items := []*types.MenuItem{}
604+
switch file.ShortStatus {
605+
case "DD":
606+
// For "both deleted" conflicts, deleting the file is the only reasonable thing you can do.
607+
// Restoring to the state before deletion is not the responsibility of a conflict resolution tool.
608+
items = append(items, deleteItem)
609+
case "DU", "UD":
610+
// For these, we put the delete option first because it's the most common one,
611+
// even if it's more destructive.
612+
items = append(items, deleteItem, keepItem)
613+
case "AU", "UA":
614+
// For these, we put the keep option first because it's less destructive,
615+
// and the chances between keep and delete are 50/50.
616+
items = append(items, keepItem, deleteItem)
617+
default:
618+
panic("should only be called if there's a merge conflict")
619+
}
620+
return self.c.Menu(types.CreateMenuOptions{
621+
Title: self.c.Tr.MergeConflictsTitle,
622+
Prompt: file.GetMergeStateDescription(self.c.Tr),
623+
Items: items,
624+
})
625+
}
626+
581627
func (self *FilesController) toggleStagedAll() error {
582628
if err := self.toggleStagedAllWithLock(); err != nil {
583629
return err

pkg/i18n/english.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ type TranslationSet struct {
107107
MergeConflictIncomingDiff string
108108
MergeConflictCurrentDiff string
109109
MergeConflictPressEnterToResolve string
110+
MergeConflictKeepFile string
111+
MergeConflictDeleteFile string
110112
Checkout string
111113
CheckoutTooltip string
112114
CantCheckoutBranchWhilePulling string
@@ -951,6 +953,8 @@ type Actions struct {
951953
UnstageFile string
952954
UnstageAllFiles string
953955
StageAllFiles string
956+
ResolveConflictByKeepingFile string
957+
ResolveConflictByDeletingFile string
954958
NotEnoughContextToStage string
955959
NotEnoughContextToDiscard string
956960
IgnoreExcludeFile string
@@ -1128,6 +1132,8 @@ func EnglishTranslationSet() *TranslationSet {
11281132
MergeConflictIncomingDiff: "Incoming changes:",
11291133
MergeConflictCurrentDiff: "Current changes:",
11301134
MergeConflictPressEnterToResolve: "Press %s to resolve.",
1135+
MergeConflictKeepFile: "Keep file",
1136+
MergeConflictDeleteFile: "Delete file",
11311137
Checkout: "Checkout",
11321138
CheckoutTooltip: "Checkout selected item.",
11331139
CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch",
@@ -1968,6 +1974,8 @@ func EnglishTranslationSet() *TranslationSet {
19681974
UnstageFile: "Unstage file",
19691975
UnstageAllFiles: "Unstage all files",
19701976
StageAllFiles: "Stage all files",
1977+
ResolveConflictByKeepingFile: "Resolve by keeping file",
1978+
ResolveConflictByDeletingFile: "Resolve by deleting file",
19711979
NotEnoughContextToStage: "Staging or unstaging changes is not possible with a diff context size of 0. Increase the context using '%s'.",
19721980
NotEnoughContextToDiscard: "Discarding changes is not possible with a diff context size of 0. Increase the context using '%s'.",
19731981
IgnoreExcludeFile: "Ignore or exclude file",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package conflicts
2+
3+
import (
4+
"github.com/jesseduffield/lazygit/pkg/config"
5+
. "github.com/jesseduffield/lazygit/pkg/integration/components"
6+
)
7+
8+
var ResolveNonTextualConflicts = NewIntegrationTest(NewIntegrationTestArgs{
9+
Description: "Resolve non-textual merge conflicts (e.g. one side modified, the other side deleted)",
10+
ExtraCmdArgs: []string{},
11+
Skip: false,
12+
SetupConfig: func(config *config.AppConfig) {},
13+
SetupRepo: func(shell *Shell) {
14+
shell.RunShellCommand(`echo test1 > both-deleted1.txt`)
15+
shell.RunShellCommand(`echo test2 > both-deleted2.txt`)
16+
shell.RunShellCommand(`git checkout -b conflict && git add both-deleted1.txt both-deleted2.txt`)
17+
shell.RunShellCommand(`echo haha1 > deleted-them1.txt && git add deleted-them1.txt`)
18+
shell.RunShellCommand(`echo haha2 > deleted-them2.txt && git add deleted-them2.txt`)
19+
shell.RunShellCommand(`echo haha1 > deleted-us1.txt && git add deleted-us1.txt`)
20+
shell.RunShellCommand(`echo haha2 > deleted-us2.txt && git add deleted-us2.txt`)
21+
shell.RunShellCommand(`git commit -m one`)
22+
23+
// stuff on other branch
24+
shell.RunShellCommand(`git branch conflict_second`)
25+
shell.RunShellCommand(`git mv both-deleted1.txt added-them-changed-us1.txt`)
26+
shell.RunShellCommand(`git mv both-deleted2.txt added-them-changed-us2.txt`)
27+
shell.RunShellCommand(`git rm deleted-them1.txt deleted-them2.txt`)
28+
shell.RunShellCommand(`echo modded1 > deleted-us1.txt && git add deleted-us1.txt`)
29+
shell.RunShellCommand(`echo modded2 > deleted-us2.txt && git add deleted-us2.txt`)
30+
shell.RunShellCommand(`git commit -m "two"`)
31+
32+
// stuff on our branch
33+
shell.RunShellCommand(`git checkout conflict_second`)
34+
shell.RunShellCommand(`git mv both-deleted1.txt changed-them-added-us1.txt`)
35+
shell.RunShellCommand(`git mv both-deleted2.txt changed-them-added-us2.txt`)
36+
shell.RunShellCommand(`echo modded1 > deleted-them1.txt && git add deleted-them1.txt`)
37+
shell.RunShellCommand(`echo modded2 > deleted-them2.txt && git add deleted-them2.txt`)
38+
shell.RunShellCommand(`git rm deleted-us1.txt deleted-us2.txt`)
39+
shell.RunShellCommand(`git commit -m "three"`)
40+
shell.RunShellCommand(`git reset --hard conflict_second`)
41+
shell.RunCommandExpectError([]string{"git", "merge", "conflict"})
42+
},
43+
Run: func(t *TestDriver, keys config.KeybindingConfig) {
44+
resolve := func(filename string, menuChoice string) {
45+
t.Views().Files().
46+
NavigateToLine(Contains(filename)).
47+
Tap(func() {
48+
t.Views().Main().Content(Contains("Conflict:"))
49+
}).
50+
Press(keys.Universal.GoInto).
51+
Tap(func() {
52+
t.ExpectPopup().Menu().Title(Equals("Merge conflicts")).
53+
Select(Contains(menuChoice)).
54+
Confirm()
55+
})
56+
}
57+
58+
t.Views().Files().
59+
IsFocused().
60+
Lines(
61+
Equals("▼ /").IsSelected(),
62+
Equals(" UA added-them-changed-us1.txt"),
63+
Equals(" UA added-them-changed-us2.txt"),
64+
Equals(" DD both-deleted1.txt"),
65+
Equals(" DD both-deleted2.txt"),
66+
Equals(" AU changed-them-added-us1.txt"),
67+
Equals(" AU changed-them-added-us2.txt"),
68+
Equals(" UD deleted-them1.txt"),
69+
Equals(" UD deleted-them2.txt"),
70+
Equals(" DU deleted-us1.txt"),
71+
Equals(" DU deleted-us2.txt"),
72+
).
73+
Tap(func() {
74+
resolve("added-them-changed-us1.txt", "Delete file")
75+
resolve("added-them-changed-us2.txt", "Keep file")
76+
resolve("both-deleted1.txt", "Delete file")
77+
resolve("both-deleted2.txt", "Delete file")
78+
resolve("changed-them-added-us1.txt", "Delete file")
79+
resolve("changed-them-added-us2.txt", "Keep file")
80+
resolve("deleted-them1.txt", "Delete file")
81+
resolve("deleted-them2.txt", "Keep file")
82+
resolve("deleted-us1.txt", "Delete file")
83+
resolve("deleted-us2.txt", "Keep file")
84+
}).
85+
Lines(
86+
Equals("▼ /"),
87+
Equals(" A added-them-changed-us2.txt"),
88+
Equals(" D changed-them-added-us1.txt"),
89+
Equals(" D deleted-them1.txt"),
90+
Equals(" A deleted-us2.txt"),
91+
)
92+
93+
t.FileSystem().
94+
PathNotPresent("added-them-changed-us1.txt").
95+
FileContent("added-them-changed-us2.txt", Equals("test2\n")).
96+
PathNotPresent("both-deleted1.txt").
97+
PathNotPresent("both-deleted2.txt").
98+
PathNotPresent("changed-them-added-us1.txt").
99+
FileContent("changed-them-added-us2.txt", Equals("test2\n")).
100+
PathNotPresent("deleted-them1.txt").
101+
FileContent("deleted-them2.txt", Equals("modded2\n")).
102+
PathNotPresent("deleted-us1.txt").
103+
FileContent("deleted-us2.txt", Equals("modded2\n"))
104+
},
105+
})

pkg/integration/tests/test_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ var tests = []*components.IntegrationTest{
141141
conflicts.ResolveExternally,
142142
conflicts.ResolveMultipleFiles,
143143
conflicts.ResolveNoAutoStage,
144+
conflicts.ResolveNonTextualConflicts,
144145
conflicts.ResolveWithoutTrailingLf,
145146
conflicts.UndoChooseHunk,
146147
custom_commands.AccessCommitProperties,

0 commit comments

Comments
 (0)