fix(fugitive): handle git-quoted filenames in status buffer
Problem: git quotes filenames containing spaces, unicode, or special characters (e.g. M "path with spaces/file.lua"). parse_file_line passed the quotes through, causing file-not-found on diff operations. Solution: add unquote() helper that strips surrounding quotes and unescapes \\, \", \n, \t, and octal \NNN sequences. Apply it to both filename returns in parse_file_line.
This commit is contained in:
parent
f5a090baae
commit
603c966c71
2 changed files with 98 additions and 2 deletions
|
|
@ -26,17 +26,62 @@ function M.get_section_at_line(bufnr, lnum)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param s string
|
||||||
|
---@return string
|
||||||
|
local function unquote(s)
|
||||||
|
if s:sub(1, 1) ~= '"' then
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
local inner = s:sub(2, -2)
|
||||||
|
local result = {}
|
||||||
|
local i = 1
|
||||||
|
while i <= #inner do
|
||||||
|
if inner:sub(i, i) == '\\' and i < #inner then
|
||||||
|
local next_char = inner:sub(i + 1, i + 1)
|
||||||
|
if next_char == 'n' then
|
||||||
|
table.insert(result, '\n')
|
||||||
|
i = i + 2
|
||||||
|
elseif next_char == 't' then
|
||||||
|
table.insert(result, '\t')
|
||||||
|
i = i + 2
|
||||||
|
elseif next_char == '"' then
|
||||||
|
table.insert(result, '"')
|
||||||
|
i = i + 2
|
||||||
|
elseif next_char == '\\' then
|
||||||
|
table.insert(result, '\\')
|
||||||
|
i = i + 2
|
||||||
|
elseif next_char:match('%d') then
|
||||||
|
local oct = inner:match('^(%d%d%d)', i + 1)
|
||||||
|
if oct then
|
||||||
|
table.insert(result, string.char(tonumber(oct, 8)))
|
||||||
|
i = i + 4
|
||||||
|
else
|
||||||
|
table.insert(result, next_char)
|
||||||
|
i = i + 2
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(result, next_char)
|
||||||
|
i = i + 2
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(result, inner:sub(i, i))
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.concat(result)
|
||||||
|
end
|
||||||
|
|
||||||
---@param line string
|
---@param line string
|
||||||
---@return string?, string?, string?
|
---@return string?, string?, string?
|
||||||
local function parse_file_line(line)
|
local function parse_file_line(line)
|
||||||
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
local old, new = line:match('^R%d*%s+(.-)%s+->%s+(.+)$')
|
||||||
if old and new then
|
if old and new then
|
||||||
return vim.trim(new), vim.trim(old), 'R'
|
return unquote(vim.trim(new)), unquote(vim.trim(old)), 'R'
|
||||||
end
|
end
|
||||||
|
|
||||||
local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$')
|
local status, filename = line:match('^([MADRCU?])[MADRCU%s]*%s+(.+)$')
|
||||||
if status and filename then
|
if status and filename then
|
||||||
return vim.trim(filename), nil, status
|
return unquote(vim.trim(filename)), nil, status
|
||||||
end
|
end
|
||||||
|
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,57 @@ describe('fugitive', function()
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('unquotes git-quoted filenames with spaces', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M "path with spaces/file.lua"',
|
||||||
|
})
|
||||||
|
local filename = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('path with spaces/file.lua', filename)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('unquotes escaped quotes in filenames', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M "file\\"name.lua"',
|
||||||
|
})
|
||||||
|
local filename = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('file"name.lua', filename)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('unquotes octal escapes in filenames', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M "\\303\\251le.lua"',
|
||||||
|
})
|
||||||
|
local filename = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('\195\169le.lua', filename)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('passes through unquoted filenames unchanged', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Unstaged (1)',
|
||||||
|
'M normal.lua',
|
||||||
|
})
|
||||||
|
local filename = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('normal.lua', filename)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('unquotes renamed files with quotes', function()
|
||||||
|
local buf = create_status_buffer({
|
||||||
|
'Staged (1)',
|
||||||
|
'R100 "old name.lua" -> "new name.lua"',
|
||||||
|
})
|
||||||
|
local filename, _, _, old_filename = fugitive.get_file_at_line(buf, 2)
|
||||||
|
assert.equals('new name.lua', filename)
|
||||||
|
assert.equals('old name.lua', old_filename)
|
||||||
|
vim.api.nvim_buf_delete(buf, { force = true })
|
||||||
|
end)
|
||||||
|
|
||||||
it('handles deeply nested paths', function()
|
it('handles deeply nested paths', function()
|
||||||
local buf = create_status_buffer({
|
local buf = create_status_buffer({
|
||||||
'Unstaged (1)',
|
'Unstaged (1)',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue