mirror of
https://github.com/harivansh-afk/forge.nvim.git
synced 2026-04-15 06:04:41 +00:00
Problem: forge.nvim silently ignored unsupported features on non-GitHub forges. Codeberg `pr_for_branch_cmd` blocked all PR creation, CI picker had zero actions, `repo_info` was hardcoded, and the compose buffer showed draft/reviewer fields that did nothing. Solution: add `forge.Capabilities` declaration (`draft`, `reviewers`, `per_pr_checks`, `ci_json`) to each source. Compose buffer hides unsupported fields. Per-PR checks falls back to repo-wide CI with a notification. Fix Codeberg `pr_for_branch_cmd` to filter by branch via jq, implement `repo_info` and `list_runs_json_cmd` via Gitea API, add `default_branch_cmd` fallback, and add yank notifications for GitLab/Codeberg.
408 lines
8.9 KiB
Lua
408 lines
8.9 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',
|
|
},
|
|
capabilities = {
|
|
draft = true,
|
|
reviewers = true,
|
|
per_pr_checks = false,
|
|
ci_json = true,
|
|
},
|
|
}
|
|
|
|
---@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('^(.+):(.+)$')
|
|
local url = ('%s/-/blob/%s/%s#L%s'):format(base, branch, file, lines)
|
|
vim.fn.setreg('+', url)
|
|
forge.log('URL copied')
|
|
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('^(.+):(.+)$')
|
|
local url = ('%s/-/blob/%s/%s#L%s'):format(base, commit, file, lines)
|
|
vim.fn.setreg('+', url)
|
|
forge.log('URL copied')
|
|
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
|