fix(gh): better way of determining current PR
Some checks are pending
CI / ci (push) Waiting to run

This commit is contained in:
Folke Lemaitre 2025-11-02 21:07:42 +01:00
parent 50436373c2
commit bd3c1a0714
No known key found for this signature in database
GPG key ID: 9B52594D560070AB
5 changed files with 86 additions and 88 deletions

View file

@ -237,7 +237,8 @@ M.actions.gh_yank = {
}
M.actions.gh_comment = {
desc = "Comment on {type} #{number}",
desc = "Add comment",
title = "Comment on {type} #{number}",
icon = "",
action = function(item, ctx)
local win = ctx.main or vim.api.nvim_get_current_win()

View file

@ -1,5 +1,6 @@
local Async = require("snacks.picker.util.async")
local Item = require("snacks.gh.item")
local Proc = require("snacks.util.spawn")
---@class snacks.gh.api
local M = {}
@ -148,6 +149,7 @@ end
---@param cb fun(proc: snacks.spawn.Proc, data?: string)
---@param opts snacks.gh.api.Cmd
function M.cmd(cb, opts)
opts = opts or {}
local args = vim.deepcopy(opts.args)
if opts.repo then
vim.list_extend(args, { "--repo", opts.repo })
@ -173,12 +175,14 @@ function M.cmd(cb, opts)
if err then
vim.schedule(function()
if not proc.aborted then
Snacks.debug.cmd({
header = "GH Error",
cmd = { "gh", unpack(args) },
footer = proc:err(),
level = vim.log.levels.ERROR,
})
if opts.notify ~= false then
Snacks.debug.cmd({
header = "GH Error",
cmd = { "gh", unpack(args) },
footer = proc:err(),
level = vim.log.levels.ERROR,
})
end
if opts.on_error then
opts.on_error(proc, proc:err())
end
@ -203,6 +207,7 @@ function M.fetch(cb, opts)
end, {
args = args,
repo = opts.repo,
notify = opts.notify,
})
end
M.fetch_sync = wrap_sync(M.fetch)
@ -306,7 +311,7 @@ function M.list(what, cb, opts)
end
---@param cb fun(item?: snacks.picker.gh.Item, updated?: boolean)
---@param item snacks.gh.api.View
---@param item snacks.gh.api.View|{number?: number}
---@param opts? { fields?: string[], force?: boolean }
function M.view(cb, item, opts)
opts = opts or {}
@ -458,77 +463,60 @@ function M.comments(item, cb)
})
end
function M.get_branch()
local branch = vim.fn.system({ "git", "branch", "--show-current" }):gsub("\n", "")
-- Get all config in one call
local git_config =
vim.fn.system({ "git", "config", "--get-regexp", ("^(branch\\.%s\\.|remote\\.)"):format(branch) }):gsub("\n$", "")
local cfg = {} ---@type table<string, string>
for _, line in ipairs(vim.split(git_config, "\n")) do
local key, value = line:match("^([^%s]+)%s+(.+)$")
if key then
cfg[key] = value
end
---@async
function M.current_pr()
local root = Snacks.git.get_root(vim.uv.cwd() or ".")
if not root then
return
end
---@type snacks.picker.gh.Item?
local pr
local branch = Proc.exec({ "git", "branch", "--show-current" })
-- Extract values
local remote = cfg[("branch.%s.remote"):format(branch)] or ""
local merge = cfg[("branch.%s.merge"):format(branch)] or ""
-- Get fork URL (either named remote or direct URL)
local url = (remote:match("^https://") or remote:match("^git@")) and remote
or cfg[("remote.%s.url"):format(remote)]
or remote
---@param u? string
---@return string?
local function parse(u)
return u and (u:match("github%.com[:/](.+/.+)%.git") or u:match("github%.com[:/](.+/.+)$")) or nil
end
-- Parse author from fork URL
local author = (parse(url) or ""):match("^([^/]+)/")
-- Parse repo from upstream or origin
local repo = parse(cfg["remote.upstream.url"]) or parse(cfg["remote.origin.url"])
-- Parse head from merge ref
local head = merge:match("^refs/heads/(.+)$") or branch
-- Get base branch (default branch from origin)
local base = vim.fn.system({ "git", "symbolic-ref", "refs/remotes/origin/HEAD" }):gsub("\n", "")
base = base:match("refs/remotes/origin/(.+)") or "main"
---@type snacks.gh.api.Branch
return {
url = url,
author = author,
repo = repo,
branch = branch,
head = head,
base = base,
}
end
---@param cb fun(item?: snacks.picker.gh.Item)
function M.current_pr(cb)
local branch = M.get_branch()
local key = string.format("%s:%s/%s->%s", branch.author or "", branch.repo or "", branch.head, branch.base)
local key = root .. "::" .. branch
if pr_cache[key] then
return cb(pr_cache[key])
return pr_cache[key]
end
return M.list("pr", function(items)
pr_cache[key] = items and items[1] or nil
cb(pr_cache[key])
end, {
author = branch.author,
head = branch.head,
base = branch.base,
repo = branch.repo,
limit = 1,
-- try with `pr view` first
local api_opts = get_opts("pr", "list")
pr = M.fetch_sync({
args = { "pr", "view" },
fields = api_opts.fields,
notify = false,
})
pr = pr and cache_set(Item.new(pr, api_opts)) or nil
if pr then
pr_cache[key] = pr
return pr
end
-- assume this is the main branch of a fork
local author, main = branch:match("^(.-)/(.+)$")
if not author or not main then
return
end
local repo = M.cmd_sync({
args = { "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner" },
notify = false,
})
if not repo then
return
end
repo = vim.trim(repo)
M.list("pr", function(items)
pr_cache[key] = items and items[1] or nil
pr = pr_cache[key]
end, {
author = author,
head = main,
base = main,
repo = repo,
limit = 1,
notify = false,
}):wait()
return pr
end
return M

View file

@ -21,6 +21,7 @@
---@field args string[]
---@field repo? string
---@field input? string
---@field notify? boolean
---@field on_error? fun(proc: snacks.spawn.Proc, err: string)
---@class snacks.gh.api.Api

View file

@ -70,18 +70,12 @@ end
---@type snacks.picker.finder
function M.get_actions(opts, ctx)
opts = opts or {}
local proc ---@type snacks.spawn.Proc?
if not opts.item and not opts.number then
proc = Api.current_pr(function(pr)
opts.item = pr
end)
end
---@async
return function(cb)
if proc then
proc:wait()
end
local item = opts.item
if not opts.item and not opts.number then
item = Api.current_pr()
end
if not item then
local required = { "type", "repo", "number" }
@ -98,7 +92,7 @@ function M.get_actions(opts, ctx)
return
end
item = Api.get({ type = opts.type or "pr", repo = opts.repo, number = opts.number })
proc = Api.view(function(it)
local proc = Api.view(function(it)
item = it
end, item)

View file

@ -124,6 +124,7 @@ function Proc:wait()
while not self.did_exit or self:running() do
self.async:suspend()
end
return self
end
function Proc:run()
@ -228,10 +229,10 @@ function Proc:on_exit()
close(self.stderr)
if self.opts.on_exit then
self.opts.on_exit(self, self.code ~= 0 or self.signal ~= 0 or self.aborted or false)
self.did_exit = true
if self.async then
self.async:resume()
end
end
self.did_exit = true
if self.async then
self.async:resume()
end
end)
end
@ -285,4 +286,17 @@ end
M.new = Proc.new
---@param cmd string[]
---@async
function M.exec(cmd)
return vim.trim(M.new({
cmd = cmd[1],
args = vim.list_slice(cmd, 2),
stdout_buffered = true,
stderr_buffered = true,
})
:wait()
:out())
end
return M