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
@@ -1,126 +1,46 @@
|
||||
-- This module is responsible for creating new comments
|
||||
-- in the reviewer's buffer. The reviewer will pass back
|
||||
-- to this module the data required to make the API calls
|
||||
--- This module is responsible for creating new comments
|
||||
--- in the reviewer's buffer. The reviewer will pass back
|
||||
--- to this module the data required to make the API calls
|
||||
local Popup = require("nui.popup")
|
||||
local Layout = require("nui.layout")
|
||||
local state = require("gitlab.state")
|
||||
local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local git = require("gitlab.git")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local draft_notes = require("gitlab.actions.draft_notes")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local Location = require("gitlab.reviewer.location")
|
||||
local M = {}
|
||||
|
||||
-- Popup creation is wrapped in a function so that it is performed *after* user
|
||||
-- configuration has been merged with default configuration, not when this file is being
|
||||
-- required.
|
||||
local function create_comment_popup()
|
||||
return Popup(u.create_popup_state("Comment", state.settings.popup.comment))
|
||||
end
|
||||
local M = {
|
||||
current_win = nil,
|
||||
start_line = nil,
|
||||
end_line = nil,
|
||||
}
|
||||
|
||||
-- This function will open a comment popup in order to create a comment on the changed/updated
|
||||
-- line in the current MR
|
||||
M.create_comment = function()
|
||||
local has_clean_tree = git.has_clean_tree()
|
||||
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||
u.notify(
|
||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
local comment_popup = create_comment_popup()
|
||||
comment_popup:mount()
|
||||
state.set_popup_keymaps(comment_popup, function(text)
|
||||
M.confirm_create_comment(text)
|
||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
||||
end
|
||||
|
||||
---Create multiline comment for the last selection.
|
||||
M.create_multiline_comment = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
local comment_popup = create_comment_popup()
|
||||
local start_line, end_line = u.get_visual_selection_boundaries()
|
||||
comment_popup:mount()
|
||||
state.set_popup_keymaps(comment_popup, function(text)
|
||||
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
|
||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
||||
end
|
||||
|
||||
---Create comment prepopulated with gitlab suggestion
|
||||
---https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
|
||||
M.create_comment_suggestion = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
local comment_popup = create_comment_popup()
|
||||
local start_line, end_line = u.get_visual_selection_boundaries()
|
||||
local current_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local range = end_line - start_line
|
||||
local backticks = "```"
|
||||
local selected_lines = u.get_lines(start_line, end_line)
|
||||
|
||||
for line in ipairs(selected_lines) do
|
||||
if string.match(line, "^```$") then
|
||||
backticks = "````"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local suggestion_start
|
||||
if start_line == current_line then
|
||||
suggestion_start = backticks .. "suggestion:-0+" .. range
|
||||
elseif end_line == current_line then
|
||||
suggestion_start = backticks .. "suggestion:-" .. range .. "+0"
|
||||
else
|
||||
-- This should never happen afaik
|
||||
u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
suggestion_start = suggestion_start
|
||||
local suggestion_lines = {}
|
||||
table.insert(suggestion_lines, suggestion_start)
|
||||
vim.list_extend(suggestion_lines, selected_lines)
|
||||
table.insert(suggestion_lines, backticks)
|
||||
|
||||
comment_popup:mount()
|
||||
vim.api.nvim_buf_set_lines(comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
||||
state.set_popup_keymaps(comment_popup, function(text)
|
||||
if range > 0 then
|
||||
M.confirm_create_comment(text, { start_line = start_line, end_line = end_line })
|
||||
else
|
||||
M.confirm_create_comment(text, nil)
|
||||
end
|
||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
||||
end
|
||||
|
||||
M.create_note = function()
|
||||
local note_popup = Popup(u.create_popup_state("Note", state.settings.popup.note))
|
||||
note_popup:mount()
|
||||
state.set_popup_keymaps(note_popup, function(text)
|
||||
M.confirm_create_comment(text, nil, true)
|
||||
end, miscellaneous.attach_file, miscellaneous.editable_popup_opts)
|
||||
end
|
||||
|
||||
---This function (settings.popup.perform_action) will send the comment to the Go server
|
||||
---Fires the API that sends the comment data to the Go server, called when you "confirm" creation
|
||||
---via the M.settings.popup.perform_action keybinding
|
||||
---@param text string comment text
|
||||
---@param visual_range LineRange | nil range of visual selection or nil
|
||||
---@param unlinked boolean | nil if true, the comment is not linked to a line
|
||||
M.confirm_create_comment = function(text, visual_range, unlinked)
|
||||
local confirm_create_comment = function(text, visual_range, unlinked)
|
||||
if text == nil then
|
||||
u.notify("Reviewer did not provide text of change", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local is_draft = M.draft_popup and u.string_to_bool(u.get_buffer_text(M.draft_popup.bufnr))
|
||||
if unlinked then
|
||||
local body = { comment = text }
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Note created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = true })
|
||||
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
||||
job.run_job(endpoint, "POST", body, function(data)
|
||||
u.notify(is_draft and "Draft note created!" or "Note created!", vim.log.levels.INFO)
|
||||
if is_draft then
|
||||
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = true })
|
||||
else
|
||||
discussions.add_discussion({ data = data, unlinked = true })
|
||||
end
|
||||
discussions.refresh()
|
||||
end)
|
||||
return
|
||||
@@ -153,11 +73,194 @@ M.confirm_create_comment = function(text, visual_range, unlinked)
|
||||
line_range = location_data.line_range,
|
||||
}
|
||||
|
||||
job.run_job("/mr/comment", "POST", body, function(data)
|
||||
u.notify("Comment created!", vim.log.levels.INFO)
|
||||
discussions.add_discussion({ data = data, unlinked = false })
|
||||
local endpoint = is_draft and "/mr/draft_notes/" or "/mr/comment"
|
||||
job.run_job(endpoint, "POST", body, function(data)
|
||||
u.notify(is_draft and "Draft comment created!" or "Comment created!", vim.log.levels.INFO)
|
||||
if is_draft then
|
||||
draft_notes.add_draft_note({ draft_note = data.draft_note, unlinked = false })
|
||||
else
|
||||
discussions.add_discussion({ data = data, has_position = true })
|
||||
end
|
||||
discussions.refresh()
|
||||
end)
|
||||
end
|
||||
|
||||
---@class LayoutOpts
|
||||
---@field ranged boolean
|
||||
---@field unlinked boolean
|
||||
|
||||
---This function sets up the layout and popups needed to create a comment, note and
|
||||
---multi-line comment. It also sets up the basic keybindings for switching between
|
||||
---window panes, and for the non-primary sections.
|
||||
---@param opts LayoutOpts|nil
|
||||
---@return NuiLayout
|
||||
local function create_comment_layout(opts)
|
||||
if opts == nil then
|
||||
opts = {}
|
||||
end
|
||||
|
||||
M.current_win = vim.api.nvim_get_current_win()
|
||||
M.comment_popup = Popup(u.create_popup_state("Comment", state.settings.popup.comment))
|
||||
M.draft_popup = Popup(u.create_box_popup_state("Draft", false))
|
||||
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||
|
||||
local internal_layout = Layout.Box({
|
||||
Layout.Box(M.comment_popup, { grow = 1 }),
|
||||
Layout.Box(M.draft_popup, { size = 3 }),
|
||||
}, { dir = "col" })
|
||||
|
||||
local layout = Layout({
|
||||
position = "50%",
|
||||
relative = "editor",
|
||||
size = {
|
||||
width = "50%",
|
||||
height = "55%",
|
||||
},
|
||||
}, internal_layout)
|
||||
|
||||
local popup_opts = {
|
||||
action_before_close = true,
|
||||
action_before_exit = false,
|
||||
}
|
||||
|
||||
miscellaneous.set_cycle_popups_keymaps({ M.comment_popup, M.draft_popup })
|
||||
|
||||
local range = opts.ranged and { start_line = M.start_line, end_line = M.end_line } or nil
|
||||
local unlinked = opts.unlinked or false
|
||||
|
||||
state.set_popup_keymaps(M.draft_popup, function()
|
||||
local text = u.get_buffer_text(M.comment_popup.bufnr)
|
||||
confirm_create_comment(text, range, unlinked)
|
||||
vim.api.nvim_set_current_win(M.current_win)
|
||||
end, miscellaneous.toggle_bool, popup_opts)
|
||||
|
||||
state.set_popup_keymaps(M.comment_popup, function(text)
|
||||
confirm_create_comment(text, range, unlinked)
|
||||
vim.api.nvim_set_current_win(M.current_win)
|
||||
end, miscellaneous.attach_file, popup_opts)
|
||||
|
||||
vim.schedule(function()
|
||||
local draft_mode = state.settings.discussion_tree.draft_mode
|
||||
vim.api.nvim_buf_set_lines(M.draft_popup.bufnr, 0, -1, false, { u.bool_to_string(draft_mode) })
|
||||
end)
|
||||
|
||||
return layout
|
||||
end
|
||||
|
||||
--- This function will open a comment popup in order to create a comment on the changed/updated
|
||||
--- line in the current MR
|
||||
M.create_comment = function()
|
||||
local has_clean_tree, err = git.has_clean_tree()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
local is_modified = vim.api.nvim_buf_get_option(0, "modified")
|
||||
if state.settings.reviewer_settings.diffview.imply_local and (is_modified or not has_clean_tree) then
|
||||
u.notify(
|
||||
"Cannot leave comments on changed files. \n Please stash all local changes or push them to the feature branch.",
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if not M.sha_exists() then
|
||||
return
|
||||
end
|
||||
|
||||
local layout = create_comment_layout()
|
||||
layout:mount()
|
||||
end
|
||||
|
||||
--- This function will open a multi-line comment popup in order to create a multi-line comment
|
||||
--- on the changed/updated line in the current MR
|
||||
M.create_multiline_comment = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
if not M.sha_exists() then
|
||||
return
|
||||
end
|
||||
|
||||
local layout = create_comment_layout({ ranged = true, unlinked = false })
|
||||
layout:mount()
|
||||
end
|
||||
|
||||
--- This function will open a a popup to create a "note" (e.g. unlinked comment)
|
||||
--- on the changed/updated line in the current MR
|
||||
M.create_note = function()
|
||||
local layout = create_comment_layout({ ranged = false, unlinked = true })
|
||||
layout:mount()
|
||||
end
|
||||
|
||||
---Given the current visually selected area of text, builds text to fill in the
|
||||
---comment popup with a suggested change
|
||||
---@return LineRange|nil
|
||||
---@return integer
|
||||
local build_suggestion = function()
|
||||
local current_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
M.start_line, M.end_line = u.get_visual_selection_boundaries()
|
||||
|
||||
local range_length = M.end_line - M.start_line
|
||||
local backticks = "```"
|
||||
local selected_lines = u.get_lines(M.start_line, M.end_line)
|
||||
|
||||
for line in ipairs(selected_lines) do
|
||||
if string.match(line, "^```$") then
|
||||
backticks = "````"
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local suggestion_start
|
||||
if M.start_line == current_line then
|
||||
suggestion_start = backticks .. "suggestion:-0+" .. range_length
|
||||
elseif M.end_line == current_line then
|
||||
suggestion_start = backticks .. "suggestion:-" .. range_length .. "+0"
|
||||
else
|
||||
--- This should never happen afaik
|
||||
u.notify("Unexpected suggestion position", vim.log.levels.ERROR)
|
||||
return nil, 0
|
||||
end
|
||||
suggestion_start = suggestion_start
|
||||
local suggestion_lines = {}
|
||||
table.insert(suggestion_lines, suggestion_start)
|
||||
vim.list_extend(suggestion_lines, selected_lines)
|
||||
table.insert(suggestion_lines, backticks)
|
||||
|
||||
return suggestion_lines, range_length
|
||||
end
|
||||
|
||||
--- This function will open a a popup to create a suggestion comment
|
||||
--- on the changed/updated line in the current MR
|
||||
--- See: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/suggestions.html
|
||||
M.create_comment_suggestion = function()
|
||||
if not u.check_visual_mode() then
|
||||
return
|
||||
end
|
||||
if not M.sha_exists() then
|
||||
return
|
||||
end
|
||||
|
||||
local suggestion_lines, range_length = build_suggestion()
|
||||
|
||||
local layout = create_comment_layout({ ranged = range_length > 0, unlinked = false })
|
||||
layout:mount()
|
||||
vim.schedule(function()
|
||||
if suggestion_lines then
|
||||
vim.api.nvim_buf_set_lines(M.comment_popup.bufnr, 0, -1, false, suggestion_lines)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Checks to see whether you are commenting on a valid buffer. The Diffview plugin names non-existent
|
||||
---buffers as 'null'
|
||||
---@return boolean
|
||||
M.sha_exists = function()
|
||||
if vim.fn.expand("%") == "diffview://null" then
|
||||
u.notify("This file does not exist, please comment on the other buffer", vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
281
lua/gitlab/actions/common.lua
Normal file
281
lua/gitlab/actions/common.lua
Normal file
@@ -0,0 +1,281 @@
|
||||
-- This module contains code shared between at least two modules. This includes
|
||||
-- actions common to multiple tree types, as well as general utility functions
|
||||
-- that are specific to actions (like jumping to a file or opening a URL)
|
||||
local List = require("gitlab.utils.list")
|
||||
local u = require("gitlab.utils")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local indicators_common = require("gitlab.indicators.common")
|
||||
local common_indicators = require("gitlab.indicators.common")
|
||||
local state = require("gitlab.state")
|
||||
local M = {}
|
||||
|
||||
---Build note header from note
|
||||
---@param note Note|DraftNote
|
||||
---@return string
|
||||
M.build_note_header = function(note)
|
||||
if note.note then
|
||||
return "@" .. state.USER.username .. " " .. ""
|
||||
end
|
||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||
end
|
||||
|
||||
M.switch_can_edit_bufs = function(bool, ...)
|
||||
local bufnrs = { ... }
|
||||
---@param v integer
|
||||
for _, v in ipairs(bufnrs) do
|
||||
u.switch_can_edit_buf(v, bool)
|
||||
vim.api.nvim_set_option_value("filetype", "gitlab", { buf = v })
|
||||
end
|
||||
end
|
||||
|
||||
---Takes in a chunk of text separated by new line characters and returns a lua table
|
||||
---@param content string
|
||||
---@return table
|
||||
M.build_content = function(content)
|
||||
local description_lines = u.lines_into_table(content)
|
||||
table.insert(description_lines, "")
|
||||
return description_lines
|
||||
end
|
||||
|
||||
M.add_empty_titles = function()
|
||||
local draft_notes = require("gitlab.actions.draft_notes")
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
local linked, unlinked, drafts =
|
||||
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.discussions)),
|
||||
List.new(u.ensure_table(state.DISCUSSION_DATA and state.DISCUSSION_DATA.unlinked_discussions)),
|
||||
List.new(u.ensure_table(state.DRAFT_NOTES))
|
||||
|
||||
local position_drafts = drafts:filter(function(note)
|
||||
return draft_notes.has_position(note)
|
||||
end)
|
||||
local non_positioned_drafts = drafts:filter(function(note)
|
||||
return not draft_notes.has_position(note)
|
||||
end)
|
||||
|
||||
local fields = {
|
||||
{
|
||||
bufnr = discussions.linked_bufnr,
|
||||
count = #linked + #position_drafts,
|
||||
title = "No Discussions for this MR",
|
||||
},
|
||||
{
|
||||
bufnr = discussions.unlinked_bufnr,
|
||||
count = #unlinked + #non_positioned_drafts,
|
||||
title = "No Notes (Unlinked Discussions) for this MR",
|
||||
},
|
||||
}
|
||||
|
||||
for _, v in ipairs(fields) do
|
||||
if v.bufnr ~= nil then
|
||||
M.switch_can_edit_bufs(true, v.bufnr)
|
||||
local ns_id = vim.api.nvim_create_namespace("GitlabNamespace")
|
||||
vim.cmd("highlight default TitleHighlight guifg=#787878")
|
||||
|
||||
-- Set empty title if applicable
|
||||
if v.count == 0 then
|
||||
vim.api.nvim_buf_set_lines(v.bufnr, 0, 1, false, { v.title })
|
||||
local linnr = 1
|
||||
vim.api.nvim_buf_set_extmark(
|
||||
v.bufnr,
|
||||
ns_id,
|
||||
linnr - 1,
|
||||
0,
|
||||
{ end_row = linnr - 1, end_col = string.len(v.title), hl_group = "TitleHighlight" }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param tree NuiTree
|
||||
M.get_url = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
local note_node = M.get_note_node(tree, current_node)
|
||||
if note_node == nil then
|
||||
return
|
||||
end
|
||||
local url = note_node.url
|
||||
if url == nil then
|
||||
u.notify("Could not get URL of note", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
---@param tree NuiTree
|
||||
M.open_in_browser = function(tree)
|
||||
local url = M.get_url(tree)
|
||||
if url ~= nil then
|
||||
u.open_in_browser(url)
|
||||
end
|
||||
end
|
||||
|
||||
---@param tree NuiTree
|
||||
M.copy_node_url = function(tree)
|
||||
local url = M.get_url(tree)
|
||||
if url == nil then
|
||||
vim.fn.setreg("+", url)
|
||||
u.notify("Copied '" .. url .. "' to clipboard", vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
-- For developers!
|
||||
M.print_node = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
vim.print(current_node)
|
||||
end
|
||||
|
||||
---Check if type of node is note or note body
|
||||
---@param node NuiTree.Node?
|
||||
---@return boolean
|
||||
M.is_node_note = function(node)
|
||||
if node and (node.type == "note_body" or node.type == "note") then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
---Get root node
|
||||
---@param tree NuiTree
|
||||
---@param node NuiTree.Node?
|
||||
---@return NuiTree.Node?
|
||||
M.get_root_node = function(tree, node)
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
if node.type == "note_body" or node.type == "note" and not node.is_root then
|
||||
local parent_id = node:get_parent_id()
|
||||
return M.get_root_node(tree, tree:get_node(parent_id))
|
||||
elseif node.is_root then
|
||||
return node
|
||||
end
|
||||
end
|
||||
|
||||
---Get note node
|
||||
---@param tree NuiTree
|
||||
---@param node NuiTree.Node?
|
||||
---@return NuiTree.Node?
|
||||
M.get_note_node = function(tree, node)
|
||||
if not node then
|
||||
return nil
|
||||
end
|
||||
|
||||
if node.type == "note_body" then
|
||||
local parent_id = node:get_parent_id()
|
||||
if parent_id == nil then
|
||||
return node
|
||||
end
|
||||
return M.get_note_node(tree, tree:get_node(parent_id))
|
||||
elseif node.type == "note" then
|
||||
return node
|
||||
end
|
||||
end
|
||||
|
||||
---Takes a node and returns the line where the note is positioned in the new SHA. If
|
||||
---the line is not in the new SHA, returns nil
|
||||
---@param node NuiTree.Node
|
||||
---@return number|nil
|
||||
local function get_new_line(node)
|
||||
---@type GitlabLineRange|nil
|
||||
local range = node.range
|
||||
if range == nil then
|
||||
return node.new_line
|
||||
end
|
||||
|
||||
local _, start_new_line = common_indicators.parse_line_code(range.start.line_code)
|
||||
return start_new_line
|
||||
end
|
||||
|
||||
---Takes a node and returns the line where the note is positioned in the old SHA. If
|
||||
---the line is not in the old SHA, returns nil
|
||||
---@param node NuiTree.Node
|
||||
---@return number|nil
|
||||
local function get_old_line(node)
|
||||
---@type GitlabLineRange|nil
|
||||
local range = node.range
|
||||
if range == nil then
|
||||
return node.old_line
|
||||
end
|
||||
|
||||
local start_old_line, _ = common_indicators.parse_line_code(range.start.line_code)
|
||||
return start_old_line
|
||||
end
|
||||
|
||||
---@param id string|integer
|
||||
---@return integer|nil
|
||||
M.get_line_number = function(id)
|
||||
---@type Discussion|DraftNote|nil
|
||||
local d_or_n
|
||||
d_or_n = List.new(state.DISCUSSION_DATA.discussions or {}):find(function(d)
|
||||
return d.id == id
|
||||
end) or List.new(state.DRAFT_NOTES or {}):find(function(d)
|
||||
return d.id == id
|
||||
end)
|
||||
|
||||
if d_or_n == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local first_note = indicators_common.get_first_note(d_or_n)
|
||||
return (indicators_common.is_new_sha(d_or_n) and first_note.position.new_line or first_note.position.old_line) or 1
|
||||
end
|
||||
|
||||
---@param root_node NuiTree.Node
|
||||
---@return integer|nil
|
||||
M.get_line_number_from_node = function(root_node)
|
||||
if root_node.range then
|
||||
local start_old_line, start_new_line = common_indicators.parse_line_code(root_node.range.start.line_code)
|
||||
return root_node.old_line and start_old_line or start_new_line
|
||||
else
|
||||
return M.get_line_number(root_node.id)
|
||||
end
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.jump_to_reviewer) will jump the cursor to the reviewer's location associated with the note. The implementation depends on the reviewer
|
||||
M.jump_to_reviewer = function(tree, callback)
|
||||
local node = tree:get_node()
|
||||
local root_node = M.get_root_node(tree, node)
|
||||
if root_node == nil then
|
||||
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
local line_number = M.get_line_number_from_node(root_node)
|
||||
if line_number == nil then
|
||||
u.notify("Could not get line number", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
reviewer.jump(root_node.file_name, line_number, root_node.old_line == nil)
|
||||
callback()
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.jump_to_file) will jump to the file changed in a new tab
|
||||
M.jump_to_file = function(tree)
|
||||
local node = tree:get_node()
|
||||
local root_node = M.get_root_node(tree, node)
|
||||
if root_node == nil then
|
||||
u.notify("Could not get discussion node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
if root_node.file_name == nil then
|
||||
u.notify("This comment was not left on a particular location", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
vim.cmd.tabnew()
|
||||
local line_number = get_new_line(root_node) or get_old_line(root_node)
|
||||
if line_number == nil then
|
||||
line_number = 1
|
||||
end
|
||||
local bufnr = vim.fn.bufnr(root_node.file_name)
|
||||
if bufnr ~= -1 then
|
||||
vim.cmd("buffer " .. bufnr)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
return
|
||||
end
|
||||
|
||||
-- If buffer is not already open, open it
|
||||
vim.cmd("edit " .. root_node.file_name)
|
||||
vim.api.nvim_win_set_cursor(0, { line_number, 0 })
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -7,6 +7,7 @@ local job = require("gitlab.job")
|
||||
local u = require("gitlab.utils")
|
||||
local git = require("gitlab.git")
|
||||
local state = require("gitlab.state")
|
||||
local common = require("gitlab.actions.common")
|
||||
local miscellaneous = require("gitlab.actions.miscellaneous")
|
||||
|
||||
---@class Mr
|
||||
@@ -42,6 +43,10 @@ end
|
||||
--- continue working on it.
|
||||
---@param args? Mr
|
||||
M.start = function(args)
|
||||
if not git.current_branch_up_to_date_on_remote(vim.log.levels.ERROR) then
|
||||
return
|
||||
end
|
||||
|
||||
if M.started then
|
||||
vim.ui.select({ "Yes", "No" }, { prompt = "Continue your previous MR?" }, function(choice)
|
||||
if choice == "Yes" then
|
||||
@@ -82,7 +87,10 @@ M.pick_target = function(mr)
|
||||
end
|
||||
|
||||
local function make_template_path(t)
|
||||
local base_dir = git.base_dir()
|
||||
local base_dir, err = git.base_dir()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
return base_dir
|
||||
.. state.settings.file_separator
|
||||
.. ".gitlab"
|
||||
@@ -202,7 +210,7 @@ M.open_confirmation_popup = function(mr)
|
||||
M.layout_visible = false
|
||||
end
|
||||
|
||||
local description_lines = mr.description and M.build_description_lines(mr.description) or { "" }
|
||||
local description_lines = mr.description and common.build_content(mr.description) or { "" }
|
||||
local delete_branch = u.get_first_non_nil_value({ mr.delete_branch, state.settings.create_mr.delete_branch })
|
||||
local squash = u.get_first_non_nil_value({ mr.squash, state.settings.create_mr.squash })
|
||||
|
||||
@@ -234,18 +242,6 @@ M.open_confirmation_popup = function(mr)
|
||||
end)
|
||||
end
|
||||
|
||||
---Builds a lua list of strings that contain the MR description
|
||||
M.build_description_lines = function(template_content)
|
||||
local description_lines = {}
|
||||
for line in u.split_by_new_lines(template_content) do
|
||||
table.insert(description_lines, line)
|
||||
end
|
||||
-- TODO: @harrisoncramer Same as in lua/gitlab/actions/summary.lua:114
|
||||
table.insert(description_lines, "")
|
||||
|
||||
return description_lines
|
||||
end
|
||||
|
||||
---Prompts for interactive selection of a new target among remote-tracking branches
|
||||
M.select_new_target = function()
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
|
||||
@@ -9,6 +9,7 @@ local labels = state.dependencies.labels
|
||||
local project_members = state.dependencies.project_members
|
||||
local revisions = state.dependencies.revisions
|
||||
local latest_pipeline = state.dependencies.latest_pipeline
|
||||
local draft_notes = state.dependencies.draft_notes
|
||||
|
||||
M.data = function(resources, cb)
|
||||
if type(resources) ~= "table" or type(cb) ~= "function" then
|
||||
@@ -23,6 +24,7 @@ M.data = function(resources, cb)
|
||||
project_members = project_members,
|
||||
revisions = revisions,
|
||||
pipeline = latest_pipeline,
|
||||
draft_notes = draft_notes,
|
||||
}
|
||||
|
||||
local api_calls = {}
|
||||
|
||||
@@ -79,9 +79,11 @@
|
||||
---@field moji string
|
||||
|
||||
---@class WinbarTable
|
||||
---@field name string
|
||||
---@field view_type string
|
||||
---@field resolvable_discussions number
|
||||
---@field resolved_discussions number
|
||||
---@field inline_draft_notes number
|
||||
---@field unlinked_draft_notes number
|
||||
---@field resolvable_notes number
|
||||
---@field resolved_notes number
|
||||
---@field help_keymap string
|
||||
@@ -120,3 +122,14 @@
|
||||
---@field old_line integer | nil
|
||||
---@field new_line integer | nil
|
||||
---@field line_range ReviewerRangeInfo|nil
|
||||
|
||||
---@class DraftNote
|
||||
---@field note string
|
||||
---@field id integer
|
||||
---@field author_id integer
|
||||
---@field merge_request_id integer
|
||||
---@field resolve_discussion boolean
|
||||
---@field discussion_id string -- This will always be ""
|
||||
---@field commit_id string -- This will always be ""
|
||||
---@field line_code string
|
||||
---@field position NotePosition
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,149 +1,22 @@
|
||||
local state = require("gitlab.state")
|
||||
-- This module contains tree code specific to the discussion tree, that
|
||||
-- is not used in the draft notes tree
|
||||
local u = require("gitlab.utils")
|
||||
local common = require("gitlab.actions.common")
|
||||
local state = require("gitlab.state")
|
||||
local NuiTree = require("nui.tree")
|
||||
local NuiLine = require("nui.line")
|
||||
|
||||
local M = {}
|
||||
|
||||
local attach_uuid = function(str)
|
||||
return { text = str, id = u.uuid() }
|
||||
end
|
||||
|
||||
---Create path node
|
||||
---@param relative_path string
|
||||
---@param full_path string
|
||||
---@param child_nodes NuiTree.Node[]?
|
||||
---@return NuiTree.Node
|
||||
local function create_path_node(relative_path, full_path, child_nodes)
|
||||
return NuiTree.Node({
|
||||
text = relative_path,
|
||||
path = full_path,
|
||||
id = full_path,
|
||||
type = "path",
|
||||
icon = " ",
|
||||
icon_hl = "GitlabDirectoryIcon",
|
||||
text_hl = "GitlabDirectory",
|
||||
}, child_nodes or {})
|
||||
end
|
||||
|
||||
---Create file name node
|
||||
---@param file_name string
|
||||
---@param full_file_path string
|
||||
---@param child_nodes NuiTree.Node[]?
|
||||
---@return NuiTree.Node
|
||||
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
||||
local icon, icon_hl = u.get_icon(file_name)
|
||||
return NuiTree.Node({
|
||||
text = file_name,
|
||||
file_name = full_file_path,
|
||||
id = full_file_path,
|
||||
type = "file_name",
|
||||
icon = icon,
|
||||
icon_hl = icon_hl,
|
||||
text_hl = "GitlabFileName",
|
||||
}, child_nodes or {})
|
||||
end
|
||||
|
||||
---Sort list of nodes (in place) of type "path" or "file_name"
|
||||
---@param nodes NuiTree.Node[]
|
||||
local function sort_nodes(nodes)
|
||||
table.sort(nodes, function(node1, node2)
|
||||
if node1.type == "path" and node2.type == "path" then
|
||||
return node1.path < node2.path
|
||||
elseif node1.type == "file_name" and node2.type == "file_name" then
|
||||
return node1.file_name < node2.file_name
|
||||
elseif node1.type == "path" and node2.type == "file_name" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Merge path nodes which have only single path child
|
||||
---@param node NuiTree.Node
|
||||
local function flatten_nodes(node)
|
||||
if node.type ~= "path" then
|
||||
return
|
||||
end
|
||||
for _, child in ipairs(node.__children) do
|
||||
flatten_nodes(child)
|
||||
end
|
||||
if #node.__children == 1 and node.__children[1].type == "path" then
|
||||
local child = node.__children[1]
|
||||
node.__children = child.__children
|
||||
node.id = child.id
|
||||
node.path = child.path
|
||||
node.text = node.text .. u.path_separator .. child.text
|
||||
end
|
||||
sort_nodes(node.__children)
|
||||
end
|
||||
|
||||
---Build note header from note.
|
||||
---@param note Note
|
||||
---@return string
|
||||
M.build_note_header = function(note)
|
||||
return "@" .. note.author.username .. " " .. u.time_since(note.created_at)
|
||||
end
|
||||
|
||||
---Build note node body
|
||||
---@param note Note
|
||||
---@param resolve_info table?
|
||||
---@return string
|
||||
---@return NuiTree.Node[]
|
||||
local function build_note_body(note, resolve_info)
|
||||
local text_nodes = {}
|
||||
for bodyLine in u.split_by_new_lines(note.body) do
|
||||
local line = attach_uuid(bodyLine)
|
||||
table.insert(
|
||||
text_nodes,
|
||||
NuiTree.Node({
|
||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||
text = line.text,
|
||||
id = line.id,
|
||||
type = "note_body",
|
||||
}, {})
|
||||
)
|
||||
end
|
||||
|
||||
local resolve_symbol = ""
|
||||
if resolve_info ~= nil and resolve_info.resolvable then
|
||||
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||
or state.settings.discussion_tree.unresolved
|
||||
end
|
||||
|
||||
local noteHeader = M.build_note_header(note) .. " " .. resolve_symbol
|
||||
|
||||
return noteHeader, text_nodes
|
||||
end
|
||||
|
||||
---Build note node
|
||||
---@param note Note
|
||||
---@param resolve_info table?
|
||||
---@return NuiTree.Node
|
||||
---@return string
|
||||
---@return NuiTree.Node[]
|
||||
M.build_note = function(note, resolve_info)
|
||||
local text, text_nodes = build_note_body(note, resolve_info)
|
||||
local note_node = NuiTree.Node({
|
||||
text = text,
|
||||
id = note.id,
|
||||
file_name = (type(note.position) == "table" and note.position.new_path),
|
||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||
type = "note",
|
||||
}, text_nodes)
|
||||
|
||||
return note_node, text, text_nodes
|
||||
end
|
||||
|
||||
---Create nodes for NuiTree from discussions
|
||||
---@param items Discussion[]
|
||||
---@param unlinked boolean? False or nil means that discussions are linked to code lines
|
||||
---@return NuiTree.Node[]
|
||||
M.add_discussions_to_table = function(items, unlinked)
|
||||
local t = {}
|
||||
if items == vim.NIL then
|
||||
items = {}
|
||||
end
|
||||
for _, discussion in ipairs(items) do
|
||||
local discussion_children = {}
|
||||
|
||||
@@ -206,10 +79,85 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
return t
|
||||
end
|
||||
|
||||
return M.create_node_list_by_file_name(t)
|
||||
end
|
||||
|
||||
---Create path node
|
||||
---@param relative_path string
|
||||
---@param full_path string
|
||||
---@param child_nodes NuiTree.Node[]?
|
||||
---@return NuiTree.Node
|
||||
local function create_path_node(relative_path, full_path, child_nodes)
|
||||
return NuiTree.Node({
|
||||
text = relative_path,
|
||||
path = full_path,
|
||||
id = full_path,
|
||||
type = "path",
|
||||
icon = " ",
|
||||
icon_hl = "GitlabDirectoryIcon",
|
||||
text_hl = "GitlabDirectory",
|
||||
}, child_nodes or {})
|
||||
end
|
||||
|
||||
---Sort list of nodes (in place) of type "path" or "file_name"
|
||||
---@param nodes NuiTree.Node[]
|
||||
local function sort_nodes(nodes)
|
||||
table.sort(nodes, function(node1, node2)
|
||||
if node1.type == "path" and node2.type == "path" then
|
||||
return node1.path < node2.path
|
||||
elseif node1.type == "file_name" and node2.type == "file_name" then
|
||||
return node1.file_name < node2.file_name
|
||||
elseif node1.type == "path" and node2.type == "file_name" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Merge path nodes which have only single path child
|
||||
---@param node NuiTree.Node
|
||||
local function flatten_nodes(node)
|
||||
if node.type ~= "path" then
|
||||
return
|
||||
end
|
||||
for _, child in ipairs(node.__children) do
|
||||
flatten_nodes(child)
|
||||
end
|
||||
if #node.__children == 1 and node.__children[1].type == "path" then
|
||||
local child = node.__children[1]
|
||||
node.__children = child.__children
|
||||
node.id = child.id
|
||||
node.path = child.path
|
||||
node.text = node.text .. u.path_separator .. child.text
|
||||
end
|
||||
sort_nodes(node.__children)
|
||||
end
|
||||
|
||||
---Create file name node
|
||||
---@param file_name string
|
||||
---@param full_file_path string
|
||||
---@param child_nodes NuiTree.Node[]?
|
||||
---@return NuiTree.Node
|
||||
local function create_file_name_node(file_name, full_file_path, child_nodes)
|
||||
local icon, icon_hl = u.get_icon(file_name)
|
||||
return NuiTree.Node({
|
||||
text = file_name,
|
||||
file_name = full_file_path,
|
||||
id = full_file_path,
|
||||
type = "file_name",
|
||||
icon = icon,
|
||||
icon_hl = icon_hl,
|
||||
text_hl = "GitlabFileName",
|
||||
}, child_nodes or {})
|
||||
end
|
||||
|
||||
local create_disscussions_by_file_name = function(node_list)
|
||||
-- Create all the folder and file name nodes.
|
||||
local discussion_by_file_name = {}
|
||||
local top_level_path_to_node = {}
|
||||
for _, node in ipairs(t) do
|
||||
|
||||
for _, node in ipairs(node_list) do
|
||||
local path = ""
|
||||
local parent_node = nil
|
||||
local path_parts = u.split_path(node.file_name)
|
||||
@@ -274,13 +222,280 @@ M.add_discussions_to_table = function(items, unlinked)
|
||||
end
|
||||
end
|
||||
|
||||
return discussion_by_file_name
|
||||
end
|
||||
|
||||
M.create_node_list_by_file_name = function(node_list)
|
||||
-- Create all the folder and file name nodes.
|
||||
local discussion_by_file_name = create_disscussions_by_file_name(node_list)
|
||||
|
||||
-- Flatten empty folders
|
||||
for _, node in ipairs(discussion_by_file_name) do
|
||||
flatten_nodes(node)
|
||||
end
|
||||
|
||||
sort_nodes(discussion_by_file_name)
|
||||
|
||||
return discussion_by_file_name
|
||||
end
|
||||
|
||||
local attach_uuid = function(str)
|
||||
return { text = str, id = u.uuid() }
|
||||
end
|
||||
|
||||
---Build note node body
|
||||
---@param note Note|DraftNote
|
||||
---@param resolve_info table?
|
||||
---@return string
|
||||
---@return NuiTree.Node[]
|
||||
local function build_note_body(note, resolve_info)
|
||||
local text_nodes = {}
|
||||
for bodyLine in u.split_by_new_lines(note.body or note.note) do
|
||||
local line = attach_uuid(bodyLine)
|
||||
table.insert(
|
||||
text_nodes,
|
||||
NuiTree.Node({
|
||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||
text = line.text,
|
||||
id = line.id,
|
||||
type = "note_body",
|
||||
}, {})
|
||||
)
|
||||
end
|
||||
|
||||
local resolve_symbol = ""
|
||||
if resolve_info ~= nil and resolve_info.resolvable then
|
||||
resolve_symbol = resolve_info.resolved and state.settings.discussion_tree.resolved
|
||||
or state.settings.discussion_tree.unresolved
|
||||
end
|
||||
|
||||
local noteHeader = common.build_note_header(note) .. " " .. resolve_symbol
|
||||
|
||||
return noteHeader, text_nodes
|
||||
end
|
||||
|
||||
---Build note node
|
||||
---@param note Note|DraftNote
|
||||
---@param resolve_info table?
|
||||
---@return NuiTree.Node
|
||||
---@return string
|
||||
---@return NuiTree.Node[]
|
||||
M.build_note = function(note, resolve_info)
|
||||
local text, text_nodes = build_note_body(note, resolve_info)
|
||||
local note_node = NuiTree.Node({
|
||||
text = text,
|
||||
is_draft = note.note ~= nil,
|
||||
id = note.id,
|
||||
file_name = (type(note.position) == "table" and note.position.new_path),
|
||||
new_line = (type(note.position) == "table" and note.position.new_line),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line),
|
||||
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||
type = "note",
|
||||
}, text_nodes)
|
||||
|
||||
return note_node, text, text_nodes
|
||||
end
|
||||
|
||||
---Inspired by default func https://github.com/MunifTanjim/nui.nvim/blob/main/lua/nui/tree/util.lua#L38
|
||||
M.nui_tree_prepare_node = function(node)
|
||||
if not node.text then
|
||||
error("missing node.text")
|
||||
end
|
||||
|
||||
local texts = node.text
|
||||
if type(node.text) ~= "table" or node.text.content then
|
||||
texts = { node.text }
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
|
||||
for i, text in ipairs(texts) do
|
||||
local line = NuiLine()
|
||||
|
||||
line:append(string.rep(" ", node._depth - 1))
|
||||
|
||||
if i == 1 and node:has_children() then
|
||||
line:append(node:is_expanded() and " " or " ")
|
||||
if node.icon then
|
||||
line:append(node.icon .. " ", node.icon_hl)
|
||||
end
|
||||
else
|
||||
line:append(" ")
|
||||
end
|
||||
|
||||
line:append(text, node.text_hl)
|
||||
|
||||
local note_id = tostring(node.is_root and node.root_note_id or node.id)
|
||||
|
||||
local e = require("gitlab.emoji")
|
||||
|
||||
---@type Emoji[]
|
||||
local emojis = state.DISCUSSION_DATA.emojis[note_id]
|
||||
local placed_emojis = {}
|
||||
if emojis ~= nil then
|
||||
for _, v in ipairs(emojis) do
|
||||
local icon = e.emoji_map[v.name]
|
||||
if icon ~= nil and not u.contains(placed_emojis, icon.moji) then
|
||||
line:append(" ")
|
||||
line:append(icon.moji)
|
||||
table.insert(placed_emojis, icon.moji)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
---@class ToggleNodesOptions
|
||||
---@field toggle_resolved boolean Whether to toggle resolved discussions.
|
||||
---@field toggle_unresolved boolean Whether to toggle unresolved discussions.
|
||||
---@field keep_current_open boolean Whether to keep the current discussion open even if it should otherwise be closed.
|
||||
|
||||
---This function (settings.discussion_tree.toggle_nodes) expands/collapses all nodes and their children according to the opts.
|
||||
---@param tree NuiTree
|
||||
---@param winid integer
|
||||
---@param unlinked boolean
|
||||
---@param opts ToggleNodesOptions
|
||||
M.toggle_nodes = function(winid, tree, unlinked, opts)
|
||||
local current_node = tree:get_node()
|
||||
if current_node == nil then
|
||||
return
|
||||
end
|
||||
local root_node = common.get_root_node(tree, current_node)
|
||||
for _, node in ipairs(tree:get_nodes()) do
|
||||
if opts.toggle_resolved then
|
||||
if
|
||||
(unlinked and state.unlinked_discussion_tree.resolved_expanded)
|
||||
or (not unlinked and state.discussion_tree.resolved_expanded)
|
||||
then
|
||||
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, true)
|
||||
else
|
||||
M.expand_recursively(tree, node, true)
|
||||
end
|
||||
end
|
||||
if opts.toggle_unresolved then
|
||||
if
|
||||
(unlinked and state.unlinked_discussion_tree.unresolved_expanded)
|
||||
or (not unlinked and state.discussion_tree.unresolved_expanded)
|
||||
then
|
||||
M.collapse_recursively(tree, node, root_node, opts.keep_current_open, false)
|
||||
else
|
||||
M.expand_recursively(tree, node, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Reset states of resolved discussions after toggling
|
||||
if opts.toggle_resolved then
|
||||
if unlinked then
|
||||
state.unlinked_discussion_tree.resolved_expanded = not state.unlinked_discussion_tree.resolved_expanded
|
||||
else
|
||||
state.discussion_tree.resolved_expanded = not state.discussion_tree.resolved_expanded
|
||||
end
|
||||
end
|
||||
-- Reset states of unresolved discussions after toggling
|
||||
if opts.toggle_unresolved then
|
||||
if unlinked then
|
||||
state.unlinked_discussion_tree.unresolved_expanded = not state.unlinked_discussion_tree.unresolved_expanded
|
||||
else
|
||||
state.discussion_tree.unresolved_expanded = not state.discussion_tree.unresolved_expanded
|
||||
end
|
||||
end
|
||||
tree:render()
|
||||
M.restore_cursor_position(winid, tree, current_node, root_node)
|
||||
end
|
||||
|
||||
---Restore cursor position to the original node if possible
|
||||
M.restore_cursor_position = function(winid, tree, original_node, root_node)
|
||||
local _, line_number = tree:get_node("-" .. tostring(original_node.id))
|
||||
-- If current_node is has been collapsed, get line number of root node instead
|
||||
if line_number == nil and root_node then
|
||||
_, line_number = tree:get_node("-" .. tostring(root_node.id))
|
||||
end
|
||||
if line_number ~= nil then
|
||||
vim.api.nvim_win_set_cursor(winid, { line_number, 0 })
|
||||
end
|
||||
end
|
||||
|
||||
---This function (settings.discussion_tree.expand_recursively) expands a node and its children.
|
||||
---@param tree NuiTree
|
||||
---@param node NuiTree.Node
|
||||
---@param is_resolved boolean If true, expand resolved discussions. If false, expand unresolved discussions.
|
||||
M.expand_recursively = function(tree, node, is_resolved)
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
if common.is_node_note(node) and common.get_root_node(tree, node).resolved == is_resolved then
|
||||
node:expand()
|
||||
end
|
||||
local children = node:get_child_ids()
|
||||
for _, child in ipairs(children) do
|
||||
M.expand_recursively(tree, tree:get_node(child), is_resolved)
|
||||
end
|
||||
end
|
||||
|
||||
---This function (settings.discussion_tree.collapse_recursively) collapses a node and its children.
|
||||
---@param tree NuiTree
|
||||
---@param node NuiTree.Node
|
||||
---@param current_root_node NuiTree.Node The root node of the current node.
|
||||
---@param keep_current_open boolean If true, the current node stays open, even if it should otherwise be collapsed.
|
||||
---@param is_resolved boolean If true, collapse resolved discussions. If false, collapse unresolved discussions.
|
||||
M.collapse_recursively = function(tree, node, current_root_node, keep_current_open, is_resolved)
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
local root_node = common.get_root_node(tree, node)
|
||||
if common.is_node_note(node) and root_node.resolved == is_resolved then
|
||||
if keep_current_open and root_node == current_root_node then
|
||||
return
|
||||
end
|
||||
node:collapse()
|
||||
end
|
||||
local children = node:get_child_ids()
|
||||
for _, child in ipairs(children) do
|
||||
M.collapse_recursively(tree, tree:get_node(child), current_root_node, keep_current_open, is_resolved)
|
||||
end
|
||||
end
|
||||
|
||||
-- This function (settings.discussion_tree.toggle_node) expands/collapses the current node and its children
|
||||
M.toggle_node = function(tree)
|
||||
local node = tree:get_node()
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
-- Switch to the "note" node from "note_body" nodes to enable toggling discussions inside comments
|
||||
if node.type == "note_body" then
|
||||
node = tree:get_node(node:get_parent_id())
|
||||
end
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local children = node:get_child_ids()
|
||||
if node == nil then
|
||||
return
|
||||
end
|
||||
if node:is_expanded() then
|
||||
node:collapse()
|
||||
if common.is_node_note(node) then
|
||||
for _, child in ipairs(children) do
|
||||
tree:get_node(child):collapse()
|
||||
end
|
||||
end
|
||||
else
|
||||
if common.is_node_note(node) then
|
||||
for _, child in ipairs(children) do
|
||||
tree:get_node(child):expand()
|
||||
end
|
||||
end
|
||||
node:expand()
|
||||
end
|
||||
|
||||
tree:render()
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
local M = {}
|
||||
local state = require("gitlab.state")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
|
||||
local M = {
|
||||
bufnr_map = {
|
||||
discussions = nil,
|
||||
notes = nil,
|
||||
},
|
||||
current_view_type = state.settings.discussion_tree.default_view,
|
||||
}
|
||||
|
||||
M.set_buffers = function(linked_bufnr, unlinked_bufnr)
|
||||
M.bufnr_map = {
|
||||
discussions = linked_bufnr,
|
||||
notes = unlinked_bufnr,
|
||||
}
|
||||
end
|
||||
|
||||
---@param nodes Discussion[]|UnlinkedDiscussion[]|nil
|
||||
---@return number, number
|
||||
@@ -30,36 +44,131 @@ local get_data = function(nodes)
|
||||
return total_resolvable, total_resolved
|
||||
end
|
||||
|
||||
---@param discussions Discussion[]|nil
|
||||
---@param unlinked_discussions UnlinkedDiscussion[]|nil
|
||||
---@param file_name string
|
||||
local function content(discussions, unlinked_discussions, file_name)
|
||||
local resolvable_discussions, resolved_discussions = get_data(discussions)
|
||||
local resolvable_notes, resolved_notes = get_data(unlinked_discussions)
|
||||
local function content()
|
||||
local resolvable_discussions, resolved_discussions = get_data(state.DISCUSSION_DATA.discussions)
|
||||
local resolvable_notes, resolved_notes = get_data(state.DISCUSSION_DATA.unlinked_discussions)
|
||||
|
||||
local draft_notes = require("gitlab.actions.draft_notes")
|
||||
local inline_draft_notes = List.new(state.DRAFT_NOTES):filter(draft_notes.has_position)
|
||||
local unlinked_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||
return not draft_notes.has_position(note)
|
||||
end)
|
||||
|
||||
local t = {
|
||||
name = file_name,
|
||||
resolvable_discussions = resolvable_discussions,
|
||||
resolved_discussions = resolved_discussions,
|
||||
inline_draft_notes = #inline_draft_notes,
|
||||
unlinked_draft_notes = #unlinked_draft_notes,
|
||||
resolvable_notes = resolvable_notes,
|
||||
resolved_notes = resolved_notes,
|
||||
help_keymap = state.settings.help,
|
||||
}
|
||||
|
||||
return state.settings.discussion_tree.winbar(t)
|
||||
return M.make_winbar(t)
|
||||
end
|
||||
|
||||
---This function updates the winbar
|
||||
---@param discussions Discussion[]
|
||||
---@param unlinked_discussions UnlinkedDiscussion[]
|
||||
---@param base_title string
|
||||
M.update_winbar = function(discussions, unlinked_discussions, base_title)
|
||||
M.update_winbar = function()
|
||||
local d = require("gitlab.actions.discussions")
|
||||
local winId = d.split.winid
|
||||
local c = content(discussions, unlinked_discussions, base_title)
|
||||
if vim.wo[winId] then
|
||||
vim.wo[winId].winbar = c
|
||||
if d.split == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local win_id = d.split.winid
|
||||
if win_id == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if not vim.api.nvim_win_is_valid(win_id) then
|
||||
return
|
||||
end
|
||||
|
||||
local c = content()
|
||||
vim.api.nvim_set_option_value("winbar", c, { scope = "local", win = win_id })
|
||||
end
|
||||
|
||||
---Builds the title string for both sections, using the count of resolvable and draft nodes
|
||||
---@param base_title string
|
||||
---@param resolvable_count integer
|
||||
---@param resolved_count integer
|
||||
---@param drafts_count integer
|
||||
---@return string
|
||||
local add_drafts_and_resolvable = function(base_title, resolvable_count, resolved_count, drafts_count)
|
||||
if resolvable_count ~= 0 then
|
||||
base_title = base_title .. string.format(" (%d/%d resolved", resolvable_count, resolved_count)
|
||||
end
|
||||
if drafts_count ~= 0 then
|
||||
if resolvable_count ~= 0 then
|
||||
base_title = base_title .. string.format("; %d drafts)", drafts_count)
|
||||
else
|
||||
base_title = base_title .. string.format(" (%d drafts)", drafts_count)
|
||||
end
|
||||
elseif resolvable_count ~= 0 then
|
||||
base_title = base_title .. ")"
|
||||
end
|
||||
|
||||
return base_title
|
||||
end
|
||||
|
||||
---@param t WinbarTable
|
||||
M.make_winbar = function(t)
|
||||
local discussion_title =
|
||||
add_drafts_and_resolvable("Inline Comments", t.resolvable_discussions, t.resolved_discussions, t.inline_draft_notes)
|
||||
local notes_title = add_drafts_and_resolvable("Notes", t.resolvable_notes, t.resolved_notes, t.unlinked_draft_notes)
|
||||
|
||||
-- Colorize the active tab
|
||||
if M.current_view_type == "discussions" then
|
||||
discussion_title = "%#Text#" .. discussion_title
|
||||
notes_title = "%#Comment#" .. notes_title
|
||||
elseif M.current_view_type == "notes" then
|
||||
discussion_title = "%#Comment#" .. discussion_title
|
||||
notes_title = "%#Text#" .. notes_title
|
||||
end
|
||||
|
||||
local mode = M.get_mode()
|
||||
|
||||
-- Join everything together and return it
|
||||
local separator = "%#Comment#|"
|
||||
local end_section = "%="
|
||||
local help = "%#Comment#Help: " .. t.help_keymap:gsub(" ", "<space>") .. " "
|
||||
return string.format(
|
||||
" %s %s %s %s %s %s %s",
|
||||
discussion_title,
|
||||
separator,
|
||||
notes_title,
|
||||
end_section,
|
||||
mode,
|
||||
separator,
|
||||
help
|
||||
)
|
||||
end
|
||||
|
||||
---Returns a string for the winbar indicating the mode type, live or draft
|
||||
---@return string
|
||||
M.get_mode = function()
|
||||
if state.settings.discussion_tree.draft_mode then
|
||||
return "%#DiagnosticWarn#Draft Mode"
|
||||
else
|
||||
return "%#DiagnosticOK#Live Mode"
|
||||
end
|
||||
end
|
||||
|
||||
---Sets the current view type (if provided an argument)
|
||||
---and then updates the view
|
||||
---@param override any
|
||||
M.switch_view_type = function(override)
|
||||
if override then
|
||||
M.current_view_type = override
|
||||
else
|
||||
if M.current_view_type == "discussions" then
|
||||
M.current_view_type = "notes"
|
||||
elseif M.current_view_type == "notes" then
|
||||
M.current_view_type = "discussions"
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_set_current_buf(M.bufnr_map[M.current_view_type])
|
||||
M.update_winbar()
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
239
lua/gitlab/actions/draft_notes/init.lua
Executable file
239
lua/gitlab/actions/draft_notes/init.lua
Executable file
@@ -0,0 +1,239 @@
|
||||
-- This module is responsible for CRUD operations for the draft notes in the discussion tree.
|
||||
-- That includes things like editing existing draft notes in the tree, and
|
||||
-- and deleting them. Normal notes and comments are managed separately,
|
||||
-- under lua/gitlab/actions/discussions/init.lua
|
||||
local winbar = require("gitlab.actions.discussions.winbar")
|
||||
local diagnostics = require("gitlab.indicators.diagnostics")
|
||||
local common = require("gitlab.actions.common")
|
||||
local discussion_tree = require("gitlab.actions.discussions.tree")
|
||||
local job = require("gitlab.job")
|
||||
local NuiTree = require("nui.tree")
|
||||
local List = require("gitlab.utils.list")
|
||||
local u = require("gitlab.utils")
|
||||
local state = require("gitlab.state")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class AddDraftNoteOpts table
|
||||
---@field draft_note DraftNote
|
||||
---@field unlinked boolean
|
||||
|
||||
---Adds a draft note to the draft notes state, then rebuilds the view
|
||||
---@param opts AddDraftNoteOpts
|
||||
M.add_draft_note = function(opts)
|
||||
local new_draft_notes = u.ensure_table(state.DRAFT_NOTES)
|
||||
table.insert(new_draft_notes, opts.draft_note)
|
||||
state.DRAFT_NOTES = new_draft_notes
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
if opts.unlinked then
|
||||
discussions.rebuild_unlinked_discussion_tree()
|
||||
else
|
||||
discussions.rebuild_discussion_tree()
|
||||
end
|
||||
winbar.update_winbar()
|
||||
end
|
||||
|
||||
---Tells whether a draft note was left on a particular diff or is an unlinked note
|
||||
---@param note DraftNote
|
||||
M.has_position = function(note)
|
||||
return note.position.new_path ~= nil or note.position.old_path ~= nil
|
||||
end
|
||||
|
||||
---Returns a list of nodes to add to the discussion tree. Can filter and return only unlinked (note) nodes.
|
||||
---@param unlinked boolean
|
||||
---@return NuiTree.Node[]
|
||||
M.add_draft_notes_to_table = function(unlinked)
|
||||
local draft_notes = List.new(state.DRAFT_NOTES)
|
||||
|
||||
local draft_note_nodes = draft_notes
|
||||
---@param note DraftNote
|
||||
:filter(function(note)
|
||||
if unlinked then
|
||||
return not M.has_position(note)
|
||||
end
|
||||
return M.has_position(note)
|
||||
end)
|
||||
---@param note DraftNote
|
||||
:map(function(note)
|
||||
local _, root_text, root_text_nodes = discussion_tree.build_note(note)
|
||||
return NuiTree.Node({
|
||||
range = (type(note.position) == "table" and note.position.line_range or nil),
|
||||
text = root_text,
|
||||
type = "note",
|
||||
is_root = true,
|
||||
is_draft = true,
|
||||
id = note.id,
|
||||
root_note_id = note.id,
|
||||
file_name = (type(note.position) == "table" and note.position.new_path or nil),
|
||||
new_line = (type(note.position) == "table" and note.position.new_line or nil),
|
||||
old_line = (type(note.position) == "table" and note.position.old_line or nil),
|
||||
resolvable = false,
|
||||
resolved = false,
|
||||
url = state.INFO.web_url .. "#note_" .. note.id,
|
||||
}, root_text_nodes)
|
||||
end)
|
||||
|
||||
return draft_note_nodes
|
||||
|
||||
-- TODO: Combine draft_notes and normal discussion nodes in the complex discussion
|
||||
-- tree. The code for that feature is a clusterfuck so this is difficult
|
||||
-- if state.settings.discussion_tree.tree_type == "simple" then
|
||||
-- return draft_note_nodes
|
||||
-- end
|
||||
end
|
||||
|
||||
---Send edits will actually send the edits to Gitlab and refresh the draft_notes tree
|
||||
M.send_edits = function(note_id)
|
||||
return function(text)
|
||||
local all_notes = List.new(state.DRAFT_NOTES)
|
||||
local the_note = all_notes:find(function(note)
|
||||
return note.id == note_id
|
||||
end)
|
||||
local body = { note = text, position = the_note.position }
|
||||
job.run_job(string.format("/mr/draft_notes/%d", note_id), "PATCH", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
local has_position = false
|
||||
local new_draft_notes = all_notes:map(function(note)
|
||||
if note.id == note_id then
|
||||
has_position = M.has_position(note)
|
||||
note.note = text
|
||||
end
|
||||
return note
|
||||
end)
|
||||
state.DRAFT_NOTES = new_draft_notes
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
if has_position then
|
||||
discussions.rebuild_discussion_tree()
|
||||
else
|
||||
discussions.rebuild_unlinked_discussion_tree()
|
||||
end
|
||||
winbar.update_winbar()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- This function will actually send the deletion to Gitlab when you make a selection, and re-render the tree
|
||||
M.send_deletion = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
local note_node = common.get_note_node(tree, current_node)
|
||||
local root_node = common.get_root_node(tree, current_node)
|
||||
|
||||
if note_node == nil or root_node == nil then
|
||||
u.notify("Could not get note or root node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
---@type integer
|
||||
local note_id = note_node.is_root and root_node.id or note_node.id
|
||||
|
||||
job.run_job(string.format("/mr/draft_notes/%d", note_id), "DELETE", nil, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
|
||||
local has_position = false
|
||||
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||
if note.id ~= note_id then
|
||||
return true
|
||||
else
|
||||
has_position = M.has_position(note)
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
state.DRAFT_NOTES = new_draft_notes
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
if has_position then
|
||||
discussions.rebuild_discussion_tree()
|
||||
else
|
||||
discussions.rebuild_unlinked_discussion_tree()
|
||||
end
|
||||
|
||||
if state.settings.discussion_signs.enabled and state.DISCUSSION_DATA then
|
||||
diagnostics.refresh_diagnostics()
|
||||
end
|
||||
|
||||
winbar.update_winbar()
|
||||
common.add_empty_titles()
|
||||
end)
|
||||
end
|
||||
|
||||
-- This function will trigger a popup prompting you to publish the current draft comment
|
||||
M.publish_draft = function(tree)
|
||||
vim.ui.select({ "Confirm", "Cancel" }, {
|
||||
prompt = "Publish current draft comment?",
|
||||
}, function(choice)
|
||||
if choice == "Confirm" then
|
||||
M.confirm_publish_draft(tree)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- This function will trigger a popup prompting you to publish all draft notes
|
||||
M.publish_all_drafts = function()
|
||||
vim.ui.select({ "Confirm", "Cancel" }, {
|
||||
prompt = "Publish all drafts?",
|
||||
}, function(choice)
|
||||
if choice == "Confirm" then
|
||||
M.confirm_publish_all_drafts()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Publishes all draft notes and comments. Re-renders all discussion views.
|
||||
M.confirm_publish_all_drafts = function()
|
||||
local body = { publish_all = true }
|
||||
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
state.DRAFT_NOTES = {}
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.refresh(function()
|
||||
discussions.rebuild_discussion_tree()
|
||||
discussions.rebuild_unlinked_discussion_tree()
|
||||
winbar.update_winbar()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---Publishes the current draft note that is being hovered over in the tree,
|
||||
---and then makes an API call to refresh the relevant data for that tree
|
||||
---and re-render it.
|
||||
---@param tree NuiTree
|
||||
M.confirm_publish_draft = function(tree)
|
||||
local current_node = tree:get_node()
|
||||
local note_node = common.get_note_node(tree, current_node)
|
||||
local root_node = common.get_root_node(tree, current_node)
|
||||
|
||||
if note_node == nil or root_node == nil then
|
||||
u.notify("Could not get note or root node", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
---@type integer
|
||||
local note_id = note_node.is_root and root_node.id or note_node.id
|
||||
local body = { note = note_id, publish_all = false }
|
||||
job.run_job("/mr/draft_notes/publish", "POST", body, function(data)
|
||||
u.notify(data.message, vim.log.levels.INFO)
|
||||
|
||||
local has_position = false
|
||||
local new_draft_notes = List.new(state.DRAFT_NOTES):filter(function(note)
|
||||
if note.id ~= note_id then
|
||||
return true
|
||||
else
|
||||
has_position = M.has_position(note)
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
state.DRAFT_NOTES = new_draft_notes
|
||||
local discussions = require("gitlab.actions.discussions")
|
||||
discussions.refresh(function()
|
||||
if has_position then
|
||||
discussions.rebuild_discussion_tree()
|
||||
else
|
||||
discussions.rebuild_unlinked_discussion_tree()
|
||||
end
|
||||
winbar.update_winbar()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
56
lua/gitlab/actions/merge_requests.lua
Normal file
56
lua/gitlab/actions/merge_requests.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
local state = require("gitlab.state")
|
||||
local reviewer = require("gitlab.reviewer")
|
||||
local git = require("gitlab.git")
|
||||
local u = require("gitlab.utils")
|
||||
local M = {}
|
||||
|
||||
---@class SwitchOpts
|
||||
---@field open_reviewer boolean
|
||||
|
||||
---Opens up a select menu that lets you choose a different merge request.
|
||||
---@param opts SwitchOpts|nil
|
||||
M.choose_merge_request = function(opts)
|
||||
local has_clean_tree, clean_tree_err = git.has_clean_tree()
|
||||
if clean_tree_err ~= nil then
|
||||
return
|
||||
elseif has_clean_tree ~= "" then
|
||||
u.notify("Your local branch has changes, please stash or commit and push", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
if opts == nil then
|
||||
opts = state.settings.choose_merge_request
|
||||
end
|
||||
|
||||
vim.ui.select(state.MERGE_REQUESTS, {
|
||||
prompt = "Choose Merge Request",
|
||||
format_item = function(mr)
|
||||
return string.format("%s (%s)", mr.title, mr.author.name)
|
||||
end,
|
||||
}, function(choice)
|
||||
if not choice then
|
||||
return
|
||||
end
|
||||
|
||||
if reviewer.is_open then
|
||||
reviewer.close()
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
local _, branch_switch_err = git.switch_branch(choice.source_branch)
|
||||
if branch_switch_err ~= nil then
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(function()
|
||||
require("gitlab.server").restart(function()
|
||||
if opts.open_reviewer then
|
||||
require("gitlab").review()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -143,10 +143,7 @@ M.see_logs = function()
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
for line in u.split_by_new_lines(file) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
local lines = u.lines_into_table(file)
|
||||
|
||||
if #lines == 0 then
|
||||
u.notify("Log trace lines could not be parsed", vim.log.levels.ERROR)
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
-- send edits to the description back to Gitlab
|
||||
local Layout = require("nui.layout")
|
||||
local Popup = require("nui.popup")
|
||||
local git = require("gitlab.git")
|
||||
local job = require("gitlab.job")
|
||||
local common = require("gitlab.actions.common")
|
||||
local u = require("gitlab.utils")
|
||||
local List = require("gitlab.utils.list")
|
||||
local state = require("gitlab.state")
|
||||
@@ -28,7 +30,7 @@ M.summary = function()
|
||||
end
|
||||
|
||||
local title = state.INFO.title
|
||||
local description_lines = M.build_description_lines()
|
||||
local description_lines = common.build_content(state.INFO.description)
|
||||
local info_lines = state.settings.info.enabled and M.build_info_lines() or { "" }
|
||||
|
||||
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
|
||||
@@ -69,22 +71,8 @@ M.summary = function()
|
||||
|
||||
vim.api.nvim_set_current_buf(description_popup.bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Builds a lua list of strings that contain the MR description
|
||||
M.build_description_lines = function()
|
||||
local description_lines = {}
|
||||
|
||||
local description = state.INFO.description
|
||||
for line in u.split_by_new_lines(description) do
|
||||
table.insert(description_lines, line)
|
||||
end
|
||||
-- TODO: @harrisoncramer Not sure whether the following line should be here at all. It definitely
|
||||
-- didn't belong into the for loop, since it inserted an empty line after each line. But maybe
|
||||
-- there is a purpose for an empty line at the end of the buffer?
|
||||
table.insert(description_lines, "")
|
||||
|
||||
return description_lines
|
||||
git.current_branch_up_to_date_on_remote(vim.log.levels.WARN)
|
||||
end
|
||||
|
||||
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
|
||||
|
||||
Reference in New Issue
Block a user