Feat: Adds More Information to Summary Panel (#100)

This MR adds more information into the summary view, including the MR author, created at date, merge status, draft status, conflicts, and pipeline status, among other things. This is configurable via the setup function.
This commit is contained in:
Harrison (Harry) Cramer
2023-11-20 18:56:19 -05:00
committed by GitHub
parent 88b9196a2e
commit c4a3229f16
7 changed files with 333 additions and 55 deletions

View File

@@ -135,6 +135,21 @@ require("gitlab").setup({
resolved = '', -- Symbol to show next to resolved discussions
unresolved = '', -- Symbol to show next to unresolved discussions
},
info = { -- Show additional fields in the summary pane
enabled = true,
horizontal = false, -- Display metadata to the left of the summary rather than underneath
fields = { -- The fields listed here will be displayed, in whatever order you choose
"author",
"created_at",
"updated_at",
"merge_status",
"draft",
"conflicts",
"assignees",
"branch",
"pipeline",
},
},
discussion_sign_and_diagnostic = {
skip_resolved_discussion = false,
skip_old_revision_discussion = true,
@@ -206,6 +221,8 @@ require("gitlab").summary()
After editing the description or title, you may save your changes via the `settings.popup.perform_action` keybinding.
By default this plugin will also show additional metadata about the MR in a separate pane underneath the description. This can be disabled, and these fields can be reordered or removed. Please see the `settings.info` section of the configuration.
### Reviewing Diffs
The `review` action will open a diff of the changes. You can leave comments using the `create_comment` action. In visual mode, add multiline comments with the `create_multiline_comment` command, and add suggested changes with the `create_comment_suggestion` command.

36
example.lua Normal file
View File

@@ -0,0 +1,36 @@
local Layout = require("nui.layout")
local Popup = require("nui.popup")
local opts = {
buf_options = {
filetype = "markdown",
},
focusable = true,
border = {
style = "rounded",
},
}
local title_popup = Popup(opts)
local description_popup = Popup(opts)
local info_popup = Popup(opts)
local layout = Layout(
{
position = "50%",
relative = "editor",
size = {
width = "95%",
height = "95%",
},
},
Layout.Box({
Layout.Box(title_popup, { size = { height = 3 } }),
Layout.Box({
Layout.Box(description_popup, { grow = 1 }),
Layout.Box(info_popup, { size = { height = 15 } }),
}, { dir = "col", size = "100%" }),
}, { dir = "col" })
)
layout:mount()

View File

@@ -20,6 +20,14 @@ local function get_pipeline()
return pipeline
end
M.get_pipeline_status = function()
local pipeline = get_pipeline()
if pipeline == nil then
return nil
end
return string.format("%s (%s)", state.settings.pipeline[pipeline.status], pipeline.status)
end
-- The function will render the Pipeline state in a popup
M.open = function()
local pipeline = get_pipeline()
@@ -44,7 +52,7 @@ M.open = function()
local lines = {}
u.switch_can_edit_buf(bufnr, true)
table.insert(lines, string.format("Status: %s (%s)", state.settings.pipeline[pipeline.status], pipeline.status))
table.insert(lines, "Status: " .. M.get_pipeline_status())
table.insert(lines, "")
table.insert(lines, string.format("Last Run: %s", u.time_since(pipeline.created_at)))
table.insert(lines, string.format("Url: %s", pipeline.web_url))

View File

@@ -7,6 +7,8 @@ local job = require("gitlab.job")
local u = require("gitlab.utils")
local state = require("gitlab.state")
local miscellaneous = require("gitlab.actions.miscellaneous")
local pipeline = require("gitlab.actions.pipeline")
local M = {
layout_visible = false,
layout = nil,
@@ -15,7 +17,46 @@ local M = {
description_bufnr = nil,
}
-- The function will render the MR description in a popup
local title_popup_settings = {
buf_options = {
filetype = "markdown",
},
focusable = true,
border = {
style = "rounded",
},
}
local details_popup_settings = {
buf_options = {
filetype = "markdown",
},
focusable = true,
border = {
style = "rounded",
text = {
top = "Details",
},
},
}
local description_popup_settings = {
buf_options = {
filetype = "markdown",
},
enter = true,
focusable = true,
border = {
style = "rounded",
text = {
top = "Description",
},
},
}
-- The function will render a popup containing the MR title and MR description, and optionally,
-- any additional metadata that the user wants. The title and description are editable and
-- can be changed via the local action keybinding, which also closes the popup
M.summary = function()
if M.layout_visible then
M.layout:unmount()
@@ -23,7 +64,11 @@ M.summary = function()
return
end
local layout, title_popup, description_popup = M.create_layout()
local title = state.INFO.title
local description_lines = M.build_description_lines()
local info_lines = state.settings.info.enabled and M.build_info_lines() or nil
local layout, title_popup, description_popup, info_popup = M.create_layout(info_lines)
M.layout = layout
M.layout_buf = layout.bufnr
@@ -34,19 +79,16 @@ M.summary = function()
M.layout_visible = false
end
local currentBuffer = vim.api.nvim_get_current_buf()
local title = state.INFO.title
local description = state.INFO.description
local lines = {}
for line in description:gmatch("[^\n]+") do
table.insert(lines, line)
table.insert(lines, "")
end
vim.schedule(function()
vim.api.nvim_buf_set_lines(currentBuffer, 0, -1, false, lines)
vim.api.nvim_buf_set_lines(description_popup.bufnr, 0, -1, false, description_lines)
vim.api.nvim_buf_set_lines(title_popup.bufnr, 0, -1, false, { title })
if info_popup then
vim.api.nvim_buf_set_lines(info_popup.bufnr, 0, -1, false, info_lines)
vim.api.nvim_set_option_value("modifiable", false, { buf = info_popup.bufnr })
vim.api.nvim_set_option_value("readonly", false, { buf = info_popup.bufnr })
end
state.set_popup_keymaps(
description_popup,
M.edit_summary,
@@ -54,9 +96,76 @@ M.summary = function()
{ cb = exit, action_before_close = true }
)
state.set_popup_keymaps(title_popup, M.edit_summary, nil, { cb = exit, action_before_close = true })
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 description:gmatch("[^\n]+") do
table.insert(description_lines, line)
table.insert(description_lines, "")
end
return description_lines
end
-- Builds a lua list of strings that contain metadata about the current MR. Only builds the
-- lines that users include in their state.settings.info.fields list.
M.build_info_lines = function()
local info = state.INFO
local options = {
author = { title = "Author", content = "@" .. info.author.username .. " (" .. info.author.name .. ")" },
created_at = { title = "Created", content = u.format_to_local(info.created_at, vim.fn.strftime("%z")) },
updated_at = { title = "Updated", content = u.format_to_local(info.updated_at, vim.fn.strftime("%z")) },
merge_status = { title = "Status", content = info.detailed_merge_status },
draft = { title = "Draft", content = (info.draft and "Yes" or "No") },
conflicts = { title = "Merge Conflicts", content = (info.has_conflicts and "Yes" or "No") },
assignees = { title = "Assignees", content = u.make_readable_list(info.assignees, "name") },
branch = { title = "Branch", content = info.source_branch },
pipeline = {
title = "Pipeline Status:",
content = function()
return pipeline.get_pipeline_status()
end,
},
}
local longest_used = ""
for _, v in ipairs(state.settings.info.fields) do
local title = options[v].title
if string.len(title) > string.len(longest_used) then
longest_used = title
end
end
local function row_offset(row)
local offset = string.len(longest_used) - string.len(row)
return string.rep(" ", offset + 3)
end
local lines = {}
for _, v in ipairs(state.settings.info.fields) do
local row = options[v]
local line = "* " .. row.title .. row_offset(row.title)
if type(row.content) == "function" then
local content = row.content()
if content ~= nil then
line = line .. row.content()
end
else
line = line .. row.content
end
table.insert(lines, line)
end
return lines
end
-- This function will PUT the new description to the Go server
M.edit_summary = function()
local description = u.get_buffer_text(M.description_bufnr)
@@ -71,54 +180,52 @@ M.edit_summary = function()
end)
end
local top_popup = {
buf_options = {
filetype = "markdown",
},
focusable = true,
border = {
style = "rounded",
text = {
top = "Merge Request",
},
},
}
local bottom_popup = {
buf_options = {
filetype = "markdown",
},
enter = true,
focusable = true,
border = {
style = "rounded",
},
}
M.create_layout = function()
local title_popup = Popup(top_popup)
M.create_layout = function(info_lines)
local title_popup = Popup(title_popup_settings)
M.title_bufnr = title_popup.bufnr
local description_popup = Popup(bottom_popup)
local description_popup = Popup(description_popup_settings)
M.description_bufnr = description_popup.bufnr
local details_popup
local layout = Layout(
{
position = "50%",
relative = "editor",
size = {
width = "90%",
height = "70%",
},
},
Layout.Box({
Layout.Box(title_popup, { size = { height = 3 } }),
Layout.Box(description_popup, { size = "100%" }),
local internal_layout
if state.settings.info.enabled then
details_popup = Popup(details_popup_settings)
if state.settings.info.horizontal then
local longest_line = u.get_longest_string(info_lines)
print(longest_line)
internal_layout = Layout.Box({
Layout.Box(title_popup, { size = 3 }),
Layout.Box({
Layout.Box(details_popup, { size = longest_line + 3 }),
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "row", size = "100%" }),
}, { dir = "col" })
else
internal_layout = Layout.Box({
Layout.Box(title_popup, { size = 3 }),
Layout.Box(description_popup, { grow = 1 }),
Layout.Box(details_popup, { size = #info_lines + 3 }),
}, { dir = "col" })
end
else
internal_layout = Layout.Box({
Layout.Box(title_popup, { size = 3 }),
Layout.Box(description_popup, { grow = 1 }),
}, { dir = "col" })
)
end
local layout = Layout({
position = "50%",
relative = "editor",
size = {
width = "95%",
height = "95%",
},
}, internal_layout)
layout:mount()
return layout, title_popup, description_popup
return layout, title_popup, description_popup, details_popup
end
return M

View File

@@ -178,4 +178,38 @@ describe("utils/init.lua", function()
assert.are.same(got, want)
end)
end)
describe("offset_to_seconds", function()
local tests = {
est = { "-0500", -18000 },
pst = { "-0800", -28800 },
gmt = { "+0000", 0 },
cet = { "+0100", 360 },
jst = { "+0900", 32400 },
ist = { "+0530", 19800 },
art = { "-0300", -10800 },
aest = { "+1100", 39600 },
mmt = { "+0630", 23400 },
}
for _, val in ipairs(tests) do
local got = u.offset_to_seconds(val[1])
local want = val[2]
assert.are.same(got, want)
end
end)
describe("format_to_local", function()
local tests = {
{ "2023-10-28T16:25:09.482Z", "-0500", "10/28/2023 at 11:25" },
{ "2016-11-22T1:25:09.482Z", "-0500", "11/21/2016 at 20:25" },
{ "2016-11-22T1:25:09.482Z", "-0000", "11/22/2016 at 01:25" },
{ "2017-3-22T13:25:09.482Z", "+0700", "03/22/2017 at 20:25" },
}
for _, val in ipairs(tests) do
local got = u.format_to_local(val[1], val[2])
local want = val[3]
assert.are.same(got, want)
end
end)
end)

View File

@@ -34,6 +34,21 @@ M.settings = {
resolved = "",
unresolved = "",
},
info = {
enabled = true,
horizontal = false,
fields = {
"author",
"created_at",
"updated_at",
"merge_status",
"draft",
"conflicts",
"assignees",
"branch",
"pipeline",
},
},
discussion_sign_and_diagnostic = {
skip_resolved_discussion = false,
skip_old_revision_discussion = false,

View File

@@ -151,6 +151,67 @@ M.reverse = function(list)
return rev
end
---Returns the difference between a time offset and UTC time, in seconds
---@param offset string The offset to compare, e.g. -0500 for EST
---@return number
M.offset_to_seconds = function(offset)
local sign, hours, minutes = offset:match("([%+%-])(%d%d)(%d%d)")
local offset_in_seconds = tonumber(hours) * 3600 + tonumber(minutes) * 60
if sign == "-" then
offset_in_seconds = -offset_in_seconds
end
return offset_in_seconds
end
---Converts a UTC timestamp and offset to a human readable datestring
---@param date_string string The time stamp
---@param offset string The offset of the user's local time zone, e.g. -0500 for EST
---@return string
M.format_to_local = function(date_string, offset)
local year, month, day, hour, min, sec, _, tzOffset = date_string:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+).(%d+)Z")
local localTime = os.time({
year = year,
month = month,
day = day,
hour = hour,
min = min,
sec = sec,
tzOffset = tzOffset,
})
local localTimestamp = localTime + M.offset_to_seconds(offset)
return tostring(os.date("%m/%d/%Y at %H:%M", localTimestamp))
end
-- Returns a comma separated (human readable) list of values from a list of associative tables
---@param list_of_tables table The list to traverse
---@param key string The key of the values to pull from the tables
---@return string
M.make_readable_list = function(list_of_tables, key)
local res = ""
for i, t in ipairs(list_of_tables) do
res = res .. t[key]
if i < #list_of_tables then
res = res .. ", "
end
end
return res
end
-- Returns the length of the longest string in a list of strings
---@param list table The list of strings
---@return number
M.get_longest_string = function(list)
local longest = 0
for _, v in pairs(list) do
if string.len(v) > longest then
longest = string.len(v)
end
end
return longest
end
M.notify = function(msg, lvl)
vim.notify("gitlab.nvim: " .. msg, lvl)
end