use crate::gh::{awaiting_approval, etag_of, gh_json, next_page, requires_signatures, tally, PAGE, SIGNATURE_RULES};
use anyhow::{Context, Result};
use serde_json::Value;
#[derive(Clone, Default)]
pub struct Listed {
pub number: u64,
pub title: String,
pub author: String,
pub base: String,
pub branch: String,
pub draft: bool,
pub state: String,
pub created: String,
pub updated: String,
pub additions: u64,
pub deletions: u64,
pub files: u64,
pub decision: String,
pub labels: Vec<(String, String)>,
pub checks: Option<(usize, usize, usize)>,
pub checks_head: String,
}
impl Listed {
pub fn open(&self) -> bool {
self.state == "OPEN" || self.state.is_empty()
}
}
pub const LOOKING: &str = "…";
const LISTED: &str = "number,title,author,baseRefName,headRefName,isDraft,state,createdAt,updatedAt,additions,deletions,changedFiles,reviewDecision,labels";
fn listed(p: &Value) -> Listed {
let s = |v: &Value| v.as_str().unwrap_or("").to_owned();
let n = |v: &Value| v.as_u64().unwrap_or(0);
Listed {
number: n(&p["number"]),
title: s(&p["title"]),
author: s(&p["author"]["login"]),
base: s(&p["baseRefName"]),
branch: s(&p["headRefName"]),
draft: p["isDraft"] == true,
state: s(&p["state"]),
created: s(&p["createdAt"]),
updated: s(&p["updatedAt"]),
additions: n(&p["additions"]),
deletions: n(&p["deletions"]),
files: n(&p["changedFiles"]),
decision: s(&p["reviewDecision"]),
labels: p["labels"].as_array().into_iter().flatten().map(|l| (s(&l["name"]), s(&l["color"]))).collect(),
checks: None,
checks_head: String::new(),
}
}
pub const LIST_MAX: usize = 1000;
pub fn list() -> Result<Vec<Listed>> {
let v: Value = gh_json(&["pr", "list", "--limit", &LIST_MAX.to_string(), "--json", LISTED])?;
Ok(v.as_array().context("gh pr list")?.iter().map(listed).collect())
}
pub fn listed_one(n: u64) -> Result<Listed> {
let v: Value = gh_json(&["pr", "view", &n.to_string(), "--json", LISTED])?;
Ok(listed(&v))
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Extra {
pub my_review: Option<(String, String)>,
pub reviews: Vec<(String, String)>,
pub said: Vec<(String, String)>,
pub requested: bool,
pub mergeable: String,
pub head_oid: String,
pub base: String,
pub unsigned: usize,
pub signatures_required: bool,
pub awaiting: usize,
}
pub fn list_extra() -> Result<(String, std::collections::HashMap<u64, Extra>)> {
let (mut me, mut all, mut after) = (String::new(), std::collections::HashMap::new(), None::<String>);
while all.len() < LIST_MAX {
let from = after.as_ref().map_or(String::new(), |c| format!(", after: \"{c}\""));
let q = format!(
"query($owner: String!, $name: String!) {{ viewer {{ login }} repository(owner: $owner, name: $name) {{
pullRequests(states: OPEN, first: 100{from}, orderBy: {{ field: UPDATED_AT, direction: DESC }}) {{ {PAGE} nodes {{
number headRefOid mergeable baseRefName {SIGNATURE_RULES}
commits(last: 100) {{ nodes {{ commit {{ signature {{ isValid }} }} }} }}
reviewRequests(first: 30) {{ nodes {{ requestedReviewer {{ ... on User {{ login }} }} }} }}
latestReviews(first: 50) {{ nodes {{ author {{ login }} state commit {{ oid }} }} }}
comments(last: 20) {{ nodes {{ author {{ login }} authorAssociation body }} }} }} }} }} }}"
);
let v: Value = gh_json(&["api", "graphql", "-F", "owner={owner}", "-F", "name={repo}", "-f", &format!("query={q}")])?;
me = v["data"]["viewer"]["login"].as_str().unwrap_or("").to_owned();
let list = &v["data"]["repository"]["pullRequests"];
let prs = list["nodes"].as_array().context("gh api graphql: pull requests")?;
all.extend(prs.iter().map(|p| (p["number"].as_u64().unwrap_or(0), extra(p, &me))));
match next_page(list) {
Some(c) => after = Some(c),
None => break,
}
}
if let Ok(w) = awaiting_approval(None) {
for e in all.values_mut() {
e.awaiting = w.get(&e.head_oid).map_or(0, Vec::len);
}
}
Ok((me, all))
}
fn extra(p: &Value, me: &str) -> Extra {
let s = |v: &Value| v.as_str().unwrap_or("").to_owned();
let nodes = |k: &str| p[k]["nodes"].as_array().cloned().unwrap_or_default();
let my_review = nodes("latestReviews").into_iter().find(|r| r["author"]["login"] == me).map(|r| (s(&r["state"]), s(&r["commit"]["oid"])));
Extra {
my_review,
reviews: nodes("latestReviews").iter().map(|r| (s(&r["author"]["login"]), s(&r["state"]))).collect(),
said: nodes("comments")
.iter()
.filter(|c| c["author"]["login"] == me || matches!(c["authorAssociation"].as_str(), Some("OWNER" | "MEMBER" | "COLLABORATOR")))
.map(|c| (s(&c["author"]["login"]), s(&c["body"])))
.collect(),
requested: nodes("reviewRequests").iter().any(|r| r["requestedReviewer"]["login"] == me),
mergeable: s(&p["mergeable"]),
head_oid: s(&p["headRefOid"]),
base: s(&p["baseRefName"]),
unsigned: nodes("commits").iter().filter(|c| c["commit"]["signature"]["isValid"] != true).count(),
signatures_required: requires_signatures(p),
awaiting: 0, }
}
pub type ListChecks = std::collections::HashMap<u64, ((usize, usize, usize), String)>;
pub fn list_checks() -> Result<ListChecks> {
let v: Value = gh_json(&["pr", "list", "--limit", &LIST_MAX.to_string(), "--json", "number,headRefOid,statusCheckRollup"])?;
let prs = v.as_array().context("gh pr list")?;
Ok(prs
.iter()
.map(|p| {
let head = p["headRefOid"].as_str().unwrap_or("").to_owned();
(p["number"].as_u64().unwrap_or(0), (tally(p["statusCheckRollup"].as_array().map(Vec::as_slice).unwrap_or(&[])), head))
})
.collect())
}
pub fn list_etag(etag: &str) -> Result<Option<String>> {
etag_of("repos/{owner}/{repo}/pulls?state=open&sort=updated&direction=desc&per_page=100", etag)
}