diff --git a/doc/pending.txt b/doc/pending.txt index 58ad4e0..9195c76 100644 --- a/doc/pending.txt +++ b/doc/pending.txt @@ -1501,20 +1501,18 @@ Configuration: ~ vim.g.pending = { forge = { auto_close = false, + warn_missing_cli = true, github = { - token = nil, icon = '', issue_format = '%i %o/%r#%n', instances = {}, }, gitlab = { - token = nil, icon = '', issue_format = '%i %o/%r#%n', instances = {}, }, codeberg = { - token = nil, icon = '', issue_format = '%i %o/%r#%n', instances = {}, @@ -1524,27 +1522,27 @@ Configuration: ~ < Top-level fields: ~ - {auto_close} (boolean, default: false) When true, tasks linked to - closed/merged remote issues are automatically marked - done on buffer open. + {auto_close} (boolean, default: false) When true, tasks linked to + closed/merged remote issues are automatically marked + done on buffer open. + {warn_missing_cli} (boolean, default: true) When true, warns once per + forge per session if the CLI is missing or fails. Fields (per forge): ~ - {token} (string, optional) API token for authenticated requests. - Falls back to CLI: `gh auth token` (GitHub), `glab auth - token` (GitLab). Codeberg uses token only. {icon} (string) Nerd font icon used in virtual text. {issue_format} (string) Format string for the inline overlay label. {instances} (string[]) Additional hostnames for self-hosted instances (e.g. `{ 'github.company.com' }`). Authentication: ~ -Token retrieval is CLI-preferred, config fallback: -1. GitHub: `gh auth token` stdout. Falls back to `forge.github.token`. -2. GitLab: `glab auth token` stdout. Falls back to `forge.gitlab.token`. -3. Codeberg: `forge.codeberg.token` only (no standard CLI). +Forge metadata fetching uses each forge's native CLI. No tokens are +configured in pending.nvim — authenticate once in your shell: +1. GitHub: `gh auth login` +2. GitLab: `glab auth login` +3. Codeberg: `tea login add` -Unauthenticated requests work for public repositories. Private repositories -require a token. +Public repositories work without authentication. Private repositories +require a logged-in CLI session. Metadata fetching: ~ On buffer open, tasks with a `_forge_ref` whose cached metadata is older diff --git a/lua/pending/config.lua b/lua/pending/config.lua index 4c35348..c777fc9 100644 --- a/lua/pending/config.lua +++ b/lua/pending/config.lua @@ -34,13 +34,13 @@ ---@field region? string ---@class pending.ForgeInstanceConfig ----@field token? string ---@field icon? string ---@field issue_format? string ---@field instances? string[] ---@class pending.ForgeConfig ---@field auto_close? boolean +---@field warn_missing_cli? boolean ---@field github? pending.ForgeInstanceConfig ---@field gitlab? pending.ForgeInstanceConfig ---@field codeberg? pending.ForgeInstanceConfig @@ -156,6 +156,7 @@ local defaults = { sync = {}, forge = { auto_close = false, + warn_missing_cli = true, github = { icon = 'îȘ„', issue_format = '%i %o/%r#%n', diff --git a/lua/pending/forge.lua b/lua/pending/forge.lua index c7724f8..8318d26 100644 --- a/lua/pending/forge.lua +++ b/lua/pending/forge.lua @@ -26,12 +26,22 @@ local FORGE_HOSTS = { } ---@type table -local FORGE_API_BASE = { - github = 'https://api.github.com', - gitlab = 'https://gitlab.com', - codeberg = 'https://codeberg.org', +local FORGE_CLI = { + github = 'gh', + gitlab = 'glab', + codeberg = 'tea', } +---@type table +local FORGE_AUTH_CMD = { + github = 'gh auth login', + gitlab = 'glab auth login', + codeberg = 'tea login add', +} + +---@type table +local _warned_forges = {} + ---@type table local SHORTHAND_PREFIX = { gh = 'github', @@ -230,37 +240,29 @@ function M.find_refs(text) end ---@param ref pending.ForgeRef ----@return string -function M._api_url(ref) +---@return string[] +function M._api_args(ref) if ref.forge == 'github' then - return FORGE_API_BASE.github - .. '/repos/' - .. ref.owner - .. '/' - .. ref.repo - .. '/issues/' - .. ref.number + return { + 'gh', + 'api', + '/repos/' .. ref.owner .. '/' .. ref.repo .. '/issues/' .. ref.number, + } elseif ref.forge == 'gitlab' then local encoded = (ref.owner .. '/' .. ref.repo):gsub('/', '%%2F') local endpoint = ref.type == 'merge_request' and 'merge_requests' or 'issues' - return FORGE_API_BASE.gitlab - .. '/api/v4/projects/' - .. encoded - .. '/' - .. endpoint - .. '/' - .. ref.number + return { + 'glab', + 'api', + '/projects/' .. encoded .. '/' .. endpoint .. '/' .. ref.number, + } else local endpoint = ref.type == 'pull_request' and 'pulls' or 'issues' - return FORGE_API_BASE.codeberg - .. '/api/v1/repos/' - .. ref.owner - .. '/' - .. ref.repo - .. '/' - .. endpoint - .. '/' - .. ref.number + return { + 'tea', + 'api', + '/repos/' .. ref.owner .. '/' .. ref.repo .. '/' .. endpoint .. '/' .. ref.number, + } end end @@ -287,49 +289,21 @@ function M.format_label(ref, cache) return text, hl end ----@param forge string ----@return string? -function M.get_token(forge) - local cfg = config.get().forge or {} - local forge_cfg = cfg[forge] or {} - if forge_cfg.token then - return forge_cfg.token - end - if forge == 'github' then - local result = vim.fn.system({ 'gh', 'auth', 'token' }) - if vim.v.shell_error == 0 and result and result ~= '' then - return vim.trim(result) - end - elseif forge == 'gitlab' then - local result = vim.fn.system({ 'glab', 'auth', 'token' }) - if vim.v.shell_error == 0 and result and result ~= '' then - return vim.trim(result) - end - end - return nil -end - ---@param ref pending.ForgeRef ---@param callback fun(cache: pending.ForgeCache?) function M.fetch_metadata(ref, callback) - local token = M.get_token(ref.forge) - local url = M._api_url(ref) - local args = { 'curl', '-s', '-L' } - if token then - table.insert(args, '-H') - if ref.forge == 'gitlab' then - table.insert(args, 'PRIVATE-TOKEN: ' .. token) - else - table.insert(args, 'Authorization: Bearer ' .. token) - end - end - table.insert(args, '-H') - table.insert(args, 'Accept: application/json') - table.insert(args, url) + local args = M._api_args(ref) vim.system(args, { text = true }, function(result) if result.code ~= 0 or not result.stdout or result.stdout == '' then vim.schedule(function() + local forge_cfg = config.get().forge or {} + if forge_cfg.warn_missing_cli ~= false and not _warned_forges[ref.forge] then + _warned_forges[ref.forge] = true + local cli = FORGE_CLI[ref.forge] + local auth_cmd = FORGE_AUTH_CMD[ref.forge] + log.warn(('%s not found or not authenticated — run `%s`'):format(cli, auth_cmd)) + end callback(nil) end) return diff --git a/lua/pending/health.lua b/lua/pending/health.lua index f819269..209d7db 100644 --- a/lua/pending/health.lua +++ b/lua/pending/health.lua @@ -46,6 +46,20 @@ function M.check() end end + vim.health.start('pending.nvim: forge') + local forge_clis = { + { cmd = 'gh', name = 'GitHub', hint = 'gh auth login' }, + { cmd = 'glab', name = 'GitLab', hint = 'glab auth login' }, + { cmd = 'tea', name = 'Codeberg', hint = 'tea login add' }, + } + for _, cli in ipairs(forge_clis) do + if vim.fn.executable(cli.cmd) == 1 then + vim.health.ok(('%s found'):format(cli.cmd)) + else + vim.health.warn(('%s not found — run `%s`'):format(cli.cmd, cli.hint)) + end + end + local sync_paths = vim.fn.globpath(vim.o.runtimepath, 'lua/pending/sync/*.lua', false, true) if #sync_paths == 0 then vim.health.info('No sync backends found') diff --git a/spec/forge_spec.lua b/spec/forge_spec.lua index 3d17374..3e7a677 100644 --- a/spec/forge_spec.lua +++ b/spec/forge_spec.lua @@ -186,9 +186,9 @@ describe('forge', function() end) end) - describe('_api_url', function() - it('builds GitHub API URL', function() - local url = forge._api_url({ + describe('_api_args', function() + it('builds GitHub CLI args', function() + local args = forge._api_args({ forge = 'github', owner = 'user', repo = 'repo', @@ -196,11 +196,11 @@ describe('forge', function() number = 42, url = '', }) - assert.equals('https://api.github.com/repos/user/repo/issues/42', url) + assert.same({ 'gh', 'api', '/repos/user/repo/issues/42' }, args) end) - it('builds GitLab API URL for issue', function() - local url = forge._api_url({ + it('builds GitLab CLI args for issue', function() + local args = forge._api_args({ forge = 'gitlab', owner = 'group', repo = 'project', @@ -208,11 +208,11 @@ describe('forge', function() number = 15, url = '', }) - assert.equals('https://gitlab.com/api/v4/projects/group%2Fproject/issues/15', url) + assert.same({ 'glab', 'api', '/projects/group%2Fproject/issues/15' }, args) end) - it('builds GitLab API URL for merge request', function() - local url = forge._api_url({ + it('builds GitLab CLI args for merge request', function() + local args = forge._api_args({ forge = 'gitlab', owner = 'group', repo = 'project', @@ -220,11 +220,11 @@ describe('forge', function() number = 5, url = '', }) - assert.equals('https://gitlab.com/api/v4/projects/group%2Fproject/merge_requests/5', url) + assert.same({ 'glab', 'api', '/projects/group%2Fproject/merge_requests/5' }, args) end) - it('builds Codeberg API URL', function() - local url = forge._api_url({ + it('builds Codeberg CLI args', function() + local args = forge._api_args({ forge = 'codeberg', owner = 'user', repo = 'repo', @@ -232,7 +232,7 @@ describe('forge', function() number = 3, url = '', }) - assert.equals('https://codeberg.org/api/v1/repos/user/repo/issues/3', url) + assert.same({ 'tea', 'api', '/repos/user/repo/issues/3' }, args) end) end)