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.
430 lines
11 KiB
Lua
430 lines
11 KiB
Lua
vim.api.nvim_create_autocmd('FileType', {
|
|
pattern = 'qf',
|
|
callback = function()
|
|
local info = vim.fn.getwininfo(vim.api.nvim_get_current_win())[1]
|
|
local items = info.loclist == 1 and vim.fn.getloclist(0) or vim.fn.getqflist()
|
|
if #items == 0 then
|
|
return
|
|
end
|
|
local bufname = vim.fn.bufname(items[1].bufnr)
|
|
if not bufname:match('^diffs://') then
|
|
return
|
|
end
|
|
vim.fn.matchadd('DiffAdd', [[\v\+\d+]])
|
|
vim.fn.matchadd('DiffDelete', [[\v-\d+]])
|
|
vim.fn.matchadd('DiffChange', [[\v\s\zsM\ze\s]])
|
|
vim.fn.matchadd('diffAdded', [[\v\s\zsA\ze\s]])
|
|
vim.fn.matchadd('DiffDelete', [[\v\s\zsD\ze\s]])
|
|
vim.fn.matchadd('DiffText', [[\v\s\zsR\ze\s]])
|
|
end,
|
|
})
|
|
|
|
local function require_forge_or_warn()
|
|
local forge_mod = require('forge')
|
|
local f = forge_mod.detect()
|
|
if not f then
|
|
vim.notify('[forge]: no forge detected', vim.log.levels.WARN)
|
|
return nil, forge_mod
|
|
end
|
|
return f, forge_mod
|
|
end
|
|
|
|
local function require_git_or_warn()
|
|
vim.fn.system('git rev-parse --show-toplevel')
|
|
if vim.v.shell_error ~= 0 then
|
|
vim.notify('[forge]: not a git repository', vim.log.levels.WARN)
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function parse_flags(args, start)
|
|
local flags = {}
|
|
local positional = {}
|
|
for i = start, #args do
|
|
local flag = args[i]:match('^%-%-(.+)$')
|
|
if flag then
|
|
local fk, fv = flag:match('^(.-)=(.+)$')
|
|
if fk then
|
|
flags[fk] = fv
|
|
else
|
|
flags[flag] = true
|
|
end
|
|
else
|
|
table.insert(positional, args[i])
|
|
end
|
|
end
|
|
return flags, positional
|
|
end
|
|
|
|
local function dispatch(args)
|
|
local sub = args[1]
|
|
|
|
if sub == 'pr' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local f, forge_mod = require_forge_or_warn()
|
|
if not f then
|
|
return
|
|
end
|
|
local pickers = require('forge.pickers')
|
|
if #args == 1 then
|
|
pickers.pr('open', f)
|
|
return
|
|
end
|
|
local flags, pos = parse_flags(args, 2)
|
|
if flags.state then
|
|
pickers.pr(flags.state, f)
|
|
return
|
|
end
|
|
local action = pos[1]
|
|
if action == 'create' then
|
|
local cf = parse_flags(args, 3)
|
|
local opts = {}
|
|
if cf.draft then
|
|
opts.draft = true
|
|
end
|
|
if cf.fill then
|
|
opts.instant = true
|
|
end
|
|
if cf.web then
|
|
opts.web = true
|
|
end
|
|
forge_mod.create_pr(opts)
|
|
return
|
|
end
|
|
local num = pos[2]
|
|
if not num then
|
|
vim.notify('[forge]: missing argument', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
if action == 'checkout' then
|
|
pickers.pr_actions(f, num)._by_name.checkout()
|
|
elseif action == 'diff' then
|
|
pickers.pr_actions(f, num)._by_name.diff()
|
|
elseif action == 'worktree' then
|
|
pickers.pr_actions(f, num)._by_name.worktree()
|
|
elseif action == 'ci' then
|
|
if f.capabilities.per_pr_checks then
|
|
pickers.checks(f, num)
|
|
else
|
|
require('forge').log(
|
|
('per-%s checks unavailable on %s, showing repo CI'):format(f.labels.pr_one, f.name)
|
|
)
|
|
pickers.ci(f)
|
|
end
|
|
elseif action == 'browse' then
|
|
f:view_web(f.kinds.pr, num)
|
|
elseif action == 'manage' then
|
|
pickers.pr_manage(f, num)
|
|
else
|
|
vim.notify('[forge]: unknown pr action: ' .. action, vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'issue' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local f = require_forge_or_warn()
|
|
if not f then
|
|
return
|
|
end
|
|
local pickers = require('forge.pickers')
|
|
if #args == 1 then
|
|
pickers.issue('all', f)
|
|
return
|
|
end
|
|
local flags, pos = parse_flags(args, 2)
|
|
if flags.state then
|
|
pickers.issue(flags.state, f)
|
|
return
|
|
end
|
|
local action = pos[1]
|
|
local num = pos[2]
|
|
if action == 'browse' then
|
|
if not num then
|
|
vim.notify('[forge]: missing issue number', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
f:view_web(f.kinds.issue, num)
|
|
elseif action == 'close' then
|
|
if not num then
|
|
vim.notify('[forge]: missing issue number', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
pickers.issue_close(f, num)
|
|
elseif action == 'reopen' then
|
|
if not num then
|
|
vim.notify('[forge]: missing issue number', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
pickers.issue_reopen(f, num)
|
|
else
|
|
vim.notify('[forge]: unknown issue action: ' .. (action or ''), vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'ci' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local f = require_forge_or_warn()
|
|
if not f then
|
|
return
|
|
end
|
|
local flags = parse_flags(args, 2)
|
|
local branch
|
|
if not flags.all then
|
|
branch = vim.trim(vim.fn.system('git branch --show-current'))
|
|
if branch == '' then
|
|
branch = nil
|
|
end
|
|
end
|
|
require('forge.pickers').ci(f, branch)
|
|
return
|
|
end
|
|
|
|
if sub == 'commit' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local forge_mod = require('forge')
|
|
local f = forge_mod.detect()
|
|
local pickers = require('forge.pickers')
|
|
if #args == 1 then
|
|
pickers.commits(f)
|
|
return
|
|
end
|
|
local _, pos = parse_flags(args, 2)
|
|
local action = pos[1]
|
|
local sha = pos[2]
|
|
if not sha then
|
|
vim.notify('[forge]: missing commit sha', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
if action == 'checkout' then
|
|
forge_mod.log_now('checking out ' .. sha .. '...')
|
|
vim.system({ 'git', 'checkout', sha }, { text = true }, function(result)
|
|
vim.schedule(function()
|
|
if result.code == 0 then
|
|
vim.notify(('[forge]: checked out %s (detached)'):format(sha))
|
|
else
|
|
vim.notify('[forge]: checkout failed', vim.log.levels.ERROR)
|
|
end
|
|
vim.cmd.redraw()
|
|
end)
|
|
end)
|
|
elseif action == 'diff' then
|
|
local review = require('forge.review')
|
|
local range = sha .. '^..' .. sha
|
|
review.start(range)
|
|
local ok, commands = pcall(require, 'diffs.commands')
|
|
if ok then
|
|
commands.greview(range)
|
|
end
|
|
forge_mod.log_now('reviewing ' .. sha)
|
|
elseif action == 'browse' then
|
|
if not f then
|
|
vim.notify('[forge]: no forge detected', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
f:browse_commit(sha)
|
|
else
|
|
vim.notify('[forge]: unknown commit action: ' .. action, vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'branch' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local forge_mod = require('forge')
|
|
local f = forge_mod.detect()
|
|
local pickers = require('forge.pickers')
|
|
if #args == 1 then
|
|
pickers.branches(f)
|
|
return
|
|
end
|
|
local _, pos = parse_flags(args, 2)
|
|
local action = pos[1]
|
|
local name = pos[2]
|
|
if not name then
|
|
vim.notify('[forge]: missing branch name', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
if action == 'diff' then
|
|
local review = require('forge.review')
|
|
review.start(name)
|
|
local ok, commands = pcall(require, 'diffs.commands')
|
|
if ok then
|
|
commands.greview(name)
|
|
end
|
|
forge_mod.log_now('reviewing ' .. name)
|
|
elseif action == 'browse' then
|
|
if not f then
|
|
vim.notify('[forge]: no forge detected', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
f:browse_branch(name)
|
|
else
|
|
vim.notify('[forge]: unknown branch action: ' .. action, vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'worktree' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
require('fzf-lua').git_worktrees()
|
|
return
|
|
end
|
|
|
|
if sub == 'browse' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local f = require_forge_or_warn()
|
|
if not f then
|
|
return
|
|
end
|
|
local flags = parse_flags(args, 2)
|
|
if flags.root then
|
|
f:browse_root()
|
|
elseif flags.commit then
|
|
local sha = vim.trim(vim.fn.system('git rev-parse HEAD'))
|
|
f:browse_commit(sha)
|
|
else
|
|
local forge_mod = require('forge')
|
|
local loc = forge_mod.file_loc()
|
|
local branch = vim.trim(vim.fn.system('git branch --show-current'))
|
|
if branch == '' then
|
|
vim.notify('[forge]: detached HEAD', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
f:browse(loc, branch)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'yank' then
|
|
if not require_git_or_warn() then
|
|
return
|
|
end
|
|
local f = require_forge_or_warn()
|
|
if not f then
|
|
return
|
|
end
|
|
local forge_mod = require('forge')
|
|
local loc = forge_mod.file_loc()
|
|
local flags = parse_flags(args, 2)
|
|
if flags.commit then
|
|
f:yank_commit(loc)
|
|
else
|
|
f:yank_branch(loc)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'review' then
|
|
local review = require('forge.review')
|
|
if #args < 2 then
|
|
vim.notify('[forge]: missing review action (end, toggle)', vim.log.levels.WARN)
|
|
return
|
|
end
|
|
local action = args[2]
|
|
if action == 'end' then
|
|
review.stop()
|
|
elseif action == 'toggle' then
|
|
review.toggle()
|
|
else
|
|
vim.notify('[forge]: unknown review action: ' .. action, vim.log.levels.WARN)
|
|
end
|
|
return
|
|
end
|
|
|
|
if sub == 'clear' then
|
|
require('forge').clear_cache()
|
|
vim.notify('[forge]: cache cleared')
|
|
return
|
|
end
|
|
|
|
vim.notify('[forge]: unknown command: ' .. sub, vim.log.levels.WARN)
|
|
end
|
|
|
|
local function complete(arglead, cmdline, _)
|
|
local words = {}
|
|
for word in cmdline:gmatch('%S+') do
|
|
table.insert(words, word)
|
|
end
|
|
local arg_idx = arglead == '' and #words or #words - 1
|
|
|
|
local subcmds =
|
|
{ 'pr', 'issue', 'ci', 'commit', 'branch', 'worktree', 'browse', 'yank', 'review', 'clear' }
|
|
local sub_actions = {
|
|
pr = { 'checkout', 'diff', 'worktree', 'ci', 'browse', 'manage', 'create', '--state=' },
|
|
issue = { 'browse', 'close', 'reopen', '--state=' },
|
|
ci = { '--all' },
|
|
commit = { 'checkout', 'diff', 'browse' },
|
|
branch = { 'diff', 'browse' },
|
|
review = { 'end', 'toggle' },
|
|
browse = { '--root', '--commit' },
|
|
yank = { '--commit' },
|
|
}
|
|
local flag_values = {
|
|
['--state'] = { 'open', 'closed', 'all' },
|
|
}
|
|
local create_flags = { '--draft', '--fill', '--web' }
|
|
|
|
local function filter(candidates)
|
|
return vim.tbl_filter(function(s)
|
|
return s:find(arglead, 1, true) == 1
|
|
end, candidates)
|
|
end
|
|
|
|
local flag, value_prefix = arglead:match('^(%-%-[^=]+)=(.*)$')
|
|
if flag and flag_values[flag] then
|
|
return vim.tbl_map(
|
|
function(v)
|
|
return flag .. '=' .. v
|
|
end,
|
|
vim.tbl_filter(function(v)
|
|
return v:find(value_prefix, 1, true) == 1
|
|
end, flag_values[flag])
|
|
)
|
|
end
|
|
|
|
if arg_idx == 1 then
|
|
return filter(subcmds)
|
|
end
|
|
|
|
local sub = words[2]
|
|
|
|
if arg_idx == 2 then
|
|
return filter(sub_actions[sub] or {})
|
|
end
|
|
|
|
if sub == 'pr' and words[3] == 'create' then
|
|
return filter(create_flags)
|
|
end
|
|
|
|
return {}
|
|
end
|
|
|
|
vim.api.nvim_create_user_command('Forge', function(opts)
|
|
local args = vim.split(vim.trim(opts.args), '%s+')
|
|
if #args == 0 or args[1] == '' then
|
|
require('forge.pickers').git()
|
|
return
|
|
end
|
|
dispatch(args)
|
|
end, {
|
|
nargs = '*',
|
|
complete = complete,
|
|
desc = 'forge.nvim',
|
|
})
|