mirror of
https://github.com/harivansh-afk/forge.nvim.git
synced 2026-04-15 04:03:29 +00:00
feat: cli
This commit is contained in:
parent
a51dc43de7
commit
5fcbcfcf99
8 changed files with 1727 additions and 1019 deletions
|
|
@ -16,11 +16,6 @@ local M = {
|
|||
|
||||
---@param kind string
|
||||
---@param state string
|
||||
---@return string
|
||||
function M:list_cmd(kind, state)
|
||||
return ('tea %s list --state %s'):format(kind, state)
|
||||
end
|
||||
|
||||
---@param state string
|
||||
---@return string[]
|
||||
function M:list_pr_json_cmd(state)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@ end
|
|||
|
||||
---@param kind string
|
||||
---@param state string
|
||||
---@return string
|
||||
function M:list_cmd(kind, state)
|
||||
return ('gh %s list --limit 100 --state %s'):format(kind, state)
|
||||
end
|
||||
|
||||
---@param state string
|
||||
---@return string[]
|
||||
function M:list_pr_json_cmd(state)
|
||||
|
|
@ -34,7 +29,7 @@ function M:list_pr_json_cmd(state)
|
|||
'pr',
|
||||
'list',
|
||||
'--limit',
|
||||
'100',
|
||||
tostring(forge.config().display.limits.pulls),
|
||||
'--state',
|
||||
state,
|
||||
'--json',
|
||||
|
|
@ -50,7 +45,7 @@ function M:list_issue_json_cmd(state)
|
|||
'issue',
|
||||
'list',
|
||||
'--limit',
|
||||
'100',
|
||||
tostring(forge.config().display.limits.issues),
|
||||
'--state',
|
||||
state,
|
||||
'--json',
|
||||
|
|
@ -200,7 +195,7 @@ function M:list_runs_json_cmd(branch)
|
|||
'--json',
|
||||
'databaseId,name,headBranch,status,conclusion,event,url,createdAt',
|
||||
'--limit',
|
||||
'30',
|
||||
tostring(forge.config().display.limits.runs),
|
||||
}
|
||||
if branch then
|
||||
table.insert(cmd, '--branch')
|
||||
|
|
|
|||
|
|
@ -16,17 +16,6 @@ local M = {
|
|||
|
||||
---@param kind string
|
||||
---@param state string
|
||||
---@return string
|
||||
function M:list_cmd(kind, state)
|
||||
local cmd = ('glab %s list --per-page 100'):format(kind)
|
||||
if state == 'closed' then
|
||||
cmd = cmd .. ' --closed'
|
||||
elseif state == 'all' then
|
||||
cmd = cmd .. ' --all'
|
||||
end
|
||||
return cmd
|
||||
end
|
||||
|
||||
---@param state string
|
||||
---@return string[]
|
||||
function M:list_pr_json_cmd(state)
|
||||
|
|
@ -35,7 +24,7 @@ function M:list_pr_json_cmd(state)
|
|||
'mr',
|
||||
'list',
|
||||
'--per-page',
|
||||
'100',
|
||||
tostring(forge.config().display.limits.pulls),
|
||||
'--output',
|
||||
'json',
|
||||
}
|
||||
|
|
@ -55,7 +44,7 @@ function M:list_issue_json_cmd(state)
|
|||
'issue',
|
||||
'list',
|
||||
'--per-page',
|
||||
'100',
|
||||
tostring(forge.config().display.limits.issues),
|
||||
'--output',
|
||||
'json',
|
||||
}
|
||||
|
|
@ -201,7 +190,7 @@ function M:list_runs_json_cmd(branch)
|
|||
'--output',
|
||||
'json',
|
||||
'--per-page',
|
||||
'30',
|
||||
tostring(forge.config().display.limits.runs),
|
||||
}
|
||||
if branch then
|
||||
table.insert(cmd, '--ref')
|
||||
|
|
|
|||
|
|
@ -42,6 +42,17 @@ function M.check()
|
|||
else
|
||||
vim.health.info('vim-fugitive not found (fugitive keymaps disabled)')
|
||||
end
|
||||
|
||||
local forge_mod = require('forge')
|
||||
for name, source in pairs(forge_mod.registered_sources()) do
|
||||
if name ~= 'github' and name ~= 'gitlab' and name ~= 'codeberg' then
|
||||
if vim.fn.executable(source.cli) == 1 then
|
||||
vim.health.ok(source.cli .. ' found (custom: ' .. name .. ')')
|
||||
else
|
||||
vim.health.warn(source.cli .. ' not found (custom: ' .. name .. ' disabled)')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,5 +1,67 @@
|
|||
local M = {}
|
||||
|
||||
local DEFAULTS = {
|
||||
ci = { lines = 10000 },
|
||||
sources = {},
|
||||
keys = {
|
||||
picker = '<c-g>',
|
||||
next_qf = ']q',
|
||||
prev_qf = '[q',
|
||||
next_loc = ']l',
|
||||
prev_loc = '[l',
|
||||
review_toggle = 's',
|
||||
terminal_open = 'gx',
|
||||
fugitive = {
|
||||
create = 'cpr',
|
||||
create_draft = 'cpd',
|
||||
create_fill = 'cpf',
|
||||
create_web = 'cpw',
|
||||
},
|
||||
},
|
||||
picker_keys = {
|
||||
pr = { checkout = 'default', diff = 'ctrl-d', worktree = 'ctrl-w', checks = 'ctrl-t', browse = 'ctrl-x', manage = 'ctrl-e', create = 'ctrl-a', toggle = 'ctrl-o', refresh = 'ctrl-r' },
|
||||
issue = { browse = 'default', close_reopen = 'ctrl-s', toggle = 'ctrl-o', refresh = 'ctrl-r' },
|
||||
checks = { log = 'default', browse = 'ctrl-x', failed = 'ctrl-f', passed = 'ctrl-p', running = 'ctrl-n', all = 'ctrl-a' },
|
||||
ci = { log = 'default', browse = 'ctrl-x', refresh = 'ctrl-r' },
|
||||
commits = { checkout = 'default', diff = 'ctrl-d', browse = 'ctrl-x', yank = 'ctrl-y' },
|
||||
branches = { diff = 'ctrl-d', browse = 'ctrl-x' },
|
||||
},
|
||||
display = {
|
||||
icons = {
|
||||
open = '+',
|
||||
merged = 'm',
|
||||
closed = 'x',
|
||||
pass = '*',
|
||||
fail = 'x',
|
||||
pending = '~',
|
||||
skip = '-',
|
||||
unknown = '?',
|
||||
},
|
||||
widths = {
|
||||
title = 45,
|
||||
author = 15,
|
||||
name = 35,
|
||||
branch = 25,
|
||||
},
|
||||
limits = {
|
||||
pulls = 100,
|
||||
issues = 100,
|
||||
runs = 30,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@type table<string, forge.Forge>
|
||||
local sources = {}
|
||||
|
||||
function M.register(name, source)
|
||||
sources[name] = source
|
||||
end
|
||||
|
||||
function M.registered_sources()
|
||||
return sources
|
||||
end
|
||||
|
||||
local hl_defaults = {
|
||||
ForgeComposeComment = 'Comment',
|
||||
ForgeComposeBranch = 'Special',
|
||||
|
|
@ -62,7 +124,6 @@ end
|
|||
---@field cli string
|
||||
---@field kinds { issue: string, pr: string }
|
||||
---@field labels { issue: string, pr: string, pr_one: string, pr_full: string, ci: string }
|
||||
---@field list_cmd fun(self: forge.Forge, kind: string, state: string): string
|
||||
---@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 }
|
||||
|
|
@ -126,21 +187,45 @@ local function git_root()
|
|||
return root
|
||||
end
|
||||
|
||||
local builtin_hosts = {
|
||||
github = { 'github' },
|
||||
gitlab = { 'gitlab' },
|
||||
codeberg = { 'codeberg', 'gitea', 'forgejo' },
|
||||
}
|
||||
|
||||
local function resolve_source(name)
|
||||
if sources[name] then
|
||||
return sources[name]
|
||||
end
|
||||
local ok, mod = pcall(require, 'forge.' .. name)
|
||||
if ok then
|
||||
sources[name] = mod
|
||||
return mod
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param remote string
|
||||
---@return string? forge_name
|
||||
local function detect_from_remote(remote)
|
||||
if remote:find('github') and vim.fn.executable('gh') == 1 then
|
||||
return 'github'
|
||||
local cfg = M.config().sources
|
||||
|
||||
for name, opts in pairs(cfg) do
|
||||
for _, host in ipairs(opts.hosts or {}) do
|
||||
if remote:find(host, 1, true) then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
if remote:find('gitlab') and vim.fn.executable('glab') == 1 then
|
||||
return 'gitlab'
|
||||
end
|
||||
if
|
||||
(remote:find('codeberg') or remote:find('gitea') or remote:find('forgejo'))
|
||||
and vim.fn.executable('tea') == 1
|
||||
then
|
||||
return 'codeberg'
|
||||
|
||||
for name, patterns in pairs(builtin_hosts) do
|
||||
for _, pattern in ipairs(patterns) do
|
||||
if remote:find(pattern, 1, true) then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
|
|
@ -161,9 +246,15 @@ function M.detect()
|
|||
if not name then
|
||||
return nil
|
||||
end
|
||||
local f = require('forge.' .. name)
|
||||
forge_cache[root] = f
|
||||
return f
|
||||
local source = resolve_source(name)
|
||||
if not source then
|
||||
return nil
|
||||
end
|
||||
if vim.fn.executable(source.cli) ~= 1 then
|
||||
return nil
|
||||
end
|
||||
forge_cache[root] = source
|
||||
return source
|
||||
end
|
||||
|
||||
---@param f forge.Forge
|
||||
|
|
@ -362,6 +453,9 @@ end
|
|||
---@param show_state boolean
|
||||
---@return string
|
||||
function M.format_pr(entry, fields, show_state)
|
||||
local display = M.config().display
|
||||
local icons = display.icons
|
||||
local widths = display.widths
|
||||
local num = tostring(entry[fields.number] or '')
|
||||
local title = entry[fields.title] or ''
|
||||
local author = extract_author(entry, fields.author)
|
||||
|
|
@ -371,19 +465,19 @@ function M.format_pr(entry, fields, show_state)
|
|||
local state = (entry[fields.state] or ''):lower()
|
||||
local icon, color
|
||||
if state == 'open' or state == 'opened' then
|
||||
icon, color = '+', '\27[34m'
|
||||
icon, color = icons.open, '\27[34m'
|
||||
elseif state == 'merged' then
|
||||
icon, color = 'm', '\27[35m'
|
||||
icon, color = icons.merged, '\27[35m'
|
||||
else
|
||||
icon, color = 'x', '\27[31m'
|
||||
icon, color = icons.closed, '\27[31m'
|
||||
end
|
||||
prefix = color .. icon .. '\27[0m '
|
||||
end
|
||||
return ('%s\27[34m#%-5s\27[0m %s \27[2m%-15s %3s\27[0m'):format(
|
||||
return ('%s\27[34m#%-5s\27[0m %s \27[2m%-' .. widths.author .. 's %3s\27[0m'):format(
|
||||
prefix,
|
||||
num,
|
||||
pad_or_truncate(title, 45),
|
||||
pad_or_truncate(author, 15),
|
||||
pad_or_truncate(title, widths.title),
|
||||
pad_or_truncate(author, widths.author),
|
||||
age
|
||||
)
|
||||
end
|
||||
|
|
@ -393,6 +487,9 @@ end
|
|||
---@param show_state boolean
|
||||
---@return string
|
||||
function M.format_issue(entry, fields, show_state)
|
||||
local display = M.config().display
|
||||
local icons = display.icons
|
||||
local widths = display.widths
|
||||
local num = tostring(entry[fields.number] or '')
|
||||
local title = entry[fields.title] or ''
|
||||
local author = extract_author(entry, fields.author)
|
||||
|
|
@ -402,17 +499,17 @@ function M.format_issue(entry, fields, show_state)
|
|||
local state = (entry[fields.state] or ''):lower()
|
||||
local icon, color
|
||||
if state == 'open' or state == 'opened' then
|
||||
icon, color = '+', '\27[34m'
|
||||
icon, color = icons.open, '\27[34m'
|
||||
else
|
||||
icon, color = '*', '\27[2m'
|
||||
icon, color = icons.closed, '\27[2m'
|
||||
end
|
||||
prefix = color .. icon .. '\27[0m '
|
||||
end
|
||||
return ('%s\27[34m#%-5s\27[0m %s \27[2m%-15s %3s\27[0m'):format(
|
||||
return ('%s\27[34m#%-5s\27[0m %s \27[2m%-' .. widths.author .. 's %3s\27[0m'):format(
|
||||
prefix,
|
||||
num,
|
||||
pad_or_truncate(title, 45),
|
||||
pad_or_truncate(author, 15),
|
||||
pad_or_truncate(title, widths.title),
|
||||
pad_or_truncate(author, widths.author),
|
||||
age
|
||||
)
|
||||
end
|
||||
|
|
@ -420,19 +517,22 @@ end
|
|||
---@param check table
|
||||
---@return string
|
||||
function M.format_check(check)
|
||||
local display = M.config().display
|
||||
local icons = display.icons
|
||||
local widths = display.widths
|
||||
local bucket = (check.bucket or 'pending'):lower()
|
||||
local name = check.name or ''
|
||||
local icon, color
|
||||
if bucket == 'pass' then
|
||||
icon, color = '*', '\27[32m'
|
||||
icon, color = icons.pass, '\27[32m'
|
||||
elseif bucket == 'fail' then
|
||||
icon, color = 'x', '\27[31m'
|
||||
icon, color = icons.fail, '\27[31m'
|
||||
elseif bucket == 'pending' then
|
||||
icon, color = '~', '\27[33m'
|
||||
icon, color = icons.pending, '\27[33m'
|
||||
elseif bucket == 'skipping' or bucket == 'cancel' then
|
||||
icon, color = '-', '\27[2m'
|
||||
icon, color = icons.skip, '\27[2m'
|
||||
else
|
||||
icon, color = '?', '\27[2m'
|
||||
icon, color = icons.unknown, '\27[2m'
|
||||
end
|
||||
local elapsed = ''
|
||||
if check.startedAt and check.completedAt and check.completedAt ~= '' then
|
||||
|
|
@ -447,33 +547,37 @@ function M.format_check(check)
|
|||
end
|
||||
end
|
||||
end
|
||||
return ('%s%s\27[0m %s \27[2m%s\27[0m'):format(color, icon, pad_or_truncate(name, 35), elapsed)
|
||||
return ('%s%s\27[0m %s \27[2m%s\27[0m'):format(color, icon, pad_or_truncate(name, widths.name), elapsed)
|
||||
end
|
||||
|
||||
---@param run forge.CIRun
|
||||
---@return string
|
||||
function M.format_run(run)
|
||||
local display = M.config().display
|
||||
local icons = display.icons
|
||||
local widths = display.widths
|
||||
local icon, color
|
||||
local s = run.status:lower()
|
||||
if s == 'success' then
|
||||
icon, color = '*', '\27[32m'
|
||||
icon, color = icons.pass, '\27[32m'
|
||||
elseif s == 'failure' or s == 'failed' then
|
||||
icon, color = 'x', '\27[31m'
|
||||
icon, color = icons.fail, '\27[31m'
|
||||
elseif s == 'in_progress' or s == 'running' or s == 'pending' or s == 'queued' then
|
||||
icon, color = '~', '\27[33m'
|
||||
icon, color = icons.pending, '\27[33m'
|
||||
elseif s == 'cancelled' or s == 'canceled' or s == 'skipped' then
|
||||
icon, color = '-', '\27[2m'
|
||||
icon, color = icons.skip, '\27[2m'
|
||||
else
|
||||
icon, color = '?', '\27[2m'
|
||||
icon, color = icons.unknown, '\27[2m'
|
||||
end
|
||||
local event = abbreviate_event(run.event)
|
||||
local date = compact_date(run.created_at)
|
||||
if run.branch ~= '' then
|
||||
local name_w = widths.name - widths.branch + 10
|
||||
return ('%s%s\27[0m %s \27[36m%s\27[0m \27[2m%-6s %s\27[0m'):format(
|
||||
color,
|
||||
icon,
|
||||
pad_or_truncate(run.name, 20),
|
||||
pad_or_truncate(run.branch, 25),
|
||||
pad_or_truncate(run.name, name_w),
|
||||
pad_or_truncate(run.branch, widths.branch),
|
||||
event,
|
||||
date
|
||||
)
|
||||
|
|
@ -481,7 +585,7 @@ function M.format_run(run)
|
|||
return ('%s%s\27[0m %s \27[2m%-6s %s\27[0m'):format(
|
||||
color,
|
||||
icon,
|
||||
pad_or_truncate(run.name, 35),
|
||||
pad_or_truncate(run.name, widths.name),
|
||||
event,
|
||||
date
|
||||
)
|
||||
|
|
@ -510,14 +614,17 @@ function M.filter_checks(checks, filter)
|
|||
end
|
||||
|
||||
function M.config()
|
||||
return vim.tbl_deep_extend('force', {
|
||||
ci = { lines = 10000 },
|
||||
}, vim.g.forge or {})
|
||||
local user = vim.g.forge or {}
|
||||
local cfg = vim.tbl_deep_extend('force', DEFAULTS, user)
|
||||
if user.keys == false then
|
||||
cfg.keys = false
|
||||
end
|
||||
if user.picker_keys == false then
|
||||
cfg.picker_keys = false
|
||||
end
|
||||
return cfg
|
||||
end
|
||||
|
||||
---@type { base: string?, mode: 'unified'|'split' }
|
||||
M.review = { base = nil, mode = 'unified' }
|
||||
|
||||
---@param args string[]
|
||||
function M.yank_url(args)
|
||||
vim.system(args, { text = true }, function(result)
|
||||
|
|
|
|||
1031
lua/forge/pickers.lua
Normal file
1031
lua/forge/pickers.lua
Normal file
File diff suppressed because it is too large
Load diff
101
lua/forge/review.lua
Normal file
101
lua/forge/review.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
local M = {}
|
||||
|
||||
---@type { base: string?, mode: 'unified'|'split' }
|
||||
M.state = { base = nil, mode = 'unified' }
|
||||
|
||||
local review_augroup = vim.api.nvim_create_augroup('ForgeReview', { clear = true })
|
||||
|
||||
local function close_view()
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name:match('^fugitive://') or name:match('^diffs://review:') then
|
||||
pcall(vim.api.nvim_win_close, win, true)
|
||||
end
|
||||
end
|
||||
pcall(vim.cmd, 'diffoff!')
|
||||
end
|
||||
|
||||
function M.stop()
|
||||
M.state.base = nil
|
||||
M.state.mode = 'unified'
|
||||
local cfg = require('forge').config()
|
||||
local lhs = cfg.keys ~= false and cfg.keys.review_toggle
|
||||
if lhs then
|
||||
pcall(vim.keymap.del, 'n', lhs)
|
||||
end
|
||||
vim.api.nvim_clear_autocmds({ group = review_augroup })
|
||||
end
|
||||
|
||||
function M.toggle()
|
||||
if not M.state.base then
|
||||
return
|
||||
end
|
||||
if M.state.mode == 'unified' then
|
||||
local ok, commands = pcall(require, 'diffs.commands')
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
local file = commands.review_file_at_line(vim.api.nvim_get_current_buf(), vim.fn.line('.'))
|
||||
M.state.mode = 'split'
|
||||
if file then
|
||||
vim.cmd('edit ' .. vim.fn.fnameescape(file))
|
||||
pcall(vim.cmd, 'Gvdiffsplit ' .. M.state.base)
|
||||
end
|
||||
else
|
||||
local current_file = vim.fn.expand('%:.')
|
||||
close_view()
|
||||
M.state.mode = 'unified'
|
||||
local ok, commands = pcall(require, 'diffs.commands')
|
||||
if ok then
|
||||
commands.greview(M.state.base)
|
||||
end
|
||||
if current_file ~= '' then
|
||||
vim.fn.search('diff %-%-git a/' .. vim.pesc(current_file), 'cw')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param base string
|
||||
---@param mode string?
|
||||
function M.start(base, mode)
|
||||
M.state.base = base
|
||||
M.state.mode = mode or 'unified'
|
||||
local cfg = require('forge').config()
|
||||
local lhs = cfg.keys ~= false and cfg.keys.review_toggle
|
||||
if lhs then
|
||||
vim.keymap.set('n', lhs, M.toggle, { desc = 'toggle review split/unified' })
|
||||
end
|
||||
vim.api.nvim_clear_autocmds({ group = review_augroup })
|
||||
vim.api.nvim_create_autocmd('BufWipeout', {
|
||||
group = review_augroup,
|
||||
pattern = 'diffs://review:*',
|
||||
callback = M.stop,
|
||||
})
|
||||
end
|
||||
|
||||
---@param nav_cmd string
|
||||
---@return function
|
||||
function M.nav(nav_cmd)
|
||||
return function()
|
||||
if M.state.base and M.state.mode == 'split' then
|
||||
close_view()
|
||||
end
|
||||
local wrap = {
|
||||
cnext = 'cfirst',
|
||||
cprev = 'clast',
|
||||
lnext = 'lfirst',
|
||||
lprev = 'llast',
|
||||
}
|
||||
if not pcall(vim.cmd, nav_cmd) then
|
||||
if not pcall(vim.cmd, wrap[nav_cmd]) then
|
||||
return
|
||||
end
|
||||
end
|
||||
if M.state.base and M.state.mode == 'split' then
|
||||
pcall(vim.cmd, 'Gvdiffsplit ' .. M.state.base)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue