Release 2.5.1 (#271)
* feat: Support for custom authentication provider functions (#270) * feat: Support for adding "draft" notes to the review, and publishing them, either individually or all at once. Addresses feature request #223. * feat: Lets users select + checkout a merge request directly within Neovim, without exiting to the terminal * fix: Checks that the remote feature branch exists and is up-to-date before creating a MR, starting a review, or opening the MR summary (#278) * docs: We require some state from Diffview, this shows how to load that state prior to installing w/ Packer. Fixes #94. This is a #MINOR release. --------- Co-authored-by: Jakub F. Bortlík <jakub.bortlik@proton.me> Co-authored-by: sunfuze <sunfuze.1989@gmail.com> Co-authored-by: Patrick Pichler <mail@patrickpichler.dev>
This commit is contained in:
committed by
GitHub
parent
f10c4ebb8f
commit
cf6ccddce3
285
lua/gitlab/hunks.lua
Normal file
285
lua/gitlab/hunks.lua
Normal file
@@ -0,0 +1,285 @@
|
||||
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
|
||||
-- Parse the lines from the hunk and return only the added lines
|
||||
local hunk_lines = {}
|
||||
local i = 1
|
||||
local line_content = all_diff_output[matching_line_index + i]
|
||||
while line_content ~= nil and line_content:sub(1, 2) ~= "@@" do
|
||||
if string.match(line_content, "^%+") then
|
||||
table.insert(hunk_lines, line_content)
|
||||
end
|
||||
i = i + 1
|
||||
line_content = all_diff_output[matching_line_index + i]
|
||||
end
|
||||
|
||||
-- We are only looking at added lines in the changed hunk to see if their index
|
||||
-- matches the index of a line that was added
|
||||
local starting_index = found_hunk.new_line - 1 -- The "+j" will add one
|
||||
for j, _ in ipairs(hunk_lines) do
|
||||
if (starting_index + j) == linnr then
|
||||
return true
|
||||
end
|
||||
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 + net_change + 1
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user