From 2af47b6cf4ac572cc66c126486d8b8093f0b76e1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 28 Mar 2026 14:36:32 -0400 Subject: [PATCH] feat: add capabilities system and per-forge compatibility 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. --- lua/forge/codeberg.lua | 95 +++++++++++++++++++++++++++++++++--------- lua/forge/github.lua | 6 +++ lua/forge/gitlab.lua | 14 ++++++- lua/forge/init.lua | 35 ++++++++++------ lua/forge/pickers.lua | 9 +++- plugin/forge.lua | 9 +++- 6 files changed, 132 insertions(+), 36 deletions(-) diff --git a/lua/forge/codeberg.lua b/lua/forge/codeberg.lua index 9234821..f3554f8 100644 --- a/lua/forge/codeberg.lua +++ b/lua/forge/codeberg.lua @@ -12,6 +12,12 @@ local M = { pr_full = 'Pull Requests', ci = 'CI/CD', }, + capabilities = { + draft = false, + reviewers = false, + per_pr_checks = false, + ci_json = true, + }, } ---@param state string @@ -105,7 +111,9 @@ 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/src/branch/%s/%s#L%s'):format(base, branch, file, lines)) + local url = ('%s/src/branch/%s/%s#L%s'):format(base, branch, file, lines) + vim.fn.setreg('+', url) + forge.log('URL copied') end ---@param loc string @@ -113,7 +121,9 @@ 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/src/commit/%s/%s#L%s'):format(base, commit, file, lines)) + local url = ('%s/src/commit/%s/%s#L%s'):format(base, commit, file, lines) + vim.fn.setreg('+', url) + forge.log('URL copied') end ---@param num string @@ -128,19 +138,16 @@ function M:pr_base_cmd(num) return { 'tea', 'pr', num, '--fields', 'base', '--output', 'simple' } end ----@param _branch string +---@param branch string ---@return string[] -function M:pr_for_branch_cmd(_branch) +function M:pr_for_branch_cmd(branch) return { - 'tea', - 'pr', - 'list', - '--fields', - 'index,head', - '--output', - 'simple', - '--state', - 'open', + 'sh', + '-c', + ('tea pr list --state open --output json --fields index,head | jq -r \'[.[] | select(.head=="%s" or .head.name=="%s")][0].index // empty\''):format( + branch, + branch + ), } end @@ -170,8 +177,30 @@ function M:check_tail_cmd(run_id) return { 'tea', 'actions', 'runs', 'logs', run_id, '--follow' } end -function M:list_runs_cmd(_branch) - return 'tea actions runs list' +function M:list_runs_json_cmd(branch) + local limit = tostring(forge.config().display.limits.runs) + local cmd = 'tea api "/repos/:owner/:repo/actions/runs?limit=' .. limit + if branch then + cmd = cmd .. '&branch=' .. branch + end + cmd = cmd .. '" 2>/dev/null | jq -r ".workflow_runs // []"' + return { 'sh', '-c', cmd } +end + +function M:normalize_run(entry) + local status = entry.status or '' + if status == 'completed' then + status = entry.conclusion or 'unknown' + end + return { + id = tostring(entry.id or ''), + name = entry.name or '', + branch = entry.head_branch or '', + status = status, + event = entry.event or '', + url = entry.html_url or '', + created_at = entry.created_at or '', + } end function M:run_log_cmd(id, failed_only) @@ -256,7 +285,8 @@ function M:default_branch_cmd() return { 'sh', '-c', - "git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'", + "git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'" + .. " || tea api /repos/:owner/:repo 2>/dev/null | jq -r '.default_branch // empty'", } end @@ -278,10 +308,35 @@ end ---@return forge.RepoInfo function M:repo_info() - return { - permission = 'ADMIN', - merge_methods = { 'squash', 'rebase', 'merge' }, - } + local result = vim.system({ 'tea', 'api', '/repos/:owner/:repo' }, { text = true }):wait() + local ok, data = pcall(vim.json.decode, result.stdout or '{}') + if not ok or type(data) ~= 'table' then + return { permission = 'READ', merge_methods = { 'merge' } } + end + + local perms = type(data.permissions) == 'table' and data.permissions or {} + local permission = 'READ' + if perms.admin then + permission = 'ADMIN' + elseif perms.push then + permission = 'WRITE' + end + + local methods = {} + if data.allow_merge_commits ~= false then + table.insert(methods, 'merge') + end + if data.allow_squash_merge ~= false then + table.insert(methods, 'squash') + end + if data.allow_rebase ~= false then + table.insert(methods, 'rebase') + end + if #methods == 0 then + table.insert(methods, 'merge') + end + + return { permission = permission, merge_methods = methods } end ---@param num string diff --git a/lua/forge/github.lua b/lua/forge/github.lua index 5461387..b070344 100644 --- a/lua/forge/github.lua +++ b/lua/forge/github.lua @@ -12,6 +12,12 @@ local M = { pr_full = 'Pull Requests', ci = 'CI/CD', }, + capabilities = { + draft = true, + reviewers = true, + per_pr_checks = true, + ci_json = true, + }, } local function nwo() diff --git a/lua/forge/gitlab.lua b/lua/forge/gitlab.lua index 576e54d..4572c95 100644 --- a/lua/forge/gitlab.lua +++ b/lua/forge/gitlab.lua @@ -12,6 +12,12 @@ local M = { pr_full = 'Merge Requests', ci = 'CI/CD', }, + capabilities = { + draft = true, + reviewers = true, + per_pr_checks = false, + ci_json = true, + }, } ---@param state string @@ -112,7 +118,9 @@ 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)) + 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 @@ -120,7 +128,9 @@ 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)) + 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 diff --git a/lua/forge/init.lua b/lua/forge/init.lua index 3d22e43..906f8e8 100644 --- a/lua/forge/init.lua +++ b/lua/forge/init.lua @@ -216,11 +216,18 @@ end ---@field permission string ---@field merge_methods string[] +---@class forge.Capabilities +---@field draft boolean +---@field reviewers boolean +---@field per_pr_checks boolean +---@field ci_json boolean + ---@class forge.Forge ---@field name string ---@field cli string ---@field kinds { issue: string, pr: string } ---@field labels { issue: string, pr: string, pr_one: string, pr_full: string, ci: string } +---@field capabilities forge.Capabilities ---@field list_pr_json_cmd fun(self: forge.Forge, state: string): string[] ---@field list_issue_json_cmd fun(self: forge.Forge, state: string): string[] ---@field pr_json_fields fun(self: forge.Forge): { number: string, title: string, branch: string, state: string, author: string, created_at: string } @@ -929,9 +936,9 @@ local function open_compose_buffer(f, branch, base, draft) ---@param ln integer ---@param start integer ---@param len integer - ---@param hl string - local function mark(ln, start, len, hl) - table.insert(marks, { line = ln, col = start, end_col = start + len, hl = hl }) + ---@param hl_group string + local function mark(ln, start, len, hl_group) + table.insert(marks, { line = ln, col = start, end_col = start + len, hl = hl_group }) end add_line('