feat: browse GitHub GitLab and Gitea pull requests
This commit is contained in:
+123
-5
@@ -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"
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user