Adds the ability to use the local filesystem when doing reviews by passing the `--imply-local` flag to the reviewer. This functionality is disabled by default. If users make changes to their filesystem (the files are "dirty") the default reviewer (hashes not files) will be use. This is a MINOR release.
330 lines
11 KiB
Lua
330 lines
11 KiB
Lua
-- This Module contains all of the reviewer code for diffview
|
|
local u = require("gitlab.utils")
|
|
local state = require("gitlab.state")
|
|
local async_ok, async = pcall(require, "diffview.async")
|
|
local diffview_lib = require("diffview.lib")
|
|
|
|
local M = {
|
|
bufnr = nil,
|
|
tabnr = nil,
|
|
}
|
|
|
|
local all_git_manged_files_unmodified = function()
|
|
-- check local managed files are unmodified, matching the state in the MR
|
|
-- TODO: ensure correct CWD?
|
|
return vim.fn.trim(vim.fn.system({ "git", "status", "--short", "--untracked-files=no" })) == ""
|
|
end
|
|
|
|
M.open = function()
|
|
local diff_refs = state.INFO.diff_refs
|
|
if diff_refs == nil then
|
|
u.notify("Gitlab did not provide diff refs required to review this MR", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
if diff_refs.base_sha == "" or diff_refs.head_sha == "" then
|
|
u.notify("Merge request contains no changes", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
local diffview_open_command = "DiffviewOpen"
|
|
local diffview_feature_imply_local = {
|
|
user_requested = state.settings.reviewer_settings.diffview.imply_local,
|
|
usable = all_git_manged_files_unmodified(),
|
|
}
|
|
if diffview_feature_imply_local.user_requested and diffview_feature_imply_local.usable then
|
|
diffview_open_command = diffview_open_command .. " --imply-local"
|
|
end
|
|
|
|
vim.api.nvim_command(string.format("%s %s..%s", diffview_open_command, diff_refs.base_sha, diff_refs.head_sha))
|
|
M.tabnr = vim.api.nvim_get_current_tabpage()
|
|
|
|
if diffview_feature_imply_local.user_requested and not diffview_feature_imply_local.usable then
|
|
u.notify(
|
|
"There are uncommited changes in the working tree, cannot use 'imply_local' setting for gitlab reviews. Stash or commit all changes to use.",
|
|
vim.log.levels.WARN
|
|
)
|
|
end
|
|
|
|
if state.INFO.has_conflicts then
|
|
u.notify("This merge request has conflicts!", vim.log.levels.WARN)
|
|
end
|
|
|
|
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.close", {})
|
|
vim.api.nvim_create_autocmd("User", {
|
|
pattern = { "DiffviewViewClosed" },
|
|
group = group,
|
|
callback = function()
|
|
--Check if our diffview tab was closed
|
|
if vim.api.nvim_tabpage_is_valid(M.tabnr) then
|
|
M.tabnr = nil
|
|
end
|
|
end,
|
|
})
|
|
|
|
if state.settings.discussion_tree.auto_open then
|
|
local discussions = require("gitlab.actions.discussions")
|
|
discussions.close()
|
|
discussions.toggle()
|
|
end
|
|
end
|
|
|
|
M.close = function()
|
|
vim.cmd("DiffviewClose")
|
|
local discussions = require("gitlab.actions.discussions")
|
|
discussions.close()
|
|
end
|
|
|
|
M.jump = function(file_name, new_line, old_line, opts)
|
|
if M.tabnr == nil then
|
|
u.notify("Can't jump to Diffvew. Is it open?", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
vim.api.nvim_set_current_tabpage(M.tabnr)
|
|
vim.cmd("DiffviewFocusFiles")
|
|
local view = diffview_lib.get_current_view()
|
|
if view == nil then
|
|
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
local files = view.panel:ordered_file_list()
|
|
local layout = view.cur_layout
|
|
for _, file in ipairs(files) do
|
|
if file.path == file_name then
|
|
if not async_ok then
|
|
u.notify("Could not load Diffview async", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
async.await(view:set_file(file))
|
|
-- TODO: Ranged comments on unchanged lines will have both a
|
|
-- new line and a old line.
|
|
--
|
|
-- The same is true when the user leaves a single-line comment
|
|
-- on an unchanged line in the "b" buffer.
|
|
--
|
|
-- We need to distinguish them somehow from
|
|
-- range comments (which also have this) so that we can know
|
|
-- which buffer to jump to. Right now, we jump to the wrong
|
|
-- buffer for ranged comments on unchanged lines.
|
|
if new_line ~= nil and not opts.is_undefined_type then
|
|
layout.b:focus()
|
|
vim.api.nvim_win_set_cursor(0, { tonumber(new_line), 0 })
|
|
elseif old_line ~= nil then
|
|
layout.a:focus()
|
|
vim.api.nvim_win_set_cursor(0, { tonumber(old_line), 0 })
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
---Get the location of a line within the diffview. If range is specified, then also the location
|
|
---of the lines in range.
|
|
---@param range LineRange | nil Line range to get location for
|
|
---@return ReviewerInfo | nil nil is returned only if error was encountered
|
|
M.get_location = function(range)
|
|
if M.tabnr == nil then
|
|
u.notify("Diffview reviewer must be initialized first", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
local bufnr = vim.api.nvim_get_current_buf()
|
|
|
|
-- If there's a range, use the start of the visual selection, not the current line
|
|
local current_line = range and range.start_line or vim.api.nvim_win_get_cursor(0)[1]
|
|
|
|
-- check if we are in the diffview tab
|
|
local tabnr = vim.api.nvim_get_current_tabpage()
|
|
if tabnr ~= M.tabnr then
|
|
u.notify("Line location can only be determined within reviewer window", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
-- check if we are in the diffview buffer
|
|
local view = diffview_lib.get_current_view()
|
|
if view == nil then
|
|
u.notify("Could not find Diffview view", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
local layout = view.cur_layout
|
|
local result = {}
|
|
local type
|
|
local is_new
|
|
|
|
if
|
|
layout.a.file.bufnr == bufnr
|
|
or (M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil)
|
|
then
|
|
result.file_name = layout.a.file.path
|
|
result.old_line = current_line
|
|
type = "old"
|
|
is_new = false
|
|
elseif layout.b.file.bufnr == bufnr then
|
|
result.file_name = layout.b.file.path
|
|
result.new_line = current_line
|
|
type = "new"
|
|
is_new = true
|
|
else
|
|
u.notify("Line location can only be determined within reviewer window")
|
|
return
|
|
end
|
|
|
|
local hunks = u.parse_hunk_headers(result.file_name, state.INFO.target_branch)
|
|
if hunks == nil then
|
|
u.notify("Could not parse hunks", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
|
|
local current_line_info
|
|
if is_new then
|
|
current_line_info = u.get_lines_from_hunks(hunks, result.new_line, is_new)
|
|
else
|
|
current_line_info = u.get_lines_from_hunks(hunks, result.old_line, is_new)
|
|
end
|
|
|
|
-- If single line comment is outside of changed lines then we need to specify both new line and old line
|
|
-- otherwise the API returns error.
|
|
-- https://docs.gitlab.com/ee/api/discussions.html#create-a-new-thread-in-the-merge-request-diff
|
|
if not current_line_info.in_hunk then
|
|
result.old_line = current_line_info.old_line
|
|
result.new_line = current_line_info.new_line
|
|
end
|
|
|
|
-- If users leave single-line comments in the new buffer that should be in the old buffer, we can
|
|
-- tell because the line will not have changed. Send the correct payload.
|
|
if M.lines_are_same(view.cur_layout) and layout.b.file.bufnr == bufnr and range == nil then
|
|
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
|
|
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
|
|
result.old_line = a_cursor
|
|
result.new_line = a_cursor
|
|
type = "old"
|
|
end
|
|
|
|
if range == nil then
|
|
return result
|
|
end
|
|
|
|
result.range_info = { start = {}, ["end"] = {} }
|
|
if current_line == range.start_line then
|
|
result.range_info.start.old_line = current_line_info.old_line
|
|
result.range_info.start.new_line = current_line_info.new_line
|
|
result.range_info.start.type = type
|
|
else
|
|
local start_line_info = u.get_lines_from_hunks(hunks, range.start_line, is_new)
|
|
result.range_info.start.old_line = start_line_info.old_line
|
|
result.range_info.start.new_line = start_line_info.new_line
|
|
result.range_info.start.type = type
|
|
end
|
|
|
|
if current_line == range.end_line then
|
|
result.range_info["end"].old_line = current_line_info.old_line
|
|
result.range_info["end"].new_line = current_line_info.new_line
|
|
result.range_info["end"].type = type
|
|
else
|
|
local end_line_info = u.get_lines_from_hunks(hunks, range.end_line, is_new)
|
|
result.range_info["end"].old_line = end_line_info.old_line
|
|
result.range_info["end"].new_line = end_line_info.new_line
|
|
result.range_info["end"].type = type
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
---Return content between start_line and end_line
|
|
---@param start_line integer
|
|
---@param end_line integer
|
|
---@return string[]
|
|
M.get_lines = function(start_line, end_line)
|
|
return vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
|
|
end
|
|
|
|
---@return boolean
|
|
M.lines_are_same = function(layout)
|
|
local a_win = u.get_win_from_buf(layout.a.file.bufnr)
|
|
local b_win = u.get_win_from_buf(layout.b.file.bufnr)
|
|
local a_cursor = vim.api.nvim_win_get_cursor(a_win)[1]
|
|
local b_cursor = vim.api.nvim_win_get_cursor(b_win)[1]
|
|
local line_a = u.get_line_content(layout.a.file.bufnr, a_cursor)
|
|
local line_b = u.get_line_content(layout.b.file.bufnr, b_cursor)
|
|
return line_a == line_b
|
|
end
|
|
|
|
---Get currently shown file
|
|
M.get_current_file = function()
|
|
local view = diffview_lib.get_current_view()
|
|
if not view then
|
|
return
|
|
end
|
|
return view.panel.cur_file.path
|
|
end
|
|
|
|
---Place a sign in currently reviewed file. Use new line for identifing lines after changes, old
|
|
---line for identifing lines before changes and both if line was not changed.
|
|
---@param signs SignTable[] table of signs. See :h sign_placelist
|
|
---@param type string "new" if diagnostic should be in file after changes else "old"
|
|
M.place_sign = function(signs, type)
|
|
local view = diffview_lib.get_current_view()
|
|
if not view then
|
|
return
|
|
end
|
|
if type == "new" then
|
|
for _, sign in ipairs(signs) do
|
|
sign.buffer = view.cur_layout.b.file.bufnr
|
|
end
|
|
elseif type == "old" then
|
|
for _, sign in ipairs(signs) do
|
|
sign.buffer = view.cur_layout.a.file.bufnr
|
|
end
|
|
end
|
|
vim.fn.sign_placelist(signs)
|
|
end
|
|
|
|
---Set diagnostics in currently reviewed file.
|
|
---@param namespace integer namespace for diagnostics
|
|
---@param diagnostics table see :h vim.diagnostic.set
|
|
---@param type string "new" if diagnostic should be in file after changes else "old"
|
|
---@param opts table? see :h vim.diagnostic.set
|
|
M.set_diagnostics = function(namespace, diagnostics, type, opts)
|
|
local view = diffview_lib.get_current_view()
|
|
if not view then
|
|
return
|
|
end
|
|
if type == "new" and view.cur_layout.b.file.bufnr then
|
|
vim.diagnostic.set(namespace, view.cur_layout.b.file.bufnr, diagnostics, opts)
|
|
elseif type == "old" and view.cur_layout.a.file.bufnr then
|
|
vim.diagnostic.set(namespace, view.cur_layout.a.file.bufnr, diagnostics, opts)
|
|
end
|
|
end
|
|
|
|
---Diffview exposes events which can be used to setup autocommands.
|
|
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
|
M.set_callback_for_file_changed = function(callback)
|
|
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.file_changed", {})
|
|
vim.api.nvim_create_autocmd("User", {
|
|
pattern = { "DiffviewDiffBufWinEnter", "DiffviewViewEnter" },
|
|
group = group,
|
|
callback = function(...)
|
|
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
|
callback(...)
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
---Diffview exposes events which can be used to setup autocommands.
|
|
---@param callback fun(opts: table) - for more information about opts see callback in :h nvim_create_autocmd
|
|
M.set_callback_for_reviewer_leave = function(callback)
|
|
local group = vim.api.nvim_create_augroup("gitlab.diffview.autocommand.leave", {})
|
|
vim.api.nvim_create_autocmd("User", {
|
|
pattern = { "DiffviewViewLeave", "DiffviewViewClosed" },
|
|
group = group,
|
|
callback = function(...)
|
|
if M.tabnr == vim.api.nvim_get_current_tabpage() then
|
|
callback(...)
|
|
end
|
|
end,
|
|
})
|
|
end
|
|
|
|
return M
|