feat(fetch_pr): Introdue the fetch_pr command that allows to pick open

Pull requests from github and import them locally
This commit is contained in:
NicolasGB
2026-02-24 18:18:59 +01:00
parent d6aa067854
commit bbba4051c8
3 changed files with 144 additions and 1 deletions
+92
View File
@@ -127,6 +127,9 @@ local split_module = require("jj.cmd.split")
--- @class jj.cmd.open_pr_opts
--- @field list_bookmarks? boolean Whether to select from all bookmarks instead of current revision
--- @class jj.cmd.fetch_pr_opts
--- @field limit? number Limit the number of PRs to select from
--- @type jj.cmd.opts
M.config = {
describe = {
@@ -1008,6 +1011,91 @@ function M.tag_push()
end
end
--- Opens a picker to localy fetch a PR from a github repository
--- @param opts? jj.cmd.fetch_pr_opts Options for fetching PRs
function M.fetch_pr(opts)
if not utils.ensure_jj() then
return
end
if not utils.has_executable("git") then
return
end
if not utils.is_colocated() then
utils.notify("Current repository is not colocated. Cannot fetch PR.", vim.log.levels.ERROR)
return
end
-- Create a new opts table with the default limit if not provided
opts = vim.tbl_deep_extend("force", {
limit = 100,
}, opts or {})
-- Get the prs from github
local prs = utils.list_github_prs(opts)
if not prs or #prs == 0 then
return
end
local needs_refresh = terminal.is_log_buffer_open()
vim.ui.select(prs, {
prompt = "Select PR to fetch: ",
format_item = function(pr)
return string.format("#%s %s %s", pr.number, pr.title, pr.author)
end,
}, function(choice)
if choice then
utils.notify("Pulling PR #" .. choice.number .. "...", vim.log.levels.INFO)
local pr = choice.number
local count = 1
local max_retries = 30
-- the function that actually tries to fetch recursively
local function try_fetch()
if count > max_retries then
utils.notify(
string.format("Failed to fetch PR #%s. Tried %d times.", pr, max_retries),
vim.log.levels.ERROR
)
return
end
local ref = string.format("pull/%s/head:pr-%s-%d", pr, pr, count)
local cmd = string.format("git fetch origin %s", ref)
runner.execute_command_async(
cmd,
function()
-- If we successfully pulled the PR, notify the user and refresh the log if it's open
runner.execute_command_async("jj git import", function()
utils.notify(
string.format("PR #%s fetched as pr-%s-%d.", pr, pr, count),
vim.log.levels.INFO
)
if needs_refresh then
M.log({})
end
end, "Failed to import git refs")
end,
"",
nil,
true,
function()
-- If we errored increment the counter by one and try and fetch it again
count = count + 1
try_fetch()
end
)
end
-- Try and pull it once
try_fetch()
end
end)
end
--- @param args string|string[] jj command arguments
function M.j(args)
if not utils.ensure_jj() then
@@ -1164,6 +1252,9 @@ function M.j(args)
end
end
end,
fetch_pr = function()
M.fetch_pr()
end,
}
if handlers[subcommand] then
@@ -1213,6 +1304,7 @@ function M.register_command()
"annotate_line",
"commit",
"tag",
"fetch_pr",
}
local matches = {}
for _, cmd in ipairs(subcommands) do
+5 -1
View File
@@ -51,7 +51,8 @@ end
--- @param error_prefix string|nil Optional error message prefix
--- @param input string|nil Optional input to pass to stdin
--- @param silent boolean|nil Optional to silent the notification
function M.execute_command_async(cmd, on_success, error_prefix, input, silent)
--- @param on_error function|nil Callback on error, receives ouptut as the parameter
function M.execute_command_async(cmd, on_success, error_prefix, input, silent, on_error)
local output_lines = {}
local job_id = vim.fn.jobstart({ "sh", "-c", cmd }, {
@@ -85,6 +86,9 @@ function M.execute_command_async(cmd, on_success, error_prefix, input, silent)
if not silent then
vim.notify(error_message, vim.log.levels.ERROR, { title = "JJ" })
end
if on_error then
on_error(output)
end
end
end,
})
+47
View File
@@ -695,4 +695,51 @@ function M.reload_changed_file_buffers()
vim.cmd.checktime()
end
--- Using the gh cli lists all open prs
--- @param opts {limit: integer|nil}
--- @return {number: integer, title: string, author: string}[]|nil
function M.list_github_prs(opts)
if not M.has_executable("gh") then
M.notify("Missing `gh` executable to list prs", vim.log.levels.ERROR)
return
end
-- Set the default limit
local limit = 100
if opts and opts.limit then
limit = opts.limit
end
-- Start with a hardcoded limit of 100
local cmd =
[[gh pr list -L %s --json number,title,author --jq '.[] | "#\(.number);;;\(.title);;;(@\(.author.login))"']]
cmd = string.format(cmd, limit)
-- Run the command to get the pr's
local output, success = runner.execute_command_sync(cmd, nil, "Failed to get prs")
if not success or not output then
return
end
local open_prs = {}
-- Split each line
local lines = vim.split(output, "\n", { trimempty = true })
for _, line in ipairs(lines) do
local parts = vim.split(line, ";;;", { trimempty = true })
if #parts == 3 then
local number = tonumber(parts[1]:match("#(%d+)"))
local title = parts[2]
local author = parts[3]
if number and title and author then
table.insert(open_prs, { number = number, title = title, author = author })
end
else
M.notify("Unexpected PR list format: " .. line, vim.log.levels.WARN)
end
end
return open_prs
end
return M