feat: show line counts in pull request picker
This commit is contained in:
+141
-13
@@ -42,6 +42,7 @@ struct Forge {
|
||||
provider: Provider,
|
||||
host: String,
|
||||
endpoint: String,
|
||||
repository: String,
|
||||
}
|
||||
|
||||
fn encode(text: &str) -> String {
|
||||
@@ -237,6 +238,7 @@ impl Forge {
|
||||
provider,
|
||||
host: authority,
|
||||
endpoint,
|
||||
repository: path.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -308,14 +310,18 @@ impl Forge {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn get(
|
||||
fn request(
|
||||
&self,
|
||||
url: &str,
|
||||
body: Option<&str>,
|
||||
token: Option<&str>,
|
||||
root: &Path,
|
||||
cancelled: &AtomicBool,
|
||||
) -> Result<Value, String> {
|
||||
let mut headers = String::from("Accept: application/json\n");
|
||||
if body.is_some() {
|
||||
headers.push_str("Content-Type: application/json\n");
|
||||
}
|
||||
if let Some(token) = token {
|
||||
let prefix = match self.provider {
|
||||
Provider::Github => "Authorization: Bearer",
|
||||
@@ -347,6 +353,9 @@ impl Forge {
|
||||
"--url",
|
||||
url,
|
||||
]);
|
||||
if let Some(body) = body {
|
||||
command.args(["--data-binary", body]);
|
||||
}
|
||||
let bytes = checked(run(command, headers.into_bytes(), cancelled)?).map_err(
|
||||
|_| "Forge API request failed; check curl, network access, and authentication",
|
||||
)?;
|
||||
@@ -374,7 +383,7 @@ impl Forge {
|
||||
&self,
|
||||
closed: bool,
|
||||
page: u32,
|
||||
mut get: impl FnMut(&str) -> Result<Value, String>,
|
||||
mut request: impl FnMut(&str, Option<&str>) -> Result<Value, String>,
|
||||
) -> Result<PullRequestPage, String> {
|
||||
if page == 0 {
|
||||
return Err("Forge page numbers start at 1".into());
|
||||
@@ -394,17 +403,26 @@ impl Forge {
|
||||
// Gitea's default sort is descending creation time; "oldest" reverses it.
|
||||
Provider::Gitea => "limit=50",
|
||||
};
|
||||
let values = get(&format!(
|
||||
"{}?state={state}&{query}&page={page}",
|
||||
self.endpoint
|
||||
))?;
|
||||
let values = request(
|
||||
&format!("{}?state={state}&{query}&page={page}", self.endpoint),
|
||||
None,
|
||||
)?;
|
||||
let values = values
|
||||
.as_array()
|
||||
.ok_or("Forge API returned a non-array pull request list")?;
|
||||
// Servers may cap page sizes. Only an empty page proves exhaustion.
|
||||
has_more |= !values.is_empty();
|
||||
let sizes = self.sizes_with(values, &mut request);
|
||||
for value in values {
|
||||
let entry = summary(self.provider, value)?;
|
||||
let id = number(
|
||||
value,
|
||||
if self.provider == Provider::Gitlab {
|
||||
"iid"
|
||||
} else {
|
||||
"number"
|
||||
},
|
||||
)?;
|
||||
let entry = summary(self.provider, value, sizes.get(&id).copied())?;
|
||||
if entry.closed != closed {
|
||||
return Err("Forge API returned a pull request in the wrong category".into());
|
||||
}
|
||||
@@ -422,6 +440,108 @@ impl Forge {
|
||||
Ok(PullRequestPage { entries, has_more })
|
||||
}
|
||||
|
||||
/// One statistics query per page, never one detail request per pull request.
|
||||
fn sizes_with(
|
||||
&self,
|
||||
values: &[Value],
|
||||
request: &mut impl FnMut(&str, Option<&str>) -> Result<Value, String>,
|
||||
) -> BTreeMap<u64, (u64, u64)> {
|
||||
if values.is_empty() {
|
||||
return BTreeMap::new();
|
||||
}
|
||||
if self.provider == Provider::Gitea {
|
||||
return values
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
Some((
|
||||
value.get("number")?.as_u64()?,
|
||||
(
|
||||
value.get("additions")?.as_u64()?,
|
||||
value.get("deletions")?.as_u64()?,
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let mut fields = String::new();
|
||||
for value in values {
|
||||
let Some(id) = value
|
||||
.get(if self.provider == Provider::Gitlab {
|
||||
"iid"
|
||||
} else {
|
||||
"number"
|
||||
})
|
||||
.and_then(Value::as_u64)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
use std::fmt::Write;
|
||||
match self.provider {
|
||||
Provider::Github => write!(
|
||||
fields,
|
||||
"p{id}:pullRequest(number:{id}){{additions deletions}}"
|
||||
)
|
||||
.unwrap(),
|
||||
Provider::Gitlab => write!(
|
||||
fields,
|
||||
"p{id}:mergeRequest(iid:\"{id}\"){{diffStatsSummary{{additions deletions}}}}"
|
||||
)
|
||||
.unwrap(),
|
||||
Provider::Gitea => unreachable!(),
|
||||
}
|
||||
}
|
||||
let (url, query) = if self.provider == Provider::Github {
|
||||
let (owner, name) = self.repository.split_once('/').unwrap();
|
||||
let api = self.endpoint.split_once("/repos/").unwrap().0;
|
||||
(
|
||||
format!("{}/graphql", api.strip_suffix("/v3").unwrap_or(api)),
|
||||
format!(
|
||||
"query{{repository(owner:{},name:{}){{{fields}}}}}",
|
||||
serde_json::to_string(owner).unwrap(),
|
||||
serde_json::to_string(name).unwrap()
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("https://{}/api/graphql", self.host),
|
||||
format!(
|
||||
"query{{project(fullPath:{}){{{fields}}}}}",
|
||||
serde_json::to_string(&self.repository).unwrap()
|
||||
),
|
||||
)
|
||||
};
|
||||
let body = serde_json::json!({ "query": query }).to_string();
|
||||
// Missing/denied statistics must not prevent browsing or imply a zero-size PR.
|
||||
let Ok(response) = request(&url, Some(&body)) else {
|
||||
return BTreeMap::new();
|
||||
};
|
||||
let container = if self.provider == Provider::Github {
|
||||
"/data/repository"
|
||||
} else {
|
||||
"/data/project"
|
||||
};
|
||||
let Some(entries) = response.pointer(container).and_then(Value::as_object) else {
|
||||
return BTreeMap::new();
|
||||
};
|
||||
entries
|
||||
.iter()
|
||||
.filter_map(|(alias, value)| {
|
||||
let value = if self.provider == Provider::Gitlab {
|
||||
value.get("diffStatsSummary")?
|
||||
} else {
|
||||
value
|
||||
};
|
||||
Some((
|
||||
alias.strip_prefix('p')?.parse().ok()?,
|
||||
(
|
||||
value.get("additions")?.as_u64()?,
|
||||
value.get("deletions")?.as_u64()?,
|
||||
),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_with(
|
||||
&self,
|
||||
id: u64,
|
||||
@@ -567,7 +687,11 @@ fn created_at(value: &Value) -> Result<String, String> {
|
||||
))
|
||||
}
|
||||
|
||||
fn summary(provider: Provider, value: &Value) -> Result<PullRequestSummary, String> {
|
||||
fn summary(
|
||||
provider: Provider,
|
||||
value: &Value,
|
||||
size: Option<(u64, u64)>,
|
||||
) -> Result<PullRequestSummary, String> {
|
||||
let gitlab = provider == Provider::Gitlab;
|
||||
let (closed, merged) = status(provider, value)?;
|
||||
let author = author(provider, value)?;
|
||||
@@ -578,6 +702,10 @@ fn summary(provider: Provider, value: &Value) -> Result<PullRequestSummary, Stri
|
||||
} else {
|
||||
"open"
|
||||
};
|
||||
let size = size.map_or_else(
|
||||
|| "size unavailable".into(),
|
||||
|(added, deleted)| format!("+{added} -{deleted}"),
|
||||
);
|
||||
let (base, head) = if gitlab {
|
||||
(
|
||||
value.get("target_branch").and_then(Value::as_str),
|
||||
@@ -601,8 +729,8 @@ fn summary(provider: Provider, value: &Value) -> Result<PullRequestSummary, Stri
|
||||
number: number(value, if gitlab { "iid" } else { "number" })?,
|
||||
title: field(value, "title")?.to_owned(),
|
||||
description: match (base, head) {
|
||||
(Some(base), Some(head)) => format!("{author} · {state} · {head} → {base}"),
|
||||
_ => format!("{author} · {state}"),
|
||||
(Some(base), Some(head)) => format!("{author} · {state} · {size} · {head} → {base}"),
|
||||
_ => format!("{author} · {state} · {size}"),
|
||||
},
|
||||
closed,
|
||||
created_at: created_at(value)?,
|
||||
@@ -788,8 +916,8 @@ pub(crate) fn page(
|
||||
ssh_hostname(command, destination, port, cancelled)
|
||||
})?;
|
||||
let token = forge.token(root, cancelled)?;
|
||||
forge.page_with(closed, page, |url| {
|
||||
forge.get(url, token.as_deref(), root, cancelled)
|
||||
forge.page_with(closed, page, |url, body| {
|
||||
forge.request(url, body, token.as_deref(), root, cancelled)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -807,7 +935,7 @@ pub(crate) fn resolve(
|
||||
})?;
|
||||
let token = forge.token(root, cancelled)?;
|
||||
forge.resolve_with(number, |url| {
|
||||
forge.get(url, token.as_deref(), root, cancelled)
|
||||
forge.request(url, None, token.as_deref(), root, cancelled)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user