feat: add :Gdiff command for unified diff against git revision
Compares current buffer against any git revision (default HEAD), opens result in vsplit with full diffs.nvim syntax highlighting.
This commit is contained in:
parent
2ce76e7683
commit
bf2c91f79f
5 changed files with 271 additions and 0 deletions
98
lua/diffs/commands.lua
Normal file
98
lua/diffs/commands.lua
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
local M = {}
|
||||
|
||||
local git = require('diffs.git')
|
||||
local dbg = require('diffs.log').dbg
|
||||
|
||||
---@param old_lines string[]
|
||||
---@param new_lines string[]
|
||||
---@param old_name string
|
||||
---@param new_name string
|
||||
---@return string[]
|
||||
local function generate_unified_diff(old_lines, new_lines, old_name, new_name)
|
||||
local old_content = table.concat(old_lines, '\n')
|
||||
local new_content = table.concat(new_lines, '\n')
|
||||
|
||||
local diff_output = vim.diff(old_content, new_content, {
|
||||
result_type = 'unified',
|
||||
ctxlen = 3,
|
||||
})
|
||||
|
||||
if not diff_output or diff_output == '' then
|
||||
return {}
|
||||
end
|
||||
|
||||
local diff_lines = vim.split(diff_output, '\n', { plain = true })
|
||||
|
||||
local result = {
|
||||
'diff --git a/' .. old_name .. ' b/' .. new_name,
|
||||
'--- a/' .. old_name,
|
||||
'+++ b/' .. new_name,
|
||||
}
|
||||
for _, line in ipairs(diff_lines) do
|
||||
table.insert(result, line)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@param revision? string
|
||||
function M.gdiff(revision)
|
||||
revision = revision or 'HEAD'
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filepath = vim.api.nvim_buf_get_name(bufnr)
|
||||
|
||||
if filepath == '' then
|
||||
vim.notify('diffs: buffer has no file', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local rel_path = git.get_relative_path(filepath)
|
||||
if not rel_path then
|
||||
vim.notify('diffs: not in a git repository', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local old_lines, err = git.get_file_content(revision, filepath)
|
||||
if not old_lines then
|
||||
vim.notify('diffs: ' .. (err or 'unknown error'), vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local new_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
|
||||
local diff_lines = generate_unified_diff(old_lines, new_lines, rel_path, rel_path)
|
||||
|
||||
if #diff_lines == 0 then
|
||||
vim.notify('diffs: no changes', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
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('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)
|
||||
|
||||
vim.cmd('vsplit')
|
||||
vim.api.nvim_win_set_buf(0, diff_buf)
|
||||
|
||||
dbg('opened diff buffer %d for %s against %s', diff_buf, rel_path, revision)
|
||||
|
||||
vim.schedule(function()
|
||||
require('diffs').attach(diff_buf)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command('Gdiff', function(opts)
|
||||
M.gdiff(opts.args ~= '' and opts.args or nil)
|
||||
end, {
|
||||
nargs = '?',
|
||||
desc = 'Show unified diff against git revision (default: HEAD)',
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
66
lua/diffs/git.lua
Normal file
66
lua/diffs/git.lua
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
local M = {}
|
||||
|
||||
---@param filepath string
|
||||
---@return string?
|
||||
function M.get_git_dir(filepath)
|
||||
local dir = vim.fn.fnamemodify(filepath, ':h')
|
||||
local result = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--git-dir' })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return nil
|
||||
end
|
||||
local git_dir = result[1]
|
||||
if not git_dir then
|
||||
return nil
|
||||
end
|
||||
if vim.startswith(git_dir, '/') then
|
||||
return git_dir
|
||||
end
|
||||
return vim.fn.fnamemodify(dir .. '/' .. git_dir, ':p'):gsub('/+$', '')
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
---@return string?
|
||||
function M.get_repo_root(filepath)
|
||||
local dir = vim.fn.fnamemodify(filepath, ':h')
|
||||
local result = vim.fn.systemlist({ 'git', '-C', dir, 'rev-parse', '--show-toplevel' })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return nil
|
||||
end
|
||||
return result[1]
|
||||
end
|
||||
|
||||
---@param revision string
|
||||
---@param filepath string
|
||||
---@return string[]?, string?
|
||||
function M.get_file_content(revision, filepath)
|
||||
local repo_root = M.get_repo_root(filepath)
|
||||
if not repo_root then
|
||||
return nil, 'not in a git repository'
|
||||
end
|
||||
|
||||
local rel_path = vim.fn.fnamemodify(filepath, ':.')
|
||||
if vim.startswith(filepath, repo_root) then
|
||||
rel_path = filepath:sub(#repo_root + 2)
|
||||
end
|
||||
|
||||
local result = vim.fn.systemlist({ 'git', '-C', repo_root, 'show', revision .. ':' .. rel_path })
|
||||
if vim.v.shell_error ~= 0 then
|
||||
return nil, 'failed to get file at revision: ' .. revision
|
||||
end
|
||||
return result, nil
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
---@return string?
|
||||
function M.get_relative_path(filepath)
|
||||
local repo_root = M.get_repo_root(filepath)
|
||||
if not repo_root then
|
||||
return nil
|
||||
end
|
||||
if vim.startswith(filepath, repo_root) then
|
||||
return filepath:sub(#repo_root + 2)
|
||||
end
|
||||
return vim.fn.fnamemodify(filepath, ':.')
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue