Don't include common ancestor when picking "both" for a conflict in diff3 style (#5747)

When a merge conflict is rendered in the diff3 style, git includes the
common-ancestor section between the two sides. Pressing `b` used to pick
all three, which doesn't make sense; it now picks only the current and
incoming hunks.

Fixes #5486.
This commit is contained in:
Stefan Haller 2026-07-01 10:06:05 +02:00 committed by GitHub
commit 722239abd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 71 additions and 20 deletions

View file

@ -205,7 +205,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Previous hunk | |
| `` <down>, j `` | Next hunk | |
| `` <left>, h `` | Previous conflict | |

View file

@ -287,7 +287,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | ハンクを選択 | |
| `` b `` | すべてのハンクを選択 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 前のハンク | |
| `` <down>, j `` | 次のハンク | |
| `` <left>, h `` | 前のコンフリクト | |

View file

@ -144,7 +144,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Pick hunk | |
| `` b `` | Pick all hunks | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 이전 hunk를 선택 | |
| `` <down>, j `` | 다음 hunk를 선택 | |
| `` <left>, h `` | 이전 충돌을 선택 | |

View file

@ -213,7 +213,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Kies stuk | |
| `` b `` | Kies beide stukken | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Selecteer bovenste hunk | |
| `` <down>, j `` | Selecteer onderste hunk | |
| `` <left>, h `` | Selecteer voorgaand conflict | |

View file

@ -209,7 +209,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Wybierz fragment | |
| `` b `` | Wybierz wszystkie fragmenty | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Poprzedni fragment | |
| `` <down>, j `` | Następny fragment | |
| `` <left>, h `` | Poprzedni konflikt | |

View file

@ -273,7 +273,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Escolha o local | |
| `` b `` | Pegar todos os pedaços | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Trecho anterior | |
| `` <down>, j `` | Próximo trecho | |
| `` <left>, h `` | Conflito anterior | |

View file

@ -114,7 +114,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | Выбрать эту часть | |
| `` b `` | Выбрать все части | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | Выбрать предыдущую часть | |
| `` <down>, j `` | Выбрать следующую часть | |
| `` <left>, h `` | Выбрать предыдущий конфликт | |

View file

@ -294,7 +294,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 选中区块 | |
| `` b `` | 选中所有区块 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 选择顶部块 | |
| `` <down>, j `` | 选择底部块 | |
| `` <left>, h `` | 选择上一个冲突 | |

View file

@ -90,7 +90,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| Key | Action | Info |
|-----|--------|-------------|
| `` <space> `` | 挑選程式碼片段 | |
| `` b `` | 挑選所有程式碼片段 | |
| `` b `` | Pick both hunks | |
| `` <up>, k `` | 選擇上一段 | |
| `` <down>, j `` | 選擇下一段 | |
| `` <left>, h `` | 選擇上一個衝突 | |

View file

@ -35,8 +35,8 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts)
},
{
Keys: opts.GetKeys(opts.Config.Main.PickBothHunks),
Handler: self.withRenderAndFocus(self.HandlePickAllHunks),
Description: self.c.Tr.PickAllHunks,
Handler: self.withRenderAndFocus(self.HandlePickBothHunks),
Description: self.c.Tr.PickBothHunks,
DisplayOnScreen: true,
},
{
@ -247,8 +247,8 @@ func (self *MergeConflictsController) HandlePickHunk() error {
return self.pickSelection(self.context().GetState().Selection())
}
func (self *MergeConflictsController) HandlePickAllHunks() error {
return self.pickSelection(mergeconflicts.ALL)
func (self *MergeConflictsController) HandlePickBothHunks() error {
return self.pickSelection(mergeconflicts.BOTH)
}
func (self *MergeConflictsController) pickSelection(selection mergeconflicts.Selection) error {
@ -290,8 +290,8 @@ func (self *MergeConflictsController) resolveConflict(selection mergeconflicts.S
logStr = "Picking middle hunk"
case mergeconflicts.BOTTOM:
logStr = "Picking bottom hunk"
case mergeconflicts.ALL:
logStr = "Picking all hunks"
case mergeconflicts.BOTH:
logStr = "Picking both hunks"
}
self.c.LogAction("Resolve merge conflict")
self.c.LogCommand(logStr, false)

View file

@ -28,7 +28,7 @@ const (
TOP Selection = iota
MIDDLE
BOTTOM
ALL
BOTH
)
func (s Selection) isIndexToKeep(conflict *mergeConflict, i int) bool {
@ -56,14 +56,23 @@ func (s Selection) bounds(c *mergeConflict) (int, int) {
return c.ancestor, c.target
case BOTTOM:
return c.target, c.end
case ALL:
return c.start, c.end
case BOTH:
// BOTH spans two disjoint hunks, so it has no single range; callers
// go through selected() instead of asking for its bounds.
panic("BOTH has no single range")
}
panic("unexpected selection for merge conflict")
}
func (s Selection) selected(c *mergeConflict, idx int) bool {
// BOTH keeps the top and bottom hunks but drops the common ancestor in
// between (which is only present with the diff3 conflict style), so it
// isn't a single contiguous range like the other selections.
if s == BOTH {
return TOP.selected(c, idx) || BOTTOM.selected(c, idx)
}
start, end := s.bounds(c)
return start < idx && idx < end
}

View file

@ -320,7 +320,7 @@ type TranslationSet struct {
ViewConflictsMenuItem string
AbortMenuItem string
PickHunk string
PickAllHunks string
PickBothHunks string
ViewMergeRebaseOptions string
ViewMergeRebaseOptionsTooltip string
ViewMergeOptions string
@ -1338,7 +1338,7 @@ func EnglishTranslationSet() *TranslationSet {
RewordCommitEditor: "Reword with editor",
Error: "Error",
PickHunk: "Pick hunk",
PickAllHunks: "Pick all hunks",
PickBothHunks: "Pick both hunks",
Undo: "Undo",
UndoReflog: "Undo",
RedoReflog: "Redo",

View file

@ -0,0 +1,41 @@
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 PickBothHunksDiff3 = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Pick both hunks of a conflict rendered in the diff3 style; the common ancestor must not be included",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.SetConfig("merge.conflictStyle", "diff3")
shared.CreateMergeConflictFile(shell)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("UU file").IsSelected(),
).
PressEnter()
t.Views().MergeConflicts().
IsFocused().
// the diff3 style renders the common ancestor between the two changes
Content(Contains("<<<<<<< HEAD\nFirst Change")).
Content(Contains("||||||| ")).
Content(Contains("Original")).
Press(keys.Main.PickBothHunks)
t.Common().ContinueOnConflictsResolved("merge")
t.Views().Files().IsEmpty()
t.FileSystem().FileContent("file",
Equals("\nThis\nIs\nThe\nFirst Change\nSecond Change\nFile\n"))
},
})

View file

@ -167,6 +167,7 @@ var tests = []*components.IntegrationTest{
conflicts.MergeFileBoth,
conflicts.MergeFileCurrent,
conflicts.MergeFileIncoming,
conflicts.PickBothHunksDiff3,
conflicts.ResolveExternally,
conflicts.ResolveMultipleFiles,
conflicts.ResolveNoAutoStage,