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

@ -199,7 +199,7 @@ end
---@param bufnr integer
---@param region diffs.ConflictRegion
---@param replacement string[]
local function replace_region(bufnr, region, replacement)
function M.replace_region(bufnr, region, replacement)
vim.api.nvim_buf_set_lines(
bufnr,
region.marker_ours,
@ -211,7 +211,7 @@ end
---@param bufnr integer
---@param config diffs.ConflictConfig
local function refresh(bufnr, config)
function M.refresh(bufnr, config)
local regions = parse_buffer(bufnr)
if #regions == 0 then
vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
@ -244,8 +244,8 @@ function M.resolve_ours(bufnr, config)
return
end
local lines = vim.api.nvim_buf_get_lines(bufnr, region.ours_start, region.ours_end, false)
replace_region(bufnr, region, lines)
refresh(bufnr, config)
M.replace_region(bufnr, region, lines)
M.refresh(bufnr, config)
end
---@param bufnr integer
@ -262,8 +262,8 @@ function M.resolve_theirs(bufnr, config)
return
end
local lines = vim.api.nvim_buf_get_lines(bufnr, region.theirs_start, region.theirs_end, false)
replace_region(bufnr, region, lines)
refresh(bufnr, config)
M.replace_region(bufnr, region, lines)
M.refresh(bufnr, config)
end
---@param bufnr integer
@ -288,8 +288,8 @@ function M.resolve_both(bufnr, config)
for _, l in ipairs(theirs) do
table.insert(combined, l)
end
replace_region(bufnr, region, combined)
refresh(bufnr, config)
M.replace_region(bufnr, region, combined)
M.refresh(bufnr, config)
end
---@param bufnr integer
@ -305,8 +305,8 @@ function M.resolve_none(bufnr, config)
if not region then
return
end
replace_region(bufnr, region, {})
refresh(bufnr, config)
M.replace_region(bufnr, region, {})
M.refresh(bufnr, config)
end
---@param bufnr integer
@ -417,7 +417,7 @@ function M.attach(bufnr, config)
if not attached_buffers[bufnr] then
return true
end
refresh(bufnr, config)
M.refresh(bufnr, config)
end,
})