feat: browse GitHub GitLab and Gitea pull requests

This commit is contained in:
Valdemar Lorenzen
2026-10-14 13:22:09 +02:00
parent 15968a328c
commit af6496bd5a
11 changed files with 1349 additions and 47 deletions
+123 -5
View File
@@ -54,7 +54,7 @@ impl Drop for Temporary {
}
}
struct ProcessOutput {
pub(crate) struct ProcessOutput {
code: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
@@ -97,7 +97,7 @@ fn terminate(child: &mut std::process::Child) {
let _ = child.kill();
let _ = child.wait();
}
fn run(
pub(crate) fn run(
mut command: Command,
input: Vec<u8>,
cancelled: &AtomicBool,
@@ -200,7 +200,7 @@ fn run(
})
}
}
fn checked(output: ProcessOutput) -> Result<Vec<u8>, String> {
pub(crate) fn checked(output: ProcessOutput) -> Result<Vec<u8>, String> {
if output.code == Some(0) {
Ok(output.stdout)
} else {
@@ -1535,9 +1535,69 @@ impl Repository {
self.load_remote(remote, None)?;
self.catalog()
}
pub fn pull_requests(&mut self, name: &str) -> Result<Catalog, String> {
self.discover_remotes()?;
let remote = if name.is_empty() {
self.remotes
.iter()
.find(|remote| remote.name == "origin")
.or_else(|| (self.remotes.len() == 1).then(|| &self.remotes[0]))
.ok_or("Choose a remote with :pull_requests <remote>")?
} else {
self.remotes
.iter()
.find(|remote| remote.name == name)
.ok_or_else(|| format!("Unknown configured remote: {name}"))?
};
let mut config = git_command(&self.store);
config.args([
"config",
"--get",
&format!("remote.{}.vamp-forge", remote.name),
]);
let output = run(config, vec![], &self.cancelled)?;
let provider = if output.code == Some(1) {
None
} else {
Some(text(checked(output)?)?.trim().to_owned())
};
let pulls = crate::forge::list(
&remote.url,
provider.as_deref(),
&self.root,
&self.cancelled,
)
.map_err(|error| error.replace("remote.<name>", &format!("remote.{}", remote.name)))?;
Ok(Catalog {
kind: self.kind,
notice: Some(format!("Pull requests from {}", remote.name)),
entries: pulls
.into_iter()
.map(|pull| Revision {
target: match pull.revisions {
Ok(revisions) => RevisionTarget::PullRequest {
remote: remote.name.clone(),
base_ref: revisions.base_ref,
base_sha: revisions.base_sha,
head_ref: revisions.head_ref,
head_sha: revisions.head_sha,
},
Err(error) => RevisionTarget::UnavailablePullRequest(error),
},
label: format!("#{} {}", pull.number, pull.title),
description: pull.description,
category: if pull.closed {
Category::Closed
} else {
Category::Open
},
})
.collect(),
})
}
fn resolve(&mut self, target: &RevisionTarget) -> Result<(PathBuf, String), String> {
let id = match target {
RevisionTarget::Commit(id) => {
RevisionTarget::Commit(id) | RevisionTarget::RemoteCommit { id, .. } => {
if !matches!(id.len(), 40 | 64)
|| (self.kind == RepoKind::Jujutsu && id.len() != 40)
|| !id.bytes().all(|b| b.is_ascii_hexdigit())
@@ -1560,7 +1620,10 @@ impl Repository {
})?
.clone(),
RevisionTarget::Working => return Err("Working checkout is not a commit".into()),
RevisionTarget::FetchRemote(_) | RevisionTarget::ListRemote(_) => {
RevisionTarget::UnavailablePullRequest(error) => return Err(error.clone()),
RevisionTarget::FetchRemote(_)
| RevisionTarget::ListRemote(_)
| RevisionTarget::PullRequest { .. } => {
return Err("Select a revision, not a history loader".into());
}
};
@@ -1573,6 +1636,61 @@ impl Repository {
return Ok(resolved);
}
}
if let RevisionTarget::RemoteCommit {
remote, reference, ..
} = target
{
if !reference.starts_with("refs/") {
return Err("Invalid pull request reference".into());
}
self.git(&self.store, &["check-ref-format", reference])?;
self.discover_remotes()?;
let url = self
.remotes
.iter()
.find(|entry| &entry.name == remote)
.ok_or_else(|| format!("Unknown configured remote: {remote}"))?
.url
.clone();
self.ensure_private()?;
let store = &self.private.as_ref().unwrap().1;
// Closed PR refs can disappear; servers may still serve their immutable commits.
if self
.git(
store,
&[
"fetch",
"--no-tags",
"--no-write-fetch-head",
"--",
&url,
&format!("+{id}:refs/vamp-pull-request/selected"),
],
)
.is_err()
{
self.git(
store,
&[
"fetch",
"--no-tags",
"--no-write-fetch-head",
"--",
&url,
&format!("+{reference}:refs/vamp-pull-request/selected"),
],
)
.map_err(|error| {
format!("Pull request commit unavailable; reopen :pull_requests: {error}")
})?;
}
if let Some(resolved) = self.resolve_stored(&id)? {
return Ok(resolved);
}
return Err(
"Pull request changed or its commit is unavailable; reopen :pull_requests".into(),
);
}
Err(format!(
"Commit {id} is unavailable locally or in loaded remote history"
))