forge.nvim/lua/forge/gitlab.lua
2026-03-28 12:46:12 -04:00

398 lines
8.7 KiB
Lua

local forge = require('forge')
---@class forge.GitLab: forge.Forge
local M = {
name = 'gitlab',
cli = 'glab',
kinds = { issue = 'issue', pr = 'mr' },
labels = {
issue = 'Issues',
pr = 'MRs',
pr_one = 'MR',
pr_full = 'Merge Requests',
ci = 'CI/CD',
},
}
---@param state string
---@return string[]
function M:list_pr_json_cmd(state)
local cmd = {
'glab',
'mr',
'list',
'--per-page',
tostring(forge.config().display.limits.pulls),
'--output',
'json',
}
if state == 'closed' then
table.insert(cmd, '--closed')
elseif state == 'all' then
table.insert(cmd, '--all')
end
return cmd
end
---@param state string
---@return string[]
function M:list_issue_json_cmd(state)
local cmd = {
'glab',
'issue',
'list',
'--per-page',
tostring(forge.config().display.limits.issues),
'--output',
'json',
}
if state == 'closed' then
table.insert(cmd, '--closed')
elseif state == 'all' then
table.insert(cmd, '--all')
end
return cmd
end
function M:pr_json_fields()
return {
number = 'iid',
title = 'title',
branch = 'source_branch',
state = 'state',
author = 'author',
created_at = 'created_at',
}
end
function M:issue_json_fields()
return {
number = 'iid',
title = 'title',
state = 'state',
author = 'author',
created_at = 'created_at',
}
end
---@param kind string
---@param num string
function M:view_web(kind, num)
vim.system({ 'glab', kind, 'view', num, '--web' })
end
---@param loc string
---@param branch string
function M:browse(loc, branch)
local base = forge.remote_web_url()
local file, lines = loc:match('^(.+):(.+)$')
vim.ui.open(('%s/-/blob/%s/%s#L%s'):format(base, branch, file, lines))
end
function M:browse_root()
vim.system({ 'glab', 'repo', 'view', '--web' })
end
function M:browse_branch(branch)
local base = forge.remote_web_url()
vim.ui.open(base .. '/-/tree/' .. branch)
end
function M:browse_commit(sha)
local base = forge.remote_web_url()
vim.ui.open(base .. '/-/commit/' .. sha)
end
function M:checkout_cmd(num)
return { 'glab', 'mr', 'checkout', num }
end
---@param loc string
function M:yank_branch(loc)
local branch = vim.trim(vim.fn.system('git branch --show-current'))
local base = forge.remote_web_url()
local file, lines = loc:match('^(.+):(.+)$')
vim.fn.setreg('+', ('%s/-/blob/%s/%s#L%s'):format(base, branch, file, lines))
end
---@param loc string
function M:yank_commit(loc)
local commit = vim.trim(vim.fn.system('git rev-parse HEAD'))
local base = forge.remote_web_url()
local file, lines = loc:match('^(.+):(.+)$')
vim.fn.setreg('+', ('%s/-/blob/%s/%s#L%s'):format(base, commit, file, lines))
end
---@param num string
---@return string[]
function M:fetch_pr(num)
return {
'git',
'fetch',
'origin',
('merge-requests/%s/head:mr-%s'):format(num, num),
}
end
---@param num string
---@return string[]
function M:pr_base_cmd(num)
return {
'sh',
'-c',
('glab mr view %s -F json | jq -r .target_branch'):format(num),
}
end
---@param branch string
---@return string[]
function M:pr_for_branch_cmd(branch)
return {
'sh',
'-c',
("glab mr list --source-branch '%s' -F json | jq -r '.[0].iid // empty'"):format(branch),
}
end
---@param num string
---@return string
function M:checks_cmd(num)
local _ = num
return 'glab ci list'
end
---@param run_id string
---@param failed_only boolean
---@return string[]
function M:check_log_cmd(run_id, failed_only)
local _ = failed_only
local lines = forge.config().ci.lines
return {
'sh',
'-c',
('glab ci trace %s | tail -n %d'):format(run_id, lines),
}
end
---@param run_id string
---@return string[]
function M:check_tail_cmd(run_id)
return { 'glab', 'ci', 'trace', run_id }
end
function M:list_runs_json_cmd(branch)
local cmd = {
'glab',
'ci',
'list',
'--output',
'json',
'--per-page',
tostring(forge.config().display.limits.runs),
}
if branch then
table.insert(cmd, '--ref')
table.insert(cmd, branch)
end
return cmd
end
function M:normalize_run(entry)
local ref = entry.ref or ''
local mr_num = ref:match('^refs/merge%-requests/(%d+)/head$')
return {
id = tostring(entry.id or ''),
name = mr_num and ('!%s'):format(mr_num) or ref,
branch = '',
status = entry.status or '',
event = entry.source or '',
url = entry.web_url or '',
created_at = entry.created_at or '',
}
end
function M:run_log_cmd(id, failed_only)
local lines = forge.config().ci.lines
local jq_filter = failed_only and '[.[] | select(.status=="failed")][0].id // .[0].id'
or '.[0].id'
return {
'sh',
'-c',
('JOB=$(glab api \'projects/:id/pipelines/%s/jobs?per_page=100\' | jq -r \'%s\') && [ "$JOB" != "null" ] && glab ci trace "$JOB" | tail -n %d'):format(
id,
jq_filter,
lines
),
}
end
function M:run_tail_cmd(id)
local jq_filter = '[.[] | select(.status=="running" or .status=="pending")][0].id // .[0].id'
return {
'sh',
'-c',
('JOB=$(glab api \'projects/:id/pipelines/%s/jobs?per_page=100\' | jq -r \'%s\') && [ "$JOB" != "null" ] && glab ci trace "$JOB"'):format(
id,
jq_filter
),
}
end
---@param num string
---@param method string
---@return string[]
function M:merge_cmd(num, method)
local cmd = { 'glab', 'mr', 'merge', num }
if method == 'squash' then
table.insert(cmd, '--squash')
elseif method == 'rebase' then
table.insert(cmd, '--rebase')
end
return cmd
end
---@param num string
---@return string[]
function M:approve_cmd(num)
return { 'glab', 'mr', 'approve', num }
end
---@param num string
---@return string[]
function M:close_cmd(num)
return { 'glab', 'mr', 'close', num }
end
---@param num string
---@return string[]
function M:reopen_cmd(num)
return { 'glab', 'mr', 'reopen', num }
end
---@param num string
---@return string[]
function M:close_issue_cmd(num)
return { 'glab', 'issue', 'close', num }
end
---@param num string
---@return string[]
function M:reopen_issue_cmd(num)
return { 'glab', 'issue', 'reopen', num }
end
---@param title string
---@param body string
---@param base string
---@param draft boolean
---@param reviewers string[]?
---@return string[]
function M:create_pr_cmd(title, body, base, draft, reviewers)
local cmd = {
'glab',
'mr',
'create',
'--title',
title,
'--description',
body,
'--target-branch',
base,
'--yes',
}
if draft then
table.insert(cmd, '--draft')
end
for _, r in ipairs(reviewers or {}) do
table.insert(cmd, '--reviewer')
table.insert(cmd, r)
end
return cmd
end
---@return string[]
function M:create_pr_web_cmd()
return { 'glab', 'mr', 'create', '--web' }
end
---@return string[]
function M:default_branch_cmd()
return {
'sh',
'-c',
"glab repo view -F json | jq -r '.default_branch'",
}
end
---@return string[]
function M:template_paths()
return { '.gitlab/merge_request_templates/' }
end
---@param num string
---@param is_draft boolean
---@return string[]?
function M:draft_toggle_cmd(num, is_draft)
if is_draft then
return { 'glab', 'mr', 'update', num, '--ready' }
end
return { 'glab', 'mr', 'update', num, '--draft' }
end
---@return forge.RepoInfo
function M:repo_info()
local result = vim.system({ 'glab', 'api', 'projects/:id' }, { text = true }):wait()
local ok, data = pcall(vim.json.decode, result.stdout or '{}')
if not ok or type(data) ~= 'table' then
data = {}
end
local perms = type(data.permissions) == 'table' and data.permissions or {}
local pa = type(perms.project_access) == 'table' and perms.project_access or {}
local ga = type(perms.group_access) == 'table' and perms.group_access or {}
local access = pa.access_level or 0
local group_access = ga.access_level or 0
local level = math.max(access, group_access)
local permission = 'READ'
if level >= 40 then
permission = 'ADMIN'
elseif level >= 30 then
permission = 'WRITE'
end
local methods = {}
local merge_method = data.merge_method or 'merge'
if merge_method == 'ff' or merge_method == 'rebase_merge' then
table.insert(methods, 'rebase')
else
table.insert(methods, 'merge')
end
if data.squash_option ~= 'never' then
table.insert(methods, 'squash')
end
return {
permission = permission,
merge_methods = methods,
}
end
---@param num string
---@return forge.PRState
function M:pr_state(num)
local result = vim
.system({ 'glab', 'mr', 'view', num, '--output', 'json' }, { text = true })
:wait()
local ok, data = pcall(vim.json.decode, result.stdout or '{}')
if not ok or type(data) ~= 'table' then
data = {}
end
return {
state = (data.state or 'unknown'):upper(),
mergeable = data.merge_status or 'unknown',
review_decision = '',
is_draft = data.draft == true,
}
end
return M