| 1 | //! `g1t status`: what needs you, across every repository you can see. |
| 2 | |
| 3 | use serde_json::{Value, json}; |
| 4 | |
| 5 | use crate::client::Client; |
| 6 | use crate::context::Ctx; |
| 7 | use crate::error::Result; |
| 8 | use crate::output::{Output, ago, s}; |
| 9 | |
| 10 | fn search_pulls(client: &Client, me: &str) -> Result<Vec<Value>> { |
| 11 | let found = client |
| 12 | .op("search") |
| 13 | .query("q", format!("is:pr is:open author:{me}")) |
| 14 | .query("type", "pulls") |
| 15 | .query("per_page", 20) |
| 16 | .send()?; |
| 17 | Ok(super::items(found, "hits")) |
| 18 | } |
| 19 | |
| 20 | fn notifications(client: &Client, reason: &str) -> Result<Vec<Value>> { |
| 21 | let found = client |
| 22 | .op("list_notifications") |
| 23 | .query("reason", reason) |
| 24 | .query("per_page", 20) |
| 25 | .send()?; |
| 26 | Ok(super::items(found, "items")) |
| 27 | } |
| 28 | |
| 29 | /// Each pull request's checks, looked up side by side. |
| 30 | fn with_checks(client: &Client, pulls: Vec<Value>) -> Vec<Value> { |
| 31 | std::thread::scope(|scope| { |
| 32 | let handles: Vec<_> = pulls |
| 33 | .into_iter() |
| 34 | .map(|hit| { |
| 35 | scope.spawn(move || { |
| 36 | let repo = hit["repo"] |
| 37 | .as_str() |
| 38 | .and_then(|r| crate::repo::Repo::parse(r, &client.host.name).ok()); |
| 39 | let number = hit["number"].as_u64(); |
| 40 | let mut hit = hit; |
| 41 | if let (Some(repo), Some(n)) = (repo, number) |
| 42 | && let Ok(found) = client |
| 43 | .op("get_pull_request") |
| 44 | .repo(&repo) |
| 45 | .arg("number", n) |
| 46 | .send() |
| 47 | { |
| 48 | let pull = found.get("pull").cloned().unwrap_or(found); |
| 49 | hit["check_status"] = pull["check_status"].clone(); |
| 50 | hit["status"] = pull["status"].clone(); |
| 51 | hit["head_commit"] = pull["head_commit"].clone(); |
| 52 | } |
| 53 | hit |
| 54 | }) |
| 55 | }) |
| 56 | .collect(); |
| 57 | handles.into_iter().filter_map(|h| h.join().ok()).collect() |
| 58 | }) |
| 59 | } |
| 60 | |
| 61 | /// Your active sessions and claims in the current repository, when there |
| 62 | /// is one: none outside a repository, or from a server without them. |
| 63 | fn here(ctx: &Ctx, me: &str) -> (Option<String>, Vec<Value>, Vec<Value>) { |
| 64 | let Ok((repo, client)) = ctx.repo_client() else { |
| 65 | return (None, Vec::new(), Vec::new()); |
| 66 | }; |
| 67 | if !client.has_token() { |
| 68 | return (None, Vec::new(), Vec::new()); |
| 69 | } |
| 70 | let client = client.with_timeouts(std::time::Duration::from_secs(5), super::QUICK); |
| 71 | let fetch = |op: &str, key: &str| -> Vec<Value> { |
| 72 | client |
| 73 | .op(op) |
| 74 | .repo(&repo) |
| 75 | .query("state", "active") |
| 76 | .send() |
| 77 | .map(|found| super::items(found, key)) |
| 78 | .unwrap_or_default() |
| 79 | .into_iter() |
| 80 | .filter(|item| super::login_of(item).eq_ignore_ascii_case(me)) |
| 81 | .collect() |
| 82 | }; |
| 83 | let (sessions, claims) = std::thread::scope(|scope| { |
| 84 | let sessions = scope.spawn(|| fetch("list_sessions", "sessions")); |
| 85 | let claims = scope.spawn(|| fetch("list_claims", "claims")); |
| 86 | (sessions.join().unwrap_or_default(), claims.join().unwrap_or_default()) |
| 87 | }); |
| 88 | (Some(repo.to_string()), sessions, claims) |
| 89 | } |
| 90 | |
| 91 | fn section( |
| 92 | out: &Output, |
| 93 | title: &str, |
| 94 | items: &[Value], |
| 95 | empty: &str, |
| 96 | line: impl Fn(&Value) -> String, |
| 97 | ) { |
| 98 | println!("{}", out.bold(title)); |
| 99 | if items.is_empty() { |
| 100 | println!(" {}", out.dim(empty)); |
| 101 | } |
| 102 | for item in items { |
| 103 | println!(" {}", line(item)); |
| 104 | } |
| 105 | println!(); |
| 106 | } |
| 107 | |
| 108 | pub fn run(ctx: &Ctx) -> Result<()> { |
| 109 | let client = ctx.signed_in_client()?; |
| 110 | let me = ctx.me(&client)?; |
| 111 | let (pulls, reviews, assigned, ci, here) = std::thread::scope(|scope| { |
| 112 | let pulls = scope.spawn(|| search_pulls(&client, &me).map(|p| with_checks(&client, p))); |
| 113 | let reviews = scope.spawn(|| notifications(&client, "review_requested")); |
| 114 | let assigned = scope.spawn(|| notifications(&client, "assign")); |
| 115 | let ci = scope.spawn(|| notifications(&client, "ci_activity")); |
| 116 | let here = scope.spawn(|| here(ctx, &me)); |
| 117 | (pulls.join(), reviews.join(), assigned.join(), ci.join(), here.join()) |
| 118 | }); |
| 119 | let (repo, sessions, claims) = here.unwrap_or((None, Vec::new(), Vec::new())); |
| 120 | let unwrap = |r: std::thread::Result<Result<Vec<Value>>>| -> Result<Vec<Value>> { |
| 121 | r.unwrap_or_else(|_| Err(crate::error::Error::failed("A lookup failed."))) |
| 122 | }; |
| 123 | let pulls = unwrap(pulls)?; |
| 124 | let reviews = unwrap(reviews)?; |
| 125 | let assigned = unwrap(assigned)?; |
| 126 | let ci: Vec<Value> = unwrap(ci)? |
| 127 | .into_iter() |
| 128 | .filter(|n| n["severity"] == "error") |
| 129 | .collect(); |
| 130 | let failing: Vec<Value> = pulls |
| 131 | .iter() |
| 132 | .filter(|p| { |
| 133 | matches!( |
| 134 | p["check_status"].as_str(), |
| 135 | Some("failed" | "failure" | "error") |
| 136 | ) |
| 137 | }) |
| 138 | .cloned() |
| 139 | .collect(); |
| 140 | if ctx.out.structured() { |
| 141 | return ctx.out.emit(&json!({ |
| 142 | "user": me, |
| 143 | "pulls": pulls, |
| 144 | "review_requests": reviews, |
| 145 | "assigned": assigned, |
| 146 | "failing_checks": failing, |
| 147 | "ci_failures": ci, |
| 148 | "repo": repo, |
| 149 | "sessions": sessions, |
| 150 | "claims": claims, |
| 151 | })); |
| 152 | } |
| 153 | let out = &ctx.out; |
| 154 | let pull_line = |p: &Value| { |
| 155 | let checks = p["check_status"] |
| 156 | .as_str() |
| 157 | .map(|c| format!(" {}", out.state(c))) |
| 158 | .unwrap_or_default(); |
| 159 | format!( |
| 160 | "{} {}{checks}", |
| 161 | out.cyan(&format!("{}#{}", s(p, "repo"), p["number"])), |
| 162 | s(p, "title") |
| 163 | ) |
| 164 | }; |
| 165 | let note_line = |n: &Value| { |
| 166 | let place = match n["number"].as_u64() { |
| 167 | Some(num) => format!("{}#{num}", s(n, "repo")), |
| 168 | None => s(n, "repo").to_owned(), |
| 169 | }; |
| 170 | format!( |
| 171 | "{} {} {}", |
| 172 | out.cyan(&place), |
| 173 | s(n, "title"), |
| 174 | out.dim(&ago(s(n, "updated_at"))) |
| 175 | ) |
| 176 | }; |
| 177 | section(out, "Your open pull requests", &pulls, "None.", pull_line); |
| 178 | section(out, "Reviews asked of you", &reviews, "None.", note_line); |
| 179 | section(out, "Assigned to you", &assigned, "Nothing.", note_line); |
| 180 | let mut broken: Vec<String> = failing.iter().map(pull_line).collect(); |
| 181 | broken.extend(ci.iter().map(note_line)); |
| 182 | println!("{}", out.bold("Failing checks")); |
| 183 | if broken.is_empty() { |
| 184 | println!(" {}", out.dim("None.")); |
| 185 | } |
| 186 | for line in broken { |
| 187 | println!(" {line}"); |
| 188 | } |
| 189 | if let Some(repo) = repo { |
| 190 | println!(); |
| 191 | section(out, &format!("Your sessions in {repo}"), &sessions, "None.", |session| { |
| 192 | let on = [ |
| 193 | session["issue"].as_u64().map(|n| format!("#{n}")), |
| 194 | session["branch"].as_str().map(str::to_owned), |
| 195 | ] |
| 196 | .into_iter() |
| 197 | .flatten() |
| 198 | .collect::<Vec<_>>() |
| 199 | .join(" "); |
| 200 | format!("{} {} {} {}", s(session, "id"), out.state(s(session, "state")), s(session, "harness"), on) |
| 201 | .trim_end() |
| 202 | .to_owned() |
| 203 | }); |
| 204 | println!("{}", out.bold(&format!("Your claims in {repo}"))); |
| 205 | if claims.is_empty() { |
| 206 | println!(" {}", out.dim("None.")); |
| 207 | } |
| 208 | for claim in &claims { |
| 209 | println!(" {}", super::work::claim_line(out, claim)); |
| 210 | } |
| 211 | } |
| 212 | Ok(()) |
| 213 | } |