Merge pull request #76 from barrettruth/fix/bufread
fix(commands): handle :e on diffs:// buffers via BufReadCmd
This commit is contained in:
commit
8d4602dbcb
3 changed files with 490 additions and 6 deletions
|
|
@ -86,8 +86,9 @@ function M.gdiff(revision, vertical)
|
|||
|
||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
||||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. revision .. ':' .. rel_path)
|
||||
|
|
@ -176,14 +177,18 @@ function M.gdiff_file(filepath, opts)
|
|||
|
||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, diff_lines)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
||||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':' .. rel_path)
|
||||
if repo_root then
|
||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_repo_root', repo_root)
|
||||
end
|
||||
if old_rel_path ~= rel_path then
|
||||
vim.api.nvim_buf_set_var(diff_buf, 'diffs_old_filepath', old_rel_path)
|
||||
end
|
||||
|
||||
vim.cmd(opts.vertical and 'vsplit' or 'split')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
|
@ -231,8 +236,9 @@ function M.gdiff_section(repo_root, opts)
|
|||
local diff_label = opts.staged and 'staged' or 'unstaged'
|
||||
local diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(diff_buf, 0, -1, false, result)
|
||||
vim.api.nvim_set_option_value('buftype', 'nofile', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('swapfile', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = diff_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = diff_buf })
|
||||
vim.api.nvim_buf_set_name(diff_buf, 'diffs://' .. diff_label .. ':all')
|
||||
|
|
@ -248,6 +254,77 @@ function M.gdiff_section(repo_root, opts)
|
|||
end)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
function M.read_buffer(bufnr)
|
||||
local name = vim.api.nvim_buf_get_name(bufnr)
|
||||
local url_body = name:match('^diffs://(.+)$')
|
||||
if not url_body then
|
||||
return
|
||||
end
|
||||
|
||||
local label, path = url_body:match('^([^:]+):(.+)$')
|
||||
if not label or not path then
|
||||
return
|
||||
end
|
||||
|
||||
local ok, repo_root = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_repo_root')
|
||||
if not ok or not repo_root then
|
||||
return
|
||||
end
|
||||
|
||||
local diff_lines
|
||||
|
||||
if path == 'all' then
|
||||
local cmd = { 'git', '-C', repo_root, 'diff', '--no-ext-diff', '--no-color' }
|
||||
if label == 'staged' then
|
||||
table.insert(cmd, '--cached')
|
||||
end
|
||||
diff_lines = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error ~= 0 then
|
||||
diff_lines = {}
|
||||
end
|
||||
else
|
||||
local abs_path = repo_root .. '/' .. path
|
||||
|
||||
local old_ok, old_rel_path = pcall(vim.api.nvim_buf_get_var, bufnr, 'diffs_old_filepath')
|
||||
local old_abs_path = old_ok and old_rel_path and (repo_root .. '/' .. old_rel_path) or abs_path
|
||||
local old_name = old_ok and old_rel_path or path
|
||||
|
||||
local old_lines, new_lines
|
||||
|
||||
if label == 'untracked' then
|
||||
old_lines = {}
|
||||
new_lines = git.get_working_content(abs_path) or {}
|
||||
elseif label == 'staged' then
|
||||
old_lines = git.get_file_content('HEAD', old_abs_path) or {}
|
||||
new_lines = git.get_index_content(abs_path) or {}
|
||||
elseif label == 'unstaged' then
|
||||
old_lines = git.get_index_content(old_abs_path)
|
||||
if not old_lines then
|
||||
old_lines = git.get_file_content('HEAD', old_abs_path) or {}
|
||||
end
|
||||
new_lines = git.get_working_content(abs_path) or {}
|
||||
else
|
||||
old_lines = git.get_file_content(label, abs_path) or {}
|
||||
new_lines = git.get_working_content(abs_path) or {}
|
||||
end
|
||||
|
||||
diff_lines = generate_unified_diff(old_lines, new_lines, old_name, path)
|
||||
end
|
||||
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, diff_lines)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('buftype', 'nowrite', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'delete', { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('swapfile', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('filetype', 'diff', { buf = bufnr })
|
||||
|
||||
dbg('reloaded diff buffer %d (%s:%s)', bufnr, label, path)
|
||||
|
||||
require('diffs').attach(bufnr)
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
||||
M.gdiff(opts.args ~= '' and opts.args or nil, false)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ vim.api.nvim_create_autocmd('FileType', {
|
|||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('BufReadCmd', {
|
||||
pattern = 'diffs://*',
|
||||
callback = function(args)
|
||||
require('diffs.commands').read_buffer(args.buf)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd('OptionSet', {
|
||||
pattern = 'diff',
|
||||
callback = function()
|
||||
|
|
|
|||
400
spec/read_buffer_spec.lua
Normal file
400
spec/read_buffer_spec.lua
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local commands = require('diffs.commands')
|
||||
local diffs = require('diffs')
|
||||
local git = require('diffs.git')
|
||||
|
||||
local saved_git = {}
|
||||
local saved_systemlist
|
||||
local test_buffers = {}
|
||||
|
||||
local function mock_git(overrides)
|
||||
overrides = overrides or {}
|
||||
saved_git.get_file_content = git.get_file_content
|
||||
saved_git.get_index_content = git.get_index_content
|
||||
saved_git.get_working_content = git.get_working_content
|
||||
|
||||
git.get_file_content = overrides.get_file_content
|
||||
or function()
|
||||
return { 'local M = {}', 'return M' }
|
||||
end
|
||||
git.get_index_content = overrides.get_index_content
|
||||
or function()
|
||||
return { 'local M = {}', 'return M' }
|
||||
end
|
||||
git.get_working_content = overrides.get_working_content
|
||||
or function()
|
||||
return { 'local M = {}', 'local x = 1', 'return M' }
|
||||
end
|
||||
end
|
||||
|
||||
local function mock_systemlist(fn)
|
||||
saved_systemlist = vim.fn.systemlist
|
||||
vim.fn.systemlist = function(cmd)
|
||||
local result = fn(cmd)
|
||||
saved_systemlist({ 'true' })
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
local function restore_mocks()
|
||||
for k, v in pairs(saved_git) do
|
||||
git[k] = v
|
||||
end
|
||||
saved_git = {}
|
||||
if saved_systemlist then
|
||||
vim.fn.systemlist = saved_systemlist
|
||||
saved_systemlist = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param vars? table<string, any>
|
||||
---@return integer
|
||||
local function create_diffs_buffer(name, vars)
|
||||
local existing = vim.fn.bufnr(name)
|
||||
if existing ~= -1 then
|
||||
vim.api.nvim_buf_delete(existing, { force = true })
|
||||
end
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_name(bufnr, name)
|
||||
vars = vars or {}
|
||||
for k, v in pairs(vars) do
|
||||
vim.api.nvim_buf_set_var(bufnr, k, v)
|
||||
end
|
||||
table.insert(test_buffers, bufnr)
|
||||
return bufnr
|
||||
end
|
||||
|
||||
local function cleanup_buffers()
|
||||
for _, bufnr in ipairs(test_buffers) do
|
||||
if vim.api.nvim_buf_is_valid(bufnr) then
|
||||
vim.api.nvim_buf_delete(bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
test_buffers = {}
|
||||
end
|
||||
|
||||
describe('read_buffer', function()
|
||||
after_each(function()
|
||||
restore_mocks()
|
||||
cleanup_buffers()
|
||||
end)
|
||||
|
||||
describe('early returns', function()
|
||||
it('does nothing on non-diffs:// buffer', function()
|
||||
local bufnr = vim.api.nvim_create_buf(false, true)
|
||||
table.insert(test_buffers, bufnr)
|
||||
assert.has_no.errors(function()
|
||||
commands.read_buffer(bufnr)
|
||||
end)
|
||||
assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
||||
end)
|
||||
|
||||
it('does nothing on malformed url without colon separator', function()
|
||||
local bufnr = create_diffs_buffer('diffs://nocolonseparator')
|
||||
vim.api.nvim_buf_set_var(bufnr, 'diffs_repo_root', '/tmp')
|
||||
local lines_before = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.has_no.errors(function()
|
||||
commands.read_buffer(bufnr)
|
||||
end)
|
||||
assert.are.same(lines_before, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
||||
end)
|
||||
|
||||
it('does nothing when diffs_repo_root is missing', function()
|
||||
local bufnr = create_diffs_buffer('diffs://staged:missing_root.lua')
|
||||
assert.has_no.errors(function()
|
||||
commands.read_buffer(bufnr)
|
||||
end)
|
||||
assert.are.same({ '' }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('buffer options', function()
|
||||
it('sets buftype, bufhidden, swapfile, modifiable, filetype', function()
|
||||
mock_git()
|
||||
local bufnr = create_diffs_buffer('diffs://staged:options_test.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.are.equal('nowrite', vim.api.nvim_get_option_value('buftype', { buf = bufnr }))
|
||||
assert.are.equal('delete', vim.api.nvim_get_option_value('bufhidden', { buf = bufnr }))
|
||||
assert.is_false(vim.api.nvim_get_option_value('swapfile', { buf = bufnr }))
|
||||
assert.is_false(vim.api.nvim_get_option_value('modifiable', { buf = bufnr }))
|
||||
assert.are.equal('diff', vim.api.nvim_get_option_value('filetype', { buf = bufnr }))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('dispatch', function()
|
||||
it('calls get_file_content + get_index_content for staged label', function()
|
||||
local called_get_file = false
|
||||
local called_get_index = false
|
||||
mock_git({
|
||||
get_file_content = function()
|
||||
called_get_file = true
|
||||
return { 'old' }
|
||||
end,
|
||||
get_index_content = function()
|
||||
called_get_index = true
|
||||
return { 'new' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://staged:dispatch_staged.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.is_true(called_get_file)
|
||||
assert.is_true(called_get_index)
|
||||
end)
|
||||
|
||||
it('calls get_index_content + get_working_content for unstaged label', function()
|
||||
local called_get_index = false
|
||||
local called_get_working = false
|
||||
mock_git({
|
||||
get_index_content = function()
|
||||
called_get_index = true
|
||||
return { 'index' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
called_get_working = true
|
||||
return { 'working' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_unstaged.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.is_true(called_get_index)
|
||||
assert.is_true(called_get_working)
|
||||
end)
|
||||
|
||||
it('calls only get_working_content for untracked label', function()
|
||||
local called_get_file = false
|
||||
local called_get_working = false
|
||||
mock_git({
|
||||
get_file_content = function()
|
||||
called_get_file = true
|
||||
return {}
|
||||
end,
|
||||
get_working_content = function()
|
||||
called_get_working = true
|
||||
return { 'new file' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://untracked:dispatch_untracked.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.is_false(called_get_file)
|
||||
assert.is_true(called_get_working)
|
||||
end)
|
||||
|
||||
it('calls get_file_content + get_working_content for revision label', function()
|
||||
local captured_rev
|
||||
local called_get_working = false
|
||||
mock_git({
|
||||
get_file_content = function(rev)
|
||||
captured_rev = rev
|
||||
return { 'old' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
called_get_working = true
|
||||
return { 'new' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://HEAD~3:dispatch_rev.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.are.equal('HEAD~3', captured_rev)
|
||||
assert.is_true(called_get_working)
|
||||
end)
|
||||
|
||||
it('falls back from index to HEAD for unstaged when index returns nil', function()
|
||||
local call_order = {}
|
||||
mock_git({
|
||||
get_index_content = function()
|
||||
table.insert(call_order, 'index')
|
||||
return nil
|
||||
end,
|
||||
get_file_content = function()
|
||||
table.insert(call_order, 'head')
|
||||
return { 'head content' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
return { 'working content' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://unstaged:dispatch_fallback.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.are.same({ 'index', 'head' }, call_order)
|
||||
end)
|
||||
|
||||
it('runs git diff for section diffs with path=all', function()
|
||||
local captured_cmd
|
||||
mock_systemlist(function(cmd)
|
||||
captured_cmd = cmd
|
||||
return {
|
||||
'diff --git a/file.lua b/file.lua',
|
||||
'--- a/file.lua',
|
||||
'+++ b/file.lua',
|
||||
'@@ -1 +1 @@',
|
||||
'-old',
|
||||
'+new',
|
||||
}
|
||||
end)
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://unstaged:all', {
|
||||
diffs_repo_root = '/home/test/repo',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.is_not_nil(captured_cmd)
|
||||
assert.are.equal('git', captured_cmd[1])
|
||||
assert.are.equal('/home/test/repo', captured_cmd[3])
|
||||
assert.are.equal('diff', captured_cmd[4])
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal('diff --git a/file.lua b/file.lua', lines[1])
|
||||
end)
|
||||
|
||||
it('passes --cached for staged section diffs', function()
|
||||
local captured_cmd
|
||||
mock_systemlist(function(cmd)
|
||||
captured_cmd = cmd
|
||||
return { 'diff --git a/f.lua b/f.lua', '@@ -1 +1 @@', '-a', '+b' }
|
||||
end)
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://staged:all', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.is_truthy(vim.tbl_contains(captured_cmd, '--cached'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('content', function()
|
||||
it('generates valid unified diff header with correct paths', function()
|
||||
mock_git({
|
||||
get_file_content = function()
|
||||
return { 'old' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
return { 'new' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://HEAD:lua/diffs/init.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal('diff --git a/lua/diffs/init.lua b/lua/diffs/init.lua', lines[1])
|
||||
assert.are.equal('--- a/lua/diffs/init.lua', lines[2])
|
||||
assert.are.equal('+++ b/lua/diffs/init.lua', lines[3])
|
||||
end)
|
||||
|
||||
it('uses old_filepath for diff header in renames', function()
|
||||
mock_git({
|
||||
get_file_content = function(_, path)
|
||||
assert.are.equal('/tmp/old_name.lua', path)
|
||||
return { 'old content' }
|
||||
end,
|
||||
get_index_content = function()
|
||||
return { 'new content' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://staged:new_name.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
diffs_old_filepath = 'old_name.lua',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal('diff --git a/old_name.lua b/new_name.lua', lines[1])
|
||||
assert.are.equal('--- a/old_name.lua', lines[2])
|
||||
assert.are.equal('+++ b/new_name.lua', lines[3])
|
||||
end)
|
||||
|
||||
it('produces empty buffer when old and new are identical', function()
|
||||
mock_git({
|
||||
get_file_content = function()
|
||||
return { 'identical' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
return { 'identical' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://HEAD:nodiff.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.same({ '' }, lines)
|
||||
end)
|
||||
|
||||
it('replaces existing buffer content on reload', function()
|
||||
mock_git({
|
||||
get_file_content = function()
|
||||
return { 'old' }
|
||||
end,
|
||||
get_working_content = function()
|
||||
return { 'new' }
|
||||
end,
|
||||
})
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://HEAD:replace_test.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'stale', 'content', 'from', 'before' })
|
||||
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
assert.are.equal('diff --git a/replace_test.lua b/replace_test.lua', lines[1])
|
||||
for _, line in ipairs(lines) do
|
||||
assert.is_not_equal('stale', line)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('attach integration', function()
|
||||
it('calls attach on the buffer', function()
|
||||
mock_git()
|
||||
|
||||
local attach_called_with
|
||||
local original_attach = diffs.attach
|
||||
diffs.attach = function(bufnr)
|
||||
attach_called_with = bufnr
|
||||
end
|
||||
|
||||
local bufnr = create_diffs_buffer('diffs://staged:attach_test.lua', {
|
||||
diffs_repo_root = '/tmp',
|
||||
})
|
||||
commands.read_buffer(bufnr)
|
||||
|
||||
assert.are.equal(bufnr, attach_called_with)
|
||||
|
||||
diffs.attach = original_attach
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue