* 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>
323 lines
9.3 KiB
Lua
323 lines
9.3 KiB
Lua
-- This module is responsible for creating am MR
|
|
-- for the current branch
|
|
local Layout = require("nui.layout")
|
|
local Input = require("nui.input")
|
|
local Popup = require("nui.popup")
|
|
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
|
|
---@field target? string
|
|
---@field title? string
|
|
---@field description? string
|
|
---@field template_file? string
|
|
---@field delete_branch boolean?
|
|
---@field squash boolean?
|
|
|
|
local M = {
|
|
started = false,
|
|
layout_visible = false,
|
|
layout = nil,
|
|
layout_buf = nil,
|
|
title_bufnr = nil,
|
|
description_bufnr = nil,
|
|
mr = {
|
|
target = "",
|
|
title = "",
|
|
description = "",
|
|
},
|
|
}
|
|
|
|
M.reset_state = function()
|
|
M.started = false
|
|
M.mr.title = ""
|
|
M.mr.target = ""
|
|
M.mr.description = ""
|
|
end
|
|
|
|
---1. If the user has already begun writing an MR, prompt them to
|
|
--- 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
|
|
M.open_confirmation_popup(M.mr)
|
|
return
|
|
else
|
|
M.reset_state()
|
|
M.pick_target(args)
|
|
end
|
|
end)
|
|
else
|
|
M.pick_target(args)
|
|
end
|
|
end
|
|
|
|
---2. Pick the target branch
|
|
---@param mr? Mr
|
|
M.pick_target = function(mr)
|
|
if not mr then
|
|
mr = {}
|
|
end
|
|
if mr.target ~= nil then
|
|
M.pick_template(mr)
|
|
return
|
|
end
|
|
|
|
if state.settings.create_mr.target ~= nil then
|
|
mr.target = state.settings.create_mr.target
|
|
M.pick_template(mr)
|
|
return
|
|
end
|
|
|
|
-- Select target branch interactively if it hasn't been selected by other means
|
|
u.select_target_branch(function(target)
|
|
mr.target = target
|
|
M.pick_template(mr)
|
|
end)
|
|
end
|
|
|
|
local function make_template_path(t)
|
|
local base_dir, err = git.base_dir()
|
|
if err ~= nil then
|
|
return
|
|
end
|
|
return base_dir
|
|
.. state.settings.file_separator
|
|
.. ".gitlab"
|
|
.. state.settings.file_separator
|
|
.. "merge_request_templates"
|
|
.. state.settings.file_separator
|
|
.. t
|
|
end
|
|
|
|
---3. Pick template (if applicable). This is used as the description
|
|
---@param mr Mr
|
|
M.pick_template = function(mr)
|
|
if mr.description ~= nil then
|
|
M.add_title(mr)
|
|
return
|
|
end
|
|
|
|
local template_file = mr.template_file or state.settings.create_mr.template_file
|
|
if template_file ~= nil then
|
|
mr.description = u.read_file(make_template_path(template_file))
|
|
M.add_title(mr)
|
|
return
|
|
end
|
|
|
|
local all_templates = u.list_files_in_folder(".gitlab" .. state.settings.file_separator .. "merge_request_templates")
|
|
if all_templates == nil then
|
|
M.add_title(mr)
|
|
return
|
|
end
|
|
|
|
local opts = { "Blank Template" }
|
|
for _, v in ipairs(all_templates) do
|
|
table.insert(opts, v)
|
|
end
|
|
vim.ui.select(opts, {
|
|
prompt = "Choose Template",
|
|
}, function(choice)
|
|
if choice and choice ~= "Blank Template" then
|
|
mr.description = u.read_file(make_template_path(choice))
|
|
end
|
|
M.add_title(mr)
|
|
end)
|
|
end
|
|
|
|
---4. Prompts the user for the title of the MR
|
|
---@param mr Mr
|
|
M.add_title = function(mr)
|
|
if mr.title ~= nil then
|
|
M.open_confirmation_popup(mr)
|
|
return
|
|
end
|
|
|
|
local input = Input({
|
|
position = "50%",
|
|
relative = "editor",
|
|
size = state.settings.create_mr.title_input.width,
|
|
border = {
|
|
style = state.settings.create_mr.title_input.border,
|
|
text = {
|
|
top = "Title",
|
|
},
|
|
},
|
|
}, {
|
|
prompt = "",
|
|
default_value = "",
|
|
on_close = function() end,
|
|
on_submit = function(_value)
|
|
M.open_confirmation_popup(mr)
|
|
end,
|
|
on_change = function(value)
|
|
mr.title = value
|
|
end,
|
|
})
|
|
input:mount()
|
|
end
|
|
|
|
---5. Show the final popup.
|
|
---The function will render a popup containing the MR title and MR description,
|
|
---target branch, and the "delete_branch" and "squash" options. All fields are editable.
|
|
---@param mr Mr
|
|
M.open_confirmation_popup = function(mr)
|
|
M.started = true
|
|
if M.layout_visible then
|
|
M.layout:unmount()
|
|
M.layout_visible = false
|
|
return
|
|
end
|
|
|
|
local layout, title_popup, description_popup, target_popup, delete_branch_popup, squash_popup = M.create_layout()
|
|
|
|
local popups = {
|
|
title_popup,
|
|
description_popup,
|
|
delete_branch_popup,
|
|
squash_popup,
|
|
target_popup,
|
|
}
|
|
|
|
M.layout = layout
|
|
M.layout_buf = layout.bufnr
|
|
M.layout_visible = true
|
|
|
|
local function exit()
|
|
local title = vim.fn.trim(u.get_buffer_text(M.title_bufnr))
|
|
local description = u.get_buffer_text(M.description_bufnr)
|
|
local target = vim.fn.trim(u.get_buffer_text(M.target_bufnr))
|
|
local delete_branch = u.string_to_bool(u.get_buffer_text(M.delete_branch_bufnr))
|
|
local squash = u.string_to_bool(u.get_buffer_text(M.squash_bufnr))
|
|
M.mr = {
|
|
title = title,
|
|
description = description,
|
|
target = target,
|
|
delete_branch = delete_branch,
|
|
squash = squash,
|
|
}
|
|
layout:unmount()
|
|
M.layout_visible = false
|
|
end
|
|
|
|
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 })
|
|
|
|
vim.schedule(function()
|
|
vim.api.nvim_buf_set_lines(M.description_bufnr, 0, -1, false, description_lines)
|
|
vim.api.nvim_buf_set_lines(M.title_bufnr, 0, -1, false, { mr.title })
|
|
vim.api.nvim_buf_set_lines(M.target_bufnr, 0, -1, false, { mr.target })
|
|
vim.api.nvim_buf_set_lines(M.delete_branch_bufnr, 0, -1, false, { u.bool_to_string(delete_branch) })
|
|
vim.api.nvim_buf_set_lines(M.squash_bufnr, 0, -1, false, { u.bool_to_string(squash) })
|
|
|
|
u.switch_can_edit_buf(M.delete_branch_bufnr, false)
|
|
u.switch_can_edit_buf(M.squash_bufnr, false)
|
|
u.switch_can_edit_buf(M.target_bufnr, false)
|
|
|
|
local popup_opts = {
|
|
cb = exit,
|
|
action_before_close = true,
|
|
action_before_exit = true,
|
|
}
|
|
|
|
state.set_popup_keymaps(description_popup, M.create_mr, miscellaneous.attach_file, popup_opts)
|
|
state.set_popup_keymaps(title_popup, M.create_mr, nil, popup_opts)
|
|
state.set_popup_keymaps(target_popup, M.create_mr, M.select_new_target, popup_opts)
|
|
state.set_popup_keymaps(delete_branch_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
|
state.set_popup_keymaps(squash_popup, M.create_mr, miscellaneous.toggle_bool, popup_opts)
|
|
miscellaneous.set_cycle_popups_keymaps(popups)
|
|
|
|
vim.api.nvim_set_current_buf(M.description_bufnr)
|
|
end)
|
|
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()
|
|
u.select_target_branch(function(target)
|
|
vim.schedule(function()
|
|
u.switch_can_edit_buf(bufnr, true)
|
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { target })
|
|
u.switch_can_edit_buf(bufnr, false)
|
|
end)
|
|
end)
|
|
end
|
|
|
|
---This function will POST the new MR to create it
|
|
M.create_mr = function()
|
|
local description = u.get_buffer_text(M.description_bufnr)
|
|
local title = u.get_buffer_text(M.title_bufnr):gsub("\n", " ")
|
|
local target = u.get_buffer_text(M.target_bufnr):gsub("\n", " ")
|
|
local delete_branch = u.string_to_bool(u.get_buffer_text(M.delete_branch_bufnr))
|
|
local squash = u.string_to_bool(u.get_buffer_text(M.squash_bufnr))
|
|
|
|
local body = {
|
|
title = title,
|
|
description = description,
|
|
target_branch = target,
|
|
delete_branch = delete_branch,
|
|
squash = squash,
|
|
}
|
|
|
|
job.run_job("/create_mr", "POST", body, function(data)
|
|
u.notify(data.message, vim.log.levels.INFO)
|
|
M.reset_state()
|
|
M.layout:unmount()
|
|
M.layout_visible = false
|
|
end)
|
|
end
|
|
|
|
M.create_layout = function()
|
|
local title_popup = Popup(u.create_box_popup_state("Title", false))
|
|
M.title_bufnr = title_popup.bufnr
|
|
local description_popup = Popup(u.create_box_popup_state("Description", true))
|
|
M.description_bufnr = description_popup.bufnr
|
|
local target_branch_popup = Popup(u.create_box_popup_state("Target branch", false))
|
|
M.target_bufnr = target_branch_popup.bufnr
|
|
local delete_title = vim.o.columns > 110 and "Delete source branch" or "Delete source"
|
|
local delete_branch_popup = Popup(u.create_box_popup_state(delete_title, false))
|
|
M.delete_branch_bufnr = delete_branch_popup.bufnr
|
|
local squash_title = vim.o.columns > 110 and "Squash commits" or "Squash"
|
|
local squash_popup = Popup(u.create_box_popup_state(squash_title, false))
|
|
M.squash_bufnr = squash_popup.bufnr
|
|
|
|
local internal_layout
|
|
internal_layout = Layout.Box({
|
|
Layout.Box({
|
|
Layout.Box(title_popup, { grow = 1 }),
|
|
}, { size = 3 }),
|
|
Layout.Box(description_popup, { grow = 1 }),
|
|
Layout.Box({
|
|
Layout.Box(delete_branch_popup, { size = { width = #delete_title + 4 } }),
|
|
Layout.Box(squash_popup, { size = { width = #squash_title + 4 } }),
|
|
Layout.Box(target_branch_popup, { grow = 1 }),
|
|
}, { size = 3 }),
|
|
}, { dir = "col" })
|
|
|
|
local layout = Layout({
|
|
position = "50%",
|
|
relative = "editor",
|
|
size = {
|
|
width = "95%",
|
|
height = "95%",
|
|
},
|
|
}, internal_layout)
|
|
|
|
layout:mount()
|
|
|
|
return layout, title_popup, description_popup, target_branch_popup, delete_branch_popup, squash_popup
|
|
end
|
|
|
|
return M
|