forge.nvim/plugin/forge.lua
Barrett Ruth 5fcbcfcf99
feat: cli
2026-03-28 00:24:26 -04:00

468 lines
13 KiB
Lua

local cfg = require('forge').config()
if cfg.keys ~= false then
local k = cfg.keys
if k.picker then
vim.keymap.set({ 'n', 'v' }, k.picker, function()
require('forge.pickers').git()
end, { desc = 'forge git picker' })
end
if k.next_qf then
vim.keymap.set('n', k.next_qf, require('forge.review').nav('cnext'), { desc = 'next quickfix entry' })
end
if k.prev_qf then
vim.keymap.set('n', k.prev_qf, require('forge.review').nav('cprev'), { desc = 'prev quickfix entry' })
end
if k.next_loc then
vim.keymap.set('n', k.next_loc, require('forge.review').nav('lnext'), { desc = 'next loclist entry' })
end
if k.prev_loc then
vim.keymap.set('n', k.prev_loc, require('forge.review').nav('lprev'), { desc = 'prev loclist entry' })
end
if k.fugitive ~= false then
vim.api.nvim_create_autocmd('FileType', {
pattern = 'fugitive',
callback = function(args)
local forge_mod = require('forge')
local f = forge_mod.detect()
if not f then
return
end
local fk = k.fugitive
local buf = args.buf
if fk.create then
vim.keymap.set('n', fk.create, function()
forge_mod.create_pr({ draft = false })
end, { buffer = buf, desc = 'create PR' })
end
if fk.create_draft then
vim.keymap.set('n', fk.create_draft, function()
forge_mod.create_pr({ draft = true })
end, { buffer = buf, desc = 'create draft PR' })
end
if fk.create_fill then
vim.keymap.set('n', fk.create_fill, function()
forge_mod.create_pr({ instant = true })
end, { buffer = buf, desc = 'create PR (fill)' })
end
if fk.create_web then
vim.keymap.set('n', fk.create_web, function()
forge_mod.create_pr({ web = true })
end, { buffer = buf, desc = 'create PR (web)' })
end
end,
})
end
end
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 == 'checks' then
pickers.checks(f, num)
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 == 'cache' then
if #args < 2 then
vim.notify('[forge]: missing cache action (clear)', vim.log.levels.WARN)
return
end
if args[2] == 'clear' then
require('forge').clear_cache()
vim.notify('[forge]: cache cleared')
else
vim.notify('[forge]: unknown cache action: ' .. args[2], vim.log.levels.WARN)
end
return
end
vim.notify('[forge]: unknown command: ' .. sub, vim.log.levels.WARN)
end
local function complete(arglead, cmdline, _)
local parts = vim.split(vim.trim(cmdline), '%s+')
local subcmds = { 'pr', 'issue', 'ci', 'commit', 'branch', 'worktree', 'browse', 'yank', 'review', 'cache' }
local sub_actions = {
pr = { 'checkout', 'diff', 'worktree', 'checks', 'browse', 'manage', 'create', '--state=open', '--state=closed', '--state=all' },
issue = { 'browse', 'close', 'reopen', '--state=open', '--state=closed', '--state=all' },
ci = { '--all' },
commit = { 'checkout', 'diff', 'browse' },
branch = { 'diff', 'browse' },
review = { 'end', 'toggle' },
cache = { 'clear' },
browse = { '--root', '--commit' },
yank = { '--commit' },
}
local create_flags = { '--draft', '--fill', '--web' }
if #parts <= 2 then
return vim.tbl_filter(function(s) return s:find(arglead, 1, true) == 1 end, subcmds)
end
local sub = parts[2]
if #parts == 3 or (#parts == 4 and sub == 'pr' and parts[3] == 'create') then
local candidates = sub_actions[sub] or {}
if sub == 'pr' and #parts >= 3 and parts[3] == 'create' then
candidates = create_flags
end
return vim.tbl_filter(function(s) return s:find(arglead, 1, true) == 1 end, candidates)
end
if sub == 'pr' and parts[3] == 'create' then
return vim.tbl_filter(function(s) return s:find(arglead, 1, true) == 1 end, 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',
})