gdiff: normalize endpoint line reads for generated diffs #120

Closed
opened 2026-09-21 18:12:21 +00:00 by barrettruth · 0 comments
Owner

Original issue: barrettruth/diffs.nvim#271
Original author: barrettruth
Original date: 2026-05-09T19:21:37Z

Parent tracker: #270

Problem

Generated :Gdiff can render a fake trailing-newline hunk for files that have no text difference on the selected edge.

Observed in the local real-repo scenario matrix:

  • direct :Gdiff on a staged-only file opens diffs://unstaged:file.txt with a bogus trailing-newline-only hunk;
  • direct :Gdiff on a staged added file does the same, even though plain direct :Gdiff is index -> worktree and the index/worktree contents are identical;
  • direct :Gdiff on a mode-only file can show a fake text hunk instead of reporting the mode-only unsupported case;
  • direct :Gdiff on a pure rename can show a fake text hunk instead of correctly reporting no text changes or unsupported rename behavior.

This is not a Fugitive parity issue. Plain worktree-file :Gdiff should remain an index -> worktree generated unified diff. If that edge has no text changes, it should report no changes.

Suspected cause

Git endpoint reads and buffer/worktree reads normalize lines differently.

Relevant code:

  • lua/diffs/git.lua
    • get_file_content() uses vim.fn.systemlist({ "git", ..., "show", REV:path }, nil, true)
    • get_index_content() uses vim.fn.systemlist({ "git", ..., "show", ":0:path" }, nil, true)
    • get_working_content() uses vim.fn.readfile(filepath, "b")
  • lua/diffs/render.lua
    • read_endpoint() reads tree/index/worktree endpoints
    • unified_lines() joins line arrays with \n and delegates to vim.text.diff / vim.diff

In a temp repo, git show :0:file.txt through systemlist(..., true) produced { "one", "two", "three", "" } for a normal newline-terminated file, while the buffer/worktree representation was { "one", "two", "three" }. That final sentinel creates a fake \ No newline at end of file diff.

Expected behavior

  • Clean index -> worktree content should produce no generated diff.
  • Direct staged-only :Gdiff should report no unstaged changes.
  • Direct staged-add :Gdiff should report no unstaged changes when index and worktree match.
  • Direct mode-only :Gdiff should report Gdiff does not support mode-only changes, not a fake text hunk.
  • Real content changes should still render normally.
  • Real files ending with intentional blank lines must not lose actual blank lines. The fix should remove only the extra systemlist() EOF sentinel, not legitimate content.

Implementation guidance

Add a small normalization helper near the Git endpoint readers, or in the render endpoint path, that makes Git blob reads and worktree/buffer reads comparable.

Be careful with these cases:

  • a\n should compare as { "a" }, matching readfile().
  • a\n\n should compare as { "a", "" }, preserving the intentional blank final line.
  • a without a trailing newline should remain { "a" }.
  • Binary detection should still reject binary files before treating bytes as text hunks.

Do not solve this by suppressing all no-newline metadata globally; hunk parsing already treats \ No newline at end of file as metadata when the diff engine legitimately emits it.

Acceptance criteria

  • No fake newline-only generated diffs for clean or staged-only direct :Gdiff.
  • Mode-only and pure rename behavior is no longer masked by line normalization bugs.
  • Behavior prove both normal newline-terminated files and real trailing blank lines behave correctly.
> Original issue: barrettruth/diffs.nvim#271 > Original author: `barrettruth` > Original date: 2026-05-09T19:21:37Z Parent tracker: #270 ## Problem Generated `:Gdiff` can render a fake trailing-newline hunk for files that have no text difference on the selected edge. Observed in the local real-repo scenario matrix: - direct `:Gdiff` on a staged-only file opens `diffs://unstaged:file.txt` with a bogus trailing-newline-only hunk; - direct `:Gdiff` on a staged added file does the same, even though plain direct `:Gdiff` is `index -> worktree` and the index/worktree contents are identical; - direct `:Gdiff` on a mode-only file can show a fake text hunk instead of reporting the mode-only unsupported case; - direct `:Gdiff` on a pure rename can show a fake text hunk instead of correctly reporting no text changes or unsupported rename behavior. This is not a Fugitive parity issue. Plain worktree-file `:Gdiff` should remain an `index -> worktree` generated unified diff. If that edge has no text changes, it should report no changes. ## Suspected cause Git endpoint reads and buffer/worktree reads normalize lines differently. Relevant code: - `lua/diffs/git.lua` - `get_file_content()` uses `vim.fn.systemlist({ "git", ..., "show", REV:path }, nil, true)` - `get_index_content()` uses `vim.fn.systemlist({ "git", ..., "show", ":0:path" }, nil, true)` - `get_working_content()` uses `vim.fn.readfile(filepath, "b")` - `lua/diffs/render.lua` - `read_endpoint()` reads tree/index/worktree endpoints - `unified_lines()` joins line arrays with `\n` and delegates to `vim.text.diff` / `vim.diff` In a temp repo, `git show :0:file.txt` through `systemlist(..., true)` produced `{ "one", "two", "three", "" }` for a normal newline-terminated file, while the buffer/worktree representation was `{ "one", "two", "three" }`. That final sentinel creates a fake `\ No newline at end of file` diff. ## Expected behavior - Clean `index -> worktree` content should produce no generated diff. - Direct staged-only `:Gdiff` should report no unstaged changes. - Direct staged-add `:Gdiff` should report no unstaged changes when index and worktree match. - Direct mode-only `:Gdiff` should report `Gdiff does not support mode-only changes`, not a fake text hunk. - Real content changes should still render normally. - Real files ending with intentional blank lines must not lose actual blank lines. The fix should remove only the extra `systemlist()` EOF sentinel, not legitimate content. ## Implementation guidance Add a small normalization helper near the Git endpoint readers, or in the render endpoint path, that makes Git blob reads and worktree/buffer reads comparable. Be careful with these cases: - `a\n` should compare as `{ "a" }`, matching `readfile()`. - `a\n\n` should compare as `{ "a", "" }`, preserving the intentional blank final line. - `a` without a trailing newline should remain `{ "a" }`. - Binary detection should still reject binary files before treating bytes as text hunks. Do not solve this by suppressing all no-newline metadata globally; hunk parsing already treats `\ No newline at end of file` as metadata when the diff engine legitimately emits it. ## Acceptance criteria - No fake newline-only generated diffs for clean or staged-only direct `:Gdiff`. - Mode-only and pure rename behavior is no longer masked by line normalization bugs. - Behavior prove both normal newline-terminated files and real trailing blank lines behave correctly.
barrettruth 2026-09-21 18:12:21 +00:00
  • closed this issue
  • added the
    bug
    label
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
barrettruth/diffs.nvim#120
No description provided.