Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

213 linesCodeBlameRaw
1//! `g1t status`: what needs you, across every repository you can see.
2
3use serde_json::{Value, json};
4
5use crate::client::Client;
6use crate::context::Ctx;
7use crate::error::Result;
8use crate::output::{Output, ago, s};
9
10fn 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
20fn 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.
30fn 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.
63fn 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
91fn 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
108pub 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}