fix(fugitive): handle renamed files correctly
Parse both old and new filenames from rename lines (R old -> new). When diffing staged renames, use old filename as base to correctly show content changes rather than treating the file as entirely new. Also adds comprehensive tests for filename edge cases: - Double extensions, hyphens, underscores, dotfiles - Deep nested paths, complex renames - Documents known limitation with filenames containing ' -> '
This commit is contained in:
parent
6072dd0156
commit
9ed0639005
3 changed files with 165 additions and 22 deletions
|
|
@ -92,6 +92,7 @@ end
|
|||
---@field vertical? boolean
|
||||
---@field staged? boolean
|
||||
---@field untracked? boolean
|
||||
---@field old_filepath? string
|
||||
|
||||
---@param filepath string
|
||||
---@param opts? diffs.GdiffFileOpts
|
||||
|
|
@ -104,6 +105,8 @@ function M.gdiff_file(filepath, opts)
|
|||
return
|
||||
end
|
||||
|
||||
local old_rel_path = opts.old_filepath and git.get_relative_path(opts.old_filepath) or rel_path
|
||||
|
||||
local old_lines, new_lines, err
|
||||
local diff_label
|
||||
|
||||
|
|
@ -116,7 +119,7 @@ function M.gdiff_file(filepath, opts)
|
|||
end
|
||||
diff_label = 'untracked'
|
||||
elseif opts.staged then
|
||||
old_lines, err = git.get_file_content('HEAD', filepath)
|
||||
old_lines, err = git.get_file_content('HEAD', opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines = {}
|
||||
end
|
||||
|
|
@ -126,9 +129,9 @@ function M.gdiff_file(filepath, opts)
|
|||
end
|
||||
diff_label = 'staged'
|
||||
else
|
||||
old_lines, err = git.get_index_content(filepath)
|
||||
old_lines, err = git.get_index_content(opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines, err = git.get_file_content('HEAD', filepath)
|
||||
old_lines, err = git.get_file_content('HEAD', opts.old_filepath or filepath)
|
||||
if not old_lines then
|
||||
old_lines = {}
|
||||
diff_label = 'untracked'
|
||||
|
|
@ -144,7 +147,7 @@ function M.gdiff_file(filepath, opts)
|
|||
end
|
||||
end
|
||||
|
||||
local diff_lines = generate_unified_diff(old_lines, new_lines, rel_path, rel_path)
|
||||
local diff_lines = generate_unified_diff(old_lines, new_lines, old_rel_path, rel_path)
|
||||
|
||||
if #diff_lines == 0 then
|
||||
vim.notify('[diffs.nvim]: no changes', vim.log.levels.INFO)
|
||||
|
|
|
|||
|
|
@ -27,19 +27,19 @@ function M.get_section_at_line(bufnr, lnum)
|
|||
end
|
||||
|
||||
---@param line string
|
||||
---@return string?
|
||||
---@return string?, string?
|
||||
local function parse_file_line(line)
|
||||
local renamed = line:match('^R[%s%d]*[^%s]+%s*->%s*(.+)$')
|
||||
if renamed then
|
||||
return vim.trim(renamed)
|
||||
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
||||
if old and new then
|
||||
return vim.trim(new), vim.trim(old)
|
||||
end
|
||||
|
||||
local filename = line:match('^[MADRCU?][MADRCU%s]*%s+(.+)$')
|
||||
if filename then
|
||||
return vim.trim(filename)
|
||||
return vim.trim(filename), nil
|
||||
end
|
||||
|
||||
return nil
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
---@param line string
|
||||
|
|
@ -57,34 +57,34 @@ end
|
|||
|
||||
---@param bufnr integer
|
||||
---@param lnum integer
|
||||
---@return string?, diffs.FugitiveSection, boolean
|
||||
---@return string?, diffs.FugitiveSection, boolean, string?
|
||||
function M.get_file_at_line(bufnr, lnum)
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local current_line = lines[lnum]
|
||||
|
||||
if not current_line then
|
||||
return nil, nil, false
|
||||
return nil, nil, false, nil
|
||||
end
|
||||
|
||||
local section_header = parse_section_header(current_line)
|
||||
if section_header then
|
||||
return nil, section_header, true
|
||||
return nil, section_header, true, nil
|
||||
end
|
||||
|
||||
local filename = parse_file_line(current_line)
|
||||
local filename, old_filename = parse_file_line(current_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, lnum)
|
||||
return filename, section, false
|
||||
return filename, section, false, old_filename
|
||||
end
|
||||
|
||||
local prefix = current_line:sub(1, 1)
|
||||
if prefix == '+' or prefix == '-' or prefix == ' ' then
|
||||
for i = lnum - 1, 1, -1 do
|
||||
local prev_line = lines[i]
|
||||
filename = parse_file_line(prev_line)
|
||||
filename, old_filename = parse_file_line(prev_line)
|
||||
if filename then
|
||||
local section = M.get_section_at_line(bufnr, i)
|
||||
return filename, section, false
|
||||
return filename, section, false, old_filename
|
||||
end
|
||||
if prev_line:match('^%w+ %(') or prev_line == '' then
|
||||
break
|
||||
|
|
@ -92,7 +92,7 @@ function M.get_file_at_line(bufnr, lnum)
|
|||
end
|
||||
end
|
||||
|
||||
return nil, nil, false
|
||||
return nil, nil, false, nil
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
|
|
@ -114,7 +114,7 @@ function M.diff_file_under_cursor(vertical)
|
|||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
|
||||
local filename, section, is_header = M.get_file_at_line(bufnr, lnum)
|
||||
local filename, section, is_header, old_filename = M.get_file_at_line(bufnr, lnum)
|
||||
|
||||
local repo_root = get_repo_root_from_fugitive(bufnr)
|
||||
if not repo_root then
|
||||
|
|
@ -141,13 +141,20 @@ function M.diff_file_under_cursor(vertical)
|
|||
end
|
||||
|
||||
local filepath = repo_root .. '/' .. filename
|
||||
local old_filepath = old_filename and (repo_root .. '/' .. old_filename) or nil
|
||||
|
||||
dbg('diff_file_under_cursor: %s (section: %s)', filename, section or 'unknown')
|
||||
dbg(
|
||||
'diff_file_under_cursor: %s (section: %s, old: %s)',
|
||||
filename,
|
||||
section or 'unknown',
|
||||
old_filename or 'none'
|
||||
)
|
||||
|
||||
commands.gdiff_file(filepath, {
|
||||
vertical = vertical,
|
||||
staged = section == 'staged',
|
||||
untracked = section == 'untracked',
|
||||
old_filepath = old_filepath,
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -109,14 +109,147 @@ describe('fugitive', function()
|
|||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses renamed file and returns new name', function()
|
||||
it('parses renamed file and returns both names', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R oldname.lua -> newname.lua',
|
||||
})
|
||||
local filename, section = fugitive.get_file_at_line(buf, 2)
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('newname.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.is_false(is_header)
|
||||
assert.equals('oldname.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('parses renamed file with similarity index', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R100 old.lua -> new.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.equals('old.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('returns nil old_filename for non-renames', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'M modified.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('modified.lua', filename)
|
||||
assert.equals('staged', section)
|
||||
assert.is_nil(old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file with spaces in name', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R old file.lua -> new file.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new file.lua', filename)
|
||||
assert.equals('old file.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file in subdirectory', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R src/old.lua -> src/new.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('src/new.lua', filename)
|
||||
assert.equals('src/old.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed file moved to different directory', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R old/file.lua -> new/file.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('new/file.lua', filename)
|
||||
assert.equals('old/file.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('KNOWN LIMITATION: filename containing arrow parsed incorrectly', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R a -> b.lua -> c.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('b.lua -> c.lua', filename)
|
||||
assert.equals('a', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles double extensions', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'M test.spec.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('test.spec.lua', filename)
|
||||
assert.is_nil(old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles hyphenated filenames', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M my-component-test.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('my-component-test.lua', filename)
|
||||
assert.equals('unstaged', section)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles underscores and numbers', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'A test_file_123.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('test_file_123.lua', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles dotfiles', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M .gitignore',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('.gitignore', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles renamed with complex names', function()
|
||||
local buf = create_status_buffer({
|
||||
'Staged (1)',
|
||||
'R src/old-file.spec.lua -> src/new-file.spec.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('src/new-file.spec.lua', filename)
|
||||
assert.equals('src/old-file.spec.lua', old_filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
it('handles deeply nested paths', function()
|
||||
local buf = create_status_buffer({
|
||||
'Unstaged (1)',
|
||||
'M lua/diffs/ui/components/diff-view.lua',
|
||||
})
|
||||
local filename, section, is_header, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||
assert.equals('lua/diffs/ui/components/diff-view.lua', filename)
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue