-- lua/gpr_gh.lua
-- Async PR opener using `vim.system` + GitHub CLI.
-- 1) blame current line -> SHA
-- 2) read commit subject
-- 3) extract PR number "(#1234)"
-- 4) `gh pr view --web`
local M = {}
-- Safe notify in fast-event contexts
local notify = vim.schedule_wrap(function(msg, level)
vim.notify(msg, level or vim.log.levels.INFO, { title = "GPR" })
end)
local function run(cmd, opts, cb)
opts = opts or {}
opts.text = true
vim.system(cmd, opts, function(obj)
if obj.code ~= 0 then
cb(nil, obj.stderr ~= "" and obj.stderr or obj.stdout)
else
cb(obj.stdout or "", nil)
end
end)
end
local function blame_sha_for_line(repo_cwd, cb)
local file = vim.api.nvim_buf_get_name(0)
if file == "" then return cb(nil, "Buffer has no filename") end
-- Use Lua dirname to avoid vim.fn in fast context
local file_dir = vim.fs.dirname(file) or repo_cwd
local lnum = vim.api.nvim_win_get_cursor(0)[1]
run({ "git", "blame", "-L", ("%d,%d"):format(lnum, lnum), "--porcelain", "--", file },
{ cwd = file_dir }, function(out, err)
if err then return cb(nil, "git blame failed: " .. err) end
local sha = out:match("^([0-9a-fA-F]+)")
if not sha then return cb(nil, "Could not parse a commit for this line") end
cb(sha, nil)
end)
end
local function commit_subject(repo_cwd, sha, cb)
run({ "git", "log", "-n", "1", "--format=%s", sha }, { cwd = repo_cwd }, function(out, err)
if err then return cb(nil, "git log failed: " .. err) end
cb(vim.trim(out), nil)
end)
end
local function extract_pr_number(subject)
return subject:match("%(#(%d+)%)") -- Title (#1234)
or subject:match("PR%s*#(%d+)") -- PR #1234
or subject:match("#(%d+)") -- #1234 anywhere
end
function M.open_pr_for_line()
if vim.fn.executable("gh") == 0 then
return notify("GitHub CLI (gh) not found", vim.log.levels.ERROR)
end
-- Capture CWD *before* async (pure Lua; safe in fast events)
local repo_cwd = vim.uv.cwd() -- alias: vim.loop.cwd()
blame_sha_for_line(repo_cwd, function(sha, e1)
if not sha then return notify(e1, vim.log.levels.WARN) end
commit_subject(repo_cwd, sha, function(subj, e2)
if not subj then return notify(e2, vim.log.levels.WARN) end
local pr = extract_pr_number(subj)
if not pr then
return notify("No PR number like (#1234) in: " .. subj, vim.log.levels.WARN)
end
-- Fire-and-forget: open PR in browser via gh
run({ "gh", "pr", "view", pr, "--web" }, { cwd = repo_cwd }, function(_, err)
if err and not err:match("^warning:") then
notify("Failed to open PR #" .. pr .. ": " .. err, vim.log.levels.WARN)
else
notify("Opening PR #" .. pr)
end
end)
end)
end)
end
return M