This MR is a #MAJOR breaking change to the plugin. While the plugin will continue to work for users with their existing settings, they will be informed of outdated configuration (diagnostics and signs have been simplified) the next time they open the reviewer. Fix: Trim trailing slash from custom URLs Update: .github/CONTRIBUTING.md, .github/ISSUE_TEMPLATE/bug_report.md Feat: Improve discussion tree toggling (#192) Fix: Toggle modified notes (#188) Fix: Toggle discussion nodes correctly Feat: Show Help keymap in discussion tree winbar Fix: Enable toggling nodes from the note body Fix: Enable toggling resolved status from child nodes Fix: Only try to show emoji popup on note nodes Feat: Add keymap for toggling tree type Fix: Disable tree type toggling in Notes Fix Multi Line Issues (Large Refactor) (#197) Fix: Multi-line discussions. The calculation of a range for a multiline comment has been consolidated and moved into the location.lua file. This does not attempt to fix diagnostics. Refactor: It refactors the discussions code to split hunk parsing and management into a separate module Fix: Don't allow comments on modified buffers #194 by preventing comments on the reviewer when using --imply-local and when the working tree is dirty entirely. Refactor: It introduces a new List class for data aggregation, filtering, etc. Fix: It removes redundant API calls and refreshes from the discussion pane Fix: Location provider (#198) Fix: add nil check for Diffview performance issue (#199) Fix: Switch Tabs During Comment Creation (#200) Fix: Check if file is modified (#201) Fix: Off-By-One Issue in Old SHA (#202) Fix: Rebuild Diagnostics + Signs (#203) Fix: Off-By-One Issue in New SHA (#205) Fix: Reviewer Jumps to wrong location (#206) BREAKING CHANGE: Changes configuration of diagnostics and signs in the setup call.
281 lines
9.5 KiB
Lua
281 lines
9.5 KiB
Lua
local List = require("gitlab.utils.list")
|
|
local u = require("gitlab.utils")
|
|
local state = require("gitlab.state")
|
|
local M = {}
|
|
|
|
---@class Hunk
|
|
---@field old_line integer
|
|
---@field old_range integer
|
|
---@field new_line integer
|
|
---@field new_range integer
|
|
|
|
---@class HunksAndDiff
|
|
---@field hunks Hunk[] list of hunks
|
|
---@field all_diff_output table The data from the git diff command
|
|
|
|
---Turn hunk line into Lua table
|
|
---@param line table
|
|
---@return Hunk|nil
|
|
M.parse_possible_hunk_headers = function(line)
|
|
if line:sub(1, 2) == "@@" then
|
|
-- match:
|
|
-- @@ -23 +23 @@ ...
|
|
-- @@ -23,0 +23 @@ ...
|
|
-- @@ -41,0 +42,4 @@ ...
|
|
local old_start, old_range, new_start, new_range = line:match("@@+ %-(%d+),?(%d*) %+(%d+),?(%d*) @@+")
|
|
|
|
return {
|
|
old_line = tonumber(old_start),
|
|
old_range = tonumber(old_range) or 0,
|
|
new_line = tonumber(new_start),
|
|
new_range = tonumber(new_range) or 0,
|
|
}
|
|
end
|
|
end
|
|
---@param linnr number
|
|
---@param hunk Hunk
|
|
---@param all_diff_output table
|
|
---@return boolean
|
|
local line_was_removed = function(linnr, hunk, all_diff_output)
|
|
for matching_line_index, line in ipairs(all_diff_output) do
|
|
local found_hunk = M.parse_possible_hunk_headers(line)
|
|
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
|
-- We found a matching hunk, now we need to iterate over the lines from the raw diff output
|
|
-- at that hunk until we reach the line we are looking for. When the indexes match we check
|
|
-- to see if that line is deleted or not.
|
|
for hunk_line_index = found_hunk.old_line, hunk.old_line + hunk.old_range, 1 do
|
|
local line_content = all_diff_output[matching_line_index + 1]
|
|
if hunk_line_index == linnr then
|
|
if string.match(line_content, "^%-") then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
---@param linnr number
|
|
---@param hunk Hunk
|
|
---@param all_diff_output table
|
|
---@return boolean
|
|
local line_was_added = function(linnr, hunk, all_diff_output)
|
|
for matching_line_index, line in ipairs(all_diff_output) do
|
|
local found_hunk = M.parse_possible_hunk_headers(line)
|
|
if found_hunk ~= nil and vim.deep_equal(found_hunk, hunk) then
|
|
-- For added lines, we only want to iterate over the part of the diff that has has new lines,
|
|
-- so we skip over the old range. We then keep track of the increment to the original new line index,
|
|
-- and iterate until we reach the end of the total range of this hunk. If we arrive at the matching
|
|
-- index for the line number, we check to see if the line was added.
|
|
local i = 0
|
|
local old_range = (found_hunk.old_range == 0 and found_hunk.old_line ~= 0) and 1 or found_hunk.old_range
|
|
for hunk_line_index = matching_line_index + old_range, matching_line_index + old_range + found_hunk.new_range, 1 do
|
|
local line_content = all_diff_output[hunk_line_index]
|
|
if (found_hunk.new_line + i) == linnr then
|
|
if string.match(line_content, "^%+") then
|
|
return true
|
|
end
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
---Parse git diff hunks.
|
|
---@param file_path string Path to file.
|
|
---@param base_branch string Git base branch of merge request.
|
|
---@return HunksAndDiff
|
|
local parse_hunks_and_diff = function(file_path, base_branch)
|
|
local hunks = {}
|
|
local all_diff_output = {}
|
|
|
|
local Job = require("plenary.job")
|
|
|
|
local diff_job = Job:new({
|
|
command = "git",
|
|
args = { "diff", "--minimal", "--unified=0", "--no-color", base_branch, "--", file_path },
|
|
on_exit = function(j, return_code)
|
|
if return_code == 0 then
|
|
all_diff_output = j:result()
|
|
for _, line in ipairs(all_diff_output) do
|
|
local hunk = M.parse_possible_hunk_headers(line)
|
|
if hunk ~= nil then
|
|
table.insert(hunks, hunk)
|
|
end
|
|
end
|
|
else
|
|
M.notify("Failed to get git diff: " .. j:stderr(), vim.log.levels.WARN)
|
|
end
|
|
end,
|
|
})
|
|
|
|
diff_job:sync()
|
|
|
|
return { hunks = hunks, all_diff_output = all_diff_output }
|
|
end
|
|
|
|
-- Parses the lines from a diff and returns the
|
|
-- index of the next hunk, when provided an initial index
|
|
---@param lines table
|
|
---@param i integer
|
|
---@return integer|nil
|
|
local next_hunk_index = function(lines, i)
|
|
for j, line in ipairs(lines) do
|
|
local hunk = M.parse_possible_hunk_headers(line)
|
|
if hunk ~= nil and j > i then
|
|
return j
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Processes the number of changes until the target is reached. This returns
|
|
--- a negative or positive number indicating the number of lines in the hunk
|
|
--that have been added or removed prior to the target line
|
|
---comment
|
|
---@param line_number number
|
|
---@param hunk Hunk
|
|
---@param lines table
|
|
---@return integer
|
|
local net_changed_in_hunk_before_line = function(line_number, hunk, lines)
|
|
local net_lines = 0
|
|
local current_line_old = hunk.old_line
|
|
|
|
for _, line in ipairs(lines) do
|
|
if line:sub(1, 1) == "-" then
|
|
if current_line_old < line_number then
|
|
net_lines = net_lines - 1
|
|
end
|
|
current_line_old = current_line_old + 1
|
|
elseif line:sub(1, 1) == "+" then
|
|
if current_line_old < line_number then
|
|
net_lines = net_lines + 1
|
|
end
|
|
else
|
|
current_line_old = current_line_old + 1
|
|
end
|
|
end
|
|
|
|
return net_lines
|
|
end
|
|
|
|
---Counts the total number of changes in a set of lines, can be positive if added lines or negative if removed lines
|
|
---@param lines table
|
|
---@return integer
|
|
local count_changes = function(lines)
|
|
local total = 0
|
|
for _, line in ipairs(lines) do
|
|
if line:match("^%+") then
|
|
total = total + 1
|
|
else
|
|
total = total - 1
|
|
end
|
|
end
|
|
return total
|
|
end
|
|
|
|
---@param new_line number|nil
|
|
---@param hunks Hunk[]
|
|
---@param all_diff_output table
|
|
---@return string|nil
|
|
local function get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
|
|
if new_line == nil then
|
|
return nil
|
|
end
|
|
return List.new(hunks):find(function(hunk)
|
|
local new_line_end = hunk.new_line + hunk.new_range
|
|
local in_new_range = new_line >= hunk.new_line and new_line <= new_line_end
|
|
local is_range_zero = hunk.new_range == 0 and hunk.old_range == 0
|
|
return in_new_range and (is_range_zero or line_was_added(new_line, hunk, all_diff_output))
|
|
end) and "added" or "bad_file_unmodified"
|
|
end
|
|
|
|
---@param old_line number|nil
|
|
---@param new_line number|nil
|
|
---@param hunks Hunk[]
|
|
---@param all_diff_output table
|
|
---@return string|nil
|
|
local function get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
|
|
if old_line == nil then
|
|
return nil
|
|
end
|
|
|
|
return List.new(hunks):find(function(hunk)
|
|
local old_line_end = hunk.old_line + hunk.old_range
|
|
local new_line_end = hunk.new_line + hunk.new_range
|
|
local in_old_range = old_line >= hunk.old_line and old_line <= old_line_end
|
|
local in_new_range = old_line >= hunk.new_line and new_line <= new_line_end
|
|
return (in_old_range or in_new_range) and line_was_removed(old_line, hunk, all_diff_output)
|
|
end) and "deleted" or "unmodified"
|
|
end
|
|
|
|
---Returns whether the comment is on a deleted line, added line, or unmodified line.
|
|
---This is in order to build the payload for Gitlab correctly by setting the old line and new line.
|
|
---@param old_line number|nil
|
|
---@param new_line number|nil
|
|
---@param current_file string
|
|
---@param is_current_sha_focused boolean
|
|
---@return string|nil
|
|
function M.get_modification_type(old_line, new_line, current_file, is_current_sha_focused)
|
|
local hunk_and_diff_data = parse_hunks_and_diff(current_file, state.INFO.target_branch)
|
|
if hunk_and_diff_data.hunks == nil then
|
|
return
|
|
end
|
|
|
|
local hunks = hunk_and_diff_data.hunks
|
|
local all_diff_output = hunk_and_diff_data.all_diff_output
|
|
return is_current_sha_focused and get_modification_type_from_new_sha(new_line, hunks, all_diff_output)
|
|
or get_modification_type_from_old_sha(old_line, new_line, hunks, all_diff_output)
|
|
end
|
|
|
|
---Returns the matching line number of a line in the new/old version of the file compared to the current SHA.
|
|
---@param old_sha string
|
|
---@param new_sha string
|
|
---@param file_path string
|
|
---@param line_number number
|
|
---@return number|nil
|
|
M.calculate_matching_line_new = function(old_sha, new_sha, file_path, line_number)
|
|
local net_change = 0
|
|
local diff_cmd = string.format("git diff --minimal --unified=0 --no-color %s %s -- %s", old_sha, new_sha, file_path)
|
|
local handle = io.popen(diff_cmd)
|
|
if handle == nil then
|
|
u.notify(string.format("Error running git diff command for %s", file_path), vim.log.levels.ERROR)
|
|
return nil
|
|
end
|
|
|
|
local all_lines = List.new({})
|
|
for line in handle:lines() do
|
|
table.insert(all_lines, line)
|
|
end
|
|
|
|
for i, line in ipairs(all_lines) do
|
|
local hunk = M.parse_possible_hunk_headers(line)
|
|
if hunk ~= nil then
|
|
if line_number <= hunk.old_line then
|
|
-- We have reached a hunk which starts after our target, return the changed total lines
|
|
return line_number + net_change
|
|
end
|
|
|
|
local n = next_hunk_index(all_lines, i) or #all_lines
|
|
local diff_lines = all_lines:slice(i + 1, n - 1)
|
|
|
|
-- If the line is IN the hunk, process the hunk and return the change until that line
|
|
if line_number >= hunk.old_line and line_number < hunk.old_line + hunk.old_range then
|
|
net_change = line_number + net_change + net_changed_in_hunk_before_line(line_number, hunk, diff_lines)
|
|
return net_change
|
|
end
|
|
|
|
-- If it's not it's after this hunk, just add all the changes and keep iterating
|
|
net_change = net_change + count_changes(diff_lines)
|
|
end
|
|
end
|
|
|
|
-- TODO: Possibly handle lines that are out of range in the new files
|
|
return line_number
|
|
end
|
|
|
|
return M
|