1use crate::api::models::{BuildState, BuildStatus, PullRequest, ReviewState, ReviewerState};
2use crate::commands::pr::Ctx;
3use crate::commands::pr_build;
4use crate::error::Result;
5use crate::output::{self, Format};
6use crate::users::{current_user, resolve_user};
7use serde::Serialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
10pub enum ReviewStateArg {
11 Approved,
12 ChangesRequested,
13 Pending,
14}
15
16impl ReviewStateArg {
17 fn as_state(self) -> ReviewState {
18 match self {
19 Self::Approved => ReviewState::Approved,
20 Self::ChangesRequested => ReviewState::ChangesRequested,
21 Self::Pending => ReviewState::Pending,
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
27pub enum BuildStateArg {
28 Successful,
29 Failed,
30 Inprogress,
31 Stopped,
32 None,
34}
35
36impl BuildStateArg {
37 fn as_state(self) -> BuildState {
38 match self {
39 Self::Successful => BuildState::Successful,
40 Self::Failed => BuildState::Failed,
41 Self::Inprogress => BuildState::InProgress,
42 Self::Stopped => BuildState::Stopped,
43 Self::None => BuildState::None,
44 }
45 }
46}
47
48#[derive(Debug)]
49pub struct ListArgs {
50 pub destination: Option<String>,
51 pub state: String,
52 pub reviewer: Option<String>,
53 pub author: Option<String>,
54 pub review_state: Option<ReviewStateArg>,
55 pub needs_my_review: bool,
56 pub build: bool,
58 pub build_status: Option<BuildStateArg>,
59}
60
61pub(crate) const REVIEWER_FIELDS: &str =
69 "%2Bvalues.reviewers,%2Bvalues.participants,%2Bvalues.draft,%2Bvalues.comment_count";
70
71const ALL_STATES: &str = "OPEN,MERGED,DECLINED,SUPERSEDED";
72
73#[derive(Debug, Serialize)]
74struct PrRow {
75 id: u64,
76 title: String,
77 state: String,
79 draft: bool,
80 #[serde(skip)]
84 display_state: String,
85 author: String,
86 source: String,
87 destination: String,
88 reviewers: Vec<ReviewerState>,
89 url: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
93 build_state: Option<BuildState>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 build: Option<Vec<BuildStatus>>,
96}
97
98fn to_row(pr: &PullRequest) -> PrRow {
99 PrRow {
100 id: pr.id,
101 title: pr.title.clone().unwrap_or_default(),
102 state: pr.state.clone().unwrap_or_else(|| "-".into()),
103 draft: pr.draft,
104 display_state: pr.display_state(),
105 author: pr.author_name().to_string(),
106 source: pr.source_branch().to_string(),
107 destination: pr.destination_branch().to_string(),
108 reviewers: pr.reviewer_states(),
109 url: pr.html_url().to_string(),
110 build_state: None,
111 build: None,
112 }
113}
114
115fn reviewer_cell(reviewers: &[ReviewerState]) -> String {
116 reviewers
117 .iter()
118 .map(|r| format!("{} {}", r.name, r.state.mark()))
119 .collect::<Vec<_>>()
120 .join(", ")
121}
122
123pub(crate) fn state_query(state: &str) -> String {
130 if state.eq_ignore_ascii_case("all") {
131 ALL_STATES.to_string()
132 } else if state.eq_ignore_ascii_case("draft") {
133 "OPEN".to_string()
134 } else {
135 state.to_uppercase()
136 }
137}
138
139async fn my_uuid(ctx: &Ctx) -> Result<Option<String>> {
142 Ok(current_user(&ctx.client).await?.uuid)
143}
144
145fn my_review_state(pr: &PullRequest, my_uuid: Option<&str>) -> Option<ReviewState> {
146 let me = my_uuid?;
147 pr.reviewer_states()
148 .into_iter()
149 .find(|r| r.uuid.as_deref() == Some(me))
150 .map(|r| r.state)
151}
152
153pub async fn list(ctx: &Ctx, args: ListArgs) -> Result<()> {
154 let reviewer_uuid = match args.reviewer.as_deref() {
157 Some(name) => resolve_user(&ctx.client, &ctx.slug, name, &[]).await?.uuid,
158 None => None,
159 };
160
161 let author_is_me = args.author.as_deref() == Some("@me");
165 let me = if author_is_me || args.needs_my_review || args.review_state.is_some() {
166 my_uuid(ctx).await?
167 } else {
168 None
169 };
170
171 let author_uuid = match args.author.as_deref() {
172 Some("@me") => me.clone(),
173 Some(name) => resolve_user(&ctx.client, &ctx.slug, name, &[]).await?.uuid,
174 None => None,
175 };
176
177 let want_draft = args.state.eq_ignore_ascii_case("draft");
178
179 let spinner = output::spinner("fetching pull requests");
180 let prs: Vec<PullRequest> = ctx
181 .client
182 .paginate(&ctx.path(&format!(
183 "/pullrequests?state={}&pagelen=50&fields={REVIEWER_FIELDS}",
184 urlencoding::encode(&state_query(&args.state))
185 )))
186 .await?;
187 spinner.finish_and_clear();
188
189 let kept: Vec<&PullRequest> = prs
190 .iter()
191 .filter(|pr| match args.destination.as_deref() {
192 Some(branch) => pr.destination_branch() == branch,
193 None => true,
194 })
195 .filter(|pr| !want_draft || pr.draft)
196 .filter(|pr| match reviewer_uuid.as_deref() {
197 Some(uuid) => pr
198 .reviewer_states()
199 .iter()
200 .any(|r| r.uuid.as_deref() == Some(uuid)),
201 None => true,
202 })
203 .filter(|pr| match author_uuid.as_deref() {
204 Some(uuid) => pr.author.as_ref().and_then(|a| a.uuid.as_deref()) == Some(uuid),
205 None => true,
206 })
207 .filter(|pr| match args.review_state {
208 Some(wanted) => my_review_state(pr, me.as_deref()) == Some(wanted.as_state()),
209 None => true,
210 })
211 .filter(|pr| {
212 if !args.needs_my_review {
213 return true;
214 }
215 matches!(
217 my_review_state(pr, me.as_deref()),
218 Some(ReviewState::ChangesRequested) | Some(ReviewState::Pending)
219 )
220 })
221 .collect();
222
223 let mut rows: Vec<PrRow> = kept.iter().map(|pr| to_row(pr)).collect();
224
225 let want_build = args.build || args.build_status.is_some();
230 if want_build {
231 let ids: Vec<u64> = rows.iter().map(|r| r.id).collect();
232 let spinner = output::spinner("fetching build statuses");
233 let mut statuses = pr_build::statuses_for(&ctx.client, &ctx.slug, &ids).await?;
234 spinner.finish_and_clear();
235 for row in &mut rows {
236 let found = statuses.remove(&row.id).unwrap_or_default();
237 row.build_state = Some(BuildState::rollup(&found));
238 row.build = Some(found);
239 }
240 if let Some(wanted) = args.build_status {
241 rows.retain(|r| r.build_state == Some(wanted.as_state()));
242 }
243 }
244
245 render(ctx, &rows, want_build)
246}
247
248fn build_cell(state: Option<BuildState>) -> String {
249 let state = state.unwrap_or(BuildState::None);
250 output::colored_cell(state.label(), output::tone_for(state))
251}
252
253fn render(ctx: &Ctx, rows: &[PrRow], build: bool) -> Result<()> {
254 match ctx.format {
255 Format::Json => output::print_json(&rows)?,
256 Format::Human => {
257 let mut headers: Vec<&str> = vec!["ID", "TITLE", "STATE"];
258 if build {
259 headers.push("BUILD");
260 }
261 headers.extend(["SOURCE", "→", "TARGET", "AUTHOR", "REVIEWERS"]);
262 output::print_table(
263 &headers,
264 rows.iter()
265 .map(|r| {
266 let mut cells =
267 vec![r.id.to_string(), r.title.clone(), r.display_state.clone()];
268 if build {
269 cells.push(build_cell(r.build_state));
270 }
271 cells.extend([
272 r.source.clone(),
273 "→".into(),
274 r.destination.clone(),
275 r.author.clone(),
276 reviewer_cell(&r.reviewers),
277 ]);
278 cells
279 })
280 .collect(),
281 )
282 }
283 }
284 Ok(())
285}