From 597f98d65bbc86e671c3964add3c8be8613bb64d Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 15 Feb 2026 13:40:36 -0500 Subject: [PATCH] feat(nvim): support glab, gh in forge picker --- config/nvim/lua/plugins/git.lua | 146 +++++++++++++++++++++++++++----- home/modules/shell.nix | 1 + 2 files changed, 124 insertions(+), 23 deletions(-) diff --git a/config/nvim/lua/plugins/git.lua b/config/nvim/lua/plugins/git.lua index 9ead7a6..e62607e 100644 --- a/config/nvim/lua/plugins/git.lua +++ b/config/nvim/lua/plugins/git.lua @@ -1,5 +1,5 @@ ---@return string -local function gh_file_loc() +local function file_loc() local root = vim.trim(vim.fn.system('git rev-parse --show-toplevel')) local file = vim.api.nvim_buf_get_name(0):sub(#root + 2) local mode = vim.fn.mode() @@ -18,7 +18,7 @@ local function gh_file_loc() end ---@param args string[] -local function gh_yank(args) +local function yank_url(args) vim.system(args, { text = true }, function(result) if result.code == 0 then local url = vim.trim(result.stdout or '') @@ -31,28 +31,128 @@ local function gh_yank(args) end) end +local function remote_web_url() + local url = vim.trim(vim.fn.system('git remote get-url origin')) + url = url:gsub('%.git$', '') + url = url:gsub('^ssh://git@', 'https://') + url = url:gsub('^git@([^:]+):', 'https://%1/') + return url +end + +local function gitlab_file_url(loc, ref) + local base = remote_web_url() + local file, lines = loc:match('^(.+):(.+)$') + return ('%s/-/blob/%s/%s#L%s'):format(base, ref, file, lines) +end + +local function detect_forge() + local url = vim.trim(vim.fn.system('git remote get-url origin')) + if vim.v.shell_error ~= 0 then + return nil + end + if url:find('github') and vim.fn.executable('gh') == 1 then + return 'github' + end + if url:find('gitlab') and vim.fn.executable('glab') == 1 then + return 'gitlab' + end + return nil +end + +local forges = { + github = { + kinds = { issue = 'issue', pr = 'pr' }, + labels = { issue = 'Issues', pr = 'PRs' }, + list_cmd = function(kind, state) + return ('gh %s list --limit 100 --state %s'):format(kind, state) + end, + view_cmd = function(kind, num) + return { 'gh', kind, 'view', num, '--web' } + end, + browse = function(loc, branch) + vim.system({ 'gh', 'browse', loc, '--branch', branch }) + end, + browse_root = function() + vim.system({ 'gh', 'browse' }) + end, + yank_branch = function(loc) + yank_url({ 'gh', 'browse', loc, '-n' }) + end, + yank_commit = function(loc) + yank_url({ 'gh', 'browse', loc, '--commit=last', '-n' }) + end, + }, + gitlab = { + kinds = { issue = 'issue', pr = 'mr' }, + labels = { issue = 'Issues', pr = 'MRs' }, + list_cmd = function(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, + view_cmd = function(kind, num) + return { 'glab', kind, 'view', num, '--web' } + end, + browse = function(loc, branch) + vim.ui.open(gitlab_file_url(loc, branch)) + end, + browse_root = function() + vim.system({ 'glab', 'repo', 'view', '--web' }) + end, + yank_branch = function(loc) + local branch = vim.trim(vim.fn.system('git branch --show-current')) + vim.fn.setreg('+', gitlab_file_url(loc, branch)) + end, + yank_commit = function(loc) + local commit = vim.trim(vim.fn.system('git rev-parse HEAD')) + vim.fn.setreg('+', gitlab_file_url(loc, commit)) + end, + }, +} + ---@param kind 'issue'|'pr' ---@param state 'all'|'open'|'closed' -local function gh_picker(kind, state) +local function forge_picker(kind, state) + local forge_name = detect_forge() + if not forge_name then + vim.notify('No supported forge detected', vim.log.levels.WARN) + return + end + local forge = forges[forge_name] + local cli_kind = forge.kinds[kind] local next_state = ({ all = 'open', open = 'closed', closed = 'all' })[state] - local label = kind == 'pr' and 'PRs' or 'Issues' - require('fzf-lua').fzf_exec(('gh %s list --limit 100 --state %s'):format(kind, state), { - prompt = ('%s (%s)> '):format(label, state), + require('fzf-lua').fzf_exec(forge.list_cmd(cli_kind, state), { + prompt = ('%s (%s)> '):format(forge.labels[kind], state), header = ':: to toggle all/open/closed', actions = { ['default'] = function(selected) - local num = selected[1]:match('^(%d+)') + local num = selected[1]:match('^[#!]?(%d+)') if num then - vim.system({ 'gh', kind, 'view', num, '--web' }) + vim.system(forge.view_cmd(cli_kind, num)) end end, ['ctrl-o'] = function() - gh_picker(kind, next_state) + forge_picker(kind, next_state) end, }, }) end +local function with_forge(fn) + return function() + local forge_name = detect_forge() + if not forge_name then + vim.notify('No supported forge detected', vim.log.levels.WARN) + return + end + fn(forges[forge_name]) + end +end + return { { 'tpope/vim-fugitive', @@ -84,42 +184,42 @@ return { keys = { { 'go', - function() + with_forge(function(forge) local branch = vim.trim(vim.fn.system('git branch --show-current')) - vim.system({ 'gh', 'browse', gh_file_loc(), '--branch', branch }) - end, + forge.browse(file_loc(), branch) + end), mode = { 'n', 'v' }, }, { 'gy', - function() - gh_yank({ 'gh', 'browse', gh_file_loc(), '--commit=last', '-n' }) - end, + with_forge(function(forge) + forge.yank_commit(file_loc()) + end), mode = { 'n', 'v' }, }, { 'gl', - function() - gh_yank({ 'gh', 'browse', gh_file_loc(), '-n' }) - end, + with_forge(function(forge) + forge.yank_branch(file_loc()) + end), mode = { 'n', 'v' }, }, { 'gx', - function() - vim.system({ 'gh', 'browse' }) - end, + with_forge(function(forge) + forge.browse_root() + end), }, { 'gi', function() - gh_picker('issue', 'all') + forge_picker('issue', 'all') end, }, { 'gp', function() - gh_picker('pr', 'all') + forge_picker('pr', 'all') end, }, }, diff --git a/home/modules/shell.nix b/home/modules/shell.nix index ce9971a..dcbeee5 100644 --- a/home/modules/shell.nix +++ b/home/modules/shell.nix @@ -41,6 +41,7 @@ in poppler-utils librsvg imagemagick + graphite-cli ] ++ lib.optionals hostConfig.isLinux [ xclip ] ++ lib.optionals rust [ rustup ];