From 932708f1703e51da63f78f129993a4d2b2f72cd8 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 16 Feb 2026 00:02:10 -0500 Subject: [PATCH] fix(parser): exclude git diff metadata from neogit filename patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: "new file mode 100644" and "deleted file mode 100644" lines in git diff output matched the neogit "new file" and "deleted" filename patterns, corrupting the current filename and breaking syntax highlighting for subsequent hunks. Solution: add negative guards so "new file mode" and "deleted file mode" lines are skipped before the neogit capture runs. Guard order matters for Lua and/not semantics — the negative check must evaluate first to avoid the and-operator returning true instead of the captured string. --- lua/diffs/parser.lua | 4 +- spec/parser_spec.lua | 304 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 306 insertions(+), 2 deletions(-) diff --git a/lua/diffs/parser.lua b/lua/diffs/parser.lua index fc9eeaa..9d86cd1 100644 --- a/lua/diffs/parser.lua +++ b/lua/diffs/parser.lua @@ -197,8 +197,8 @@ function M.parse_buffer(bufnr) for i, line in ipairs(lines) do local diff_git_file = line:match('^diff %-%-git a/.+ b/(.+)$') local neogit_file = line:match('^modified%s+(.+)$') - or line:match('^new file%s+(.+)$') - or line:match('^deleted%s+(.+)$') + or (not line:match('^new file mode') and line:match('^new file%s+(.+)$')) + or (not line:match('^deleted file mode') and line:match('^deleted%s+(.+)$')) or line:match('^renamed%s+(.+)$') or line:match('^copied%s+(.+)$') local bare_file = not hunk_start and line:match('^([^%s]+%.[^%s]+)$') diff --git a/spec/parser_spec.lua b/spec/parser_spec.lua index f297f58..5613784 100644 --- a/spec/parser_spec.lua +++ b/spec/parser_spec.lua @@ -637,6 +637,310 @@ describe('parser', function() delete_buffer(bufnr) end) + it('detects neogit renamed prefix', function() + local bufnr = create_buffer({ + 'renamed old.lua', + '@@ -1,2 +1,3 @@', + ' local M = {}', + '+local x = 1', + ' return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('old.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + delete_buffer(bufnr) + end) + + it('detects neogit copied prefix', function() + local bufnr = create_buffer({ + 'copied orig.lua', + '@@ -1,2 +1,3 @@', + ' local M = {}', + '+local x = 1', + ' return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('orig.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + delete_buffer(bufnr) + end) + + it('does not treat "new file mode" as a filename', function() + local bufnr = create_buffer({ + 'diff --git a/src/new.lua b/src/new.lua', + 'new file mode 100644', + 'index 0000000..abc1234', + '--- /dev/null', + '+++ b/src/new.lua', + '@@ -0,0 +1,2 @@', + '+local M = {}', + '+return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('src/new.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + delete_buffer(bufnr) + end) + + it('does not treat "new file mode 100755" as a filename', function() + local bufnr = create_buffer({ + 'diff --git a/bin/run b/bin/run', + 'new file mode 100755', + 'index 0000000..abc1234', + '--- /dev/null', + '+++ b/bin/run', + '@@ -0,0 +1,2 @@', + '+#!/bin/bash', + '+echo hello', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('bin/run', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "deleted file mode" as a filename', function() + local bufnr = create_buffer({ + 'diff --git a/src/old.lua b/src/old.lua', + 'deleted file mode 100644', + 'index abc1234..0000000', + '--- a/src/old.lua', + '+++ /dev/null', + '@@ -1,2 +0,0 @@', + '-local M = {}', + '-return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('src/old.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + delete_buffer(bufnr) + end) + + it('does not treat "deleted file mode 100755" as a filename', function() + local bufnr = create_buffer({ + 'diff --git a/bin/old b/bin/old', + 'deleted file mode 100755', + 'index abc1234..0000000', + '--- a/bin/old', + '+++ /dev/null', + '@@ -1,1 +0,0 @@', + '-#!/bin/bash', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('bin/old', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "old mode" or "new mode" as filenames', function() + local bufnr = create_buffer({ + 'diff --git a/script.sh b/script.sh', + 'old mode 100644', + 'new mode 100755', + '@@ -1,1 +1,2 @@', + ' echo hello', + '+echo world', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('script.sh', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "rename from/to" as filenames', function() + local bufnr = create_buffer({ + 'diff --git a/old.lua b/new.lua', + 'similarity index 95%', + 'rename from old.lua', + 'rename to new.lua', + '@@ -1,2 +1,2 @@', + ' local M = {}', + '-local x = 1', + '+local x = 2', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('new.lua', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "copy from/to" as filenames', function() + local bufnr = create_buffer({ + 'diff --git a/orig.lua b/copy.lua', + 'similarity index 100%', + 'copy from orig.lua', + 'copy to copy.lua', + '@@ -1,1 +1,1 @@', + ' local M = {}', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('copy.lua', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "similarity index" or "dissimilarity index" as filenames', function() + local bufnr = create_buffer({ + 'diff --git a/foo.lua b/bar.lua', + 'similarity index 85%', + 'rename from foo.lua', + 'rename to bar.lua', + '@@ -1,2 +1,2 @@', + ' local M = {}', + '-return 1', + '+return 2', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('bar.lua', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('does not treat "index" line as a filename', function() + local bufnr = create_buffer({ + 'diff --git a/test.lua b/test.lua', + 'index abc1234..def5678 100644', + '--- a/test.lua', + '+++ b/test.lua', + '@@ -1,1 +1,2 @@', + ' local x = 1', + '+local y = 2', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('test.lua', hunks[1].filename) + delete_buffer(bufnr) + end) + + it('neogit new file with diff containing new file mode metadata', function() + local bufnr = create_buffer({ + 'new file src/foo.lua', + 'diff --git a/src/foo.lua b/src/foo.lua', + 'new file mode 100644', + 'index 0000000..abc1234', + '--- /dev/null', + '+++ b/src/foo.lua', + '@@ -0,0 +1,3 @@', + '+local M = {}', + '+M.x = 1', + '+return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('src/foo.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + assert.are.equal(3, #hunks[1].lines) + delete_buffer(bufnr) + end) + + it('neogit deleted with diff containing deleted file mode metadata', function() + local bufnr = create_buffer({ + 'deleted src/old.lua', + 'diff --git a/src/old.lua b/src/old.lua', + 'deleted file mode 100644', + 'index abc1234..0000000', + '--- a/src/old.lua', + '+++ /dev/null', + '@@ -1,2 +0,0 @@', + '-local M = {}', + '-return M', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('src/old.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + assert.are.equal(2, #hunks[1].lines) + delete_buffer(bufnr) + end) + + it('multiple new files with mode metadata do not corrupt filenames', function() + local bufnr = create_buffer({ + 'diff --git a/a.lua b/a.lua', + 'new file mode 100644', + 'index 0000000..abc1234', + '--- /dev/null', + '+++ b/a.lua', + '@@ -0,0 +1,1 @@', + '+local a = 1', + 'diff --git a/b.lua b/b.lua', + 'new file mode 100644', + 'index 0000000..def5678', + '--- /dev/null', + '+++ b/b.lua', + '@@ -0,0 +1,1 @@', + '+local b = 2', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(2, #hunks) + assert.are.equal('a.lua', hunks[1].filename) + assert.are.equal('b.lua', hunks[2].filename) + delete_buffer(bufnr) + end) + + it('fugitive status with new and deleted files containing mode metadata', function() + local bufnr = create_buffer({ + 'Head: main', + '', + 'Staged (2)', + 'A src/new.lua', + 'diff --git a/src/new.lua b/src/new.lua', + 'new file mode 100644', + 'index 0000000..abc1234', + '--- /dev/null', + '+++ b/src/new.lua', + '@@ -0,0 +1,2 @@', + '+local M = {}', + '+return M', + 'D src/old.lua', + 'diff --git a/src/old.lua b/src/old.lua', + 'deleted file mode 100644', + 'index abc1234..0000000', + '--- a/src/old.lua', + '+++ /dev/null', + '@@ -1,1 +0,0 @@', + '-local x = 1', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(2, #hunks) + assert.are.equal('src/new.lua', hunks[1].filename) + assert.are.equal('lua', hunks[1].ft) + assert.are.equal('src/old.lua', hunks[2].filename) + assert.are.equal('lua', hunks[2].ft) + delete_buffer(bufnr) + end) + + it('neogit new file with deep nested path', function() + local bufnr = create_buffer({ + 'new file src/deep/nested/path/module.lua', + '@@ -0,0 +1,1 @@', + '+return {}', + }) + local hunks = parser.parse_buffer(bufnr) + + assert.are.equal(1, #hunks) + assert.are.equal('src/deep/nested/path/module.lua', hunks[1].filename) + delete_buffer(bufnr) + end) + it('detects bare filename for untracked files', function() local bufnr = create_buffer({ 'newfile.rs',