feat: unified diff conflict resolution for unmerged files (#99)

## Problem

Pressing `du` on a `UU` (unmerged) file in the fugitive status buffer
had no
effect. There was no way to see a proper ours-vs-theirs diff with syntax
highlighting and intra-line changes, or to resolve conflicts from within
a
unified diff view.

Additionally, pressing `du` on a section header containing only unmerged
files
showed "no changes in section" because `git diff` produces combined
(`diff --cc`)
output for unmerged files, which was stripped entirely.

## Solution

Fetch `:2:` (ours) and `:3:` (theirs) from the git index and generate a
standard
unified diff. The existing highlight pipeline (treesitter + intra-line)
applies
automatically. Resolution keymaps (`doo`/`dot`/`dob`/`don`) on hunks in
the diff
view write changes back to the working file's conflict markers.
Navigation
(`]x`/`[x`) jumps between unresolved conflict hunks.

For section diffs, combined diff entries are now replaced with generated
ours-vs-theirs unified diffs instead of being stripped.

Works for merge, cherry-pick, and rebase conflicts — git populates
`:2:`/`:3:`
the same way for all three.

Closes #61
This commit is contained in:
Barrett Ruth 2026-02-09 12:21:13 -05:00 committed by GitHub
parent 49fc446aae
commit a2053a132b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1287 additions and 28 deletions

View file

@ -40,6 +40,78 @@ describe('commands', function()
end)
end)
describe('filter_combined_diffs', function()
it('strips diff --cc entries entirely', function()
local lines = {
'diff --cc main.lua',
'index d13ab94,b113aee..0000000',
'--- a/main.lua',
'+++ b/main.lua',
'@@@ -1,7 -1,7 +1,11 @@@',
' local M = {}',
'++<<<<<<< HEAD',
' + return 1',
'++=======',
'+ return 2',
'++>>>>>>> theirs',
' end',
}
local result = commands.filter_combined_diffs(lines)
assert.are.equal(0, #result)
end)
it('preserves diff --git entries', function()
local lines = {
'diff --git a/file.lua b/file.lua',
'--- a/file.lua',
'+++ b/file.lua',
'@@ -1,3 +1,3 @@',
' local M = {}',
'-local x = 1',
'+local x = 2',
' return M',
}
local result = commands.filter_combined_diffs(lines)
assert.are.equal(8, #result)
assert.are.same(lines, result)
end)
it('strips combined but keeps unified in mixed output', function()
local lines = {
'diff --cc conflict.lua',
'index aaa,bbb..000',
'@@@ -1,1 -1,1 +1,5 @@@',
'++<<<<<<< HEAD',
'diff --git a/clean.lua b/clean.lua',
'--- a/clean.lua',
'+++ b/clean.lua',
'@@ -1,1 +1,1 @@',
'-old',
'+new',
}
local result = commands.filter_combined_diffs(lines)
assert.are.equal(6, #result)
assert.are.equal('diff --git a/clean.lua b/clean.lua', result[1])
assert.are.equal('+new', result[6])
end)
it('returns empty for empty input', function()
local result = commands.filter_combined_diffs({})
assert.are.equal(0, #result)
end)
it('returns empty when all entries are combined', function()
local lines = {
'diff --cc a.lua',
'some content',
'diff --cc b.lua',
'more content',
}
local result = commands.filter_combined_diffs(lines)
assert.are.equal(0, #result)
end)
end)
describe('find_hunk_line', function()
it('finds matching @@ header and returns target line', function()
local diff_lines = {