Files
gitlab.nvim/lua/gitlab/hunks/init.lua
Harrison (Harry) Cramer b5b475ce8b 2.0.0 (#196)
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.
2024-03-03 11:52:37 -05:00

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