1use crate::api::models::{PullRequest, ReviewState, ReviewerState};
2use crate::commands::pr::Ctx;
3use crate::error::Result;
4use crate::output::{self, Format};
5use crate::users::{current_user, resolve_user};
6use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
9pub enum ReviewStateArg {
10 Approved,
11 ChangesRequested,
12 Pending,
13}
14
15impl ReviewStateArg {
16 fn as_state(self) -> ReviewState {
17 match self {
18 Self::Approved => ReviewState::Approved,
19 Self::ChangesRequested => ReviewState::ChangesRequested,
20 Self::Pending => ReviewState::Pending,
21 }
22 }
23}
24
25#[derive(Debug)]
26pub struct ListArgs {
27 pub destination: Option<String>,
28 pub state: String,
29 pub reviewer: Option<String>,
30 pub author: Option<String>,
31 pub review_state: Option<ReviewStateArg>,
32 pub needs_my_review: bool,
33}
34
35const REVIEWER_FIELDS: &str = "%2Bvalues.reviewers,%2Bvalues.participants,%2Bvalues.draft";
43
44const ALL_STATES: &str = "OPEN,MERGED,DECLINED,SUPERSEDED";
45
46#[derive(Debug, Serialize)]
47struct PrRow {
48 id: u64,
49 title: String,
50 state: String,
52 draft: bool,
53 #[serde(skip)]
57 display_state: String,
58 author: String,
59 source: String,
60 destination: String,
61 reviewers: Vec<ReviewerState>,
62 url: String,
63}
64
65fn to_row(pr: &PullRequest) -> PrRow {
66 PrRow {
67 id: pr.id,
68 title: pr.title.clone().unwrap_or_default(),
69 state: pr.state.clone().unwrap_or_else(|| "-".into()),
70 draft: pr.draft,
71 display_state: pr.display_state(),
72 author: pr.author_name().to_string(),
73 source: pr.source_branch().to_string(),
74 destination: pr.destination_branch().to_string(),
75 reviewers: pr.reviewer_states(),
76 url: pr.html_url().to_string(),
77 }
78}
79
80fn reviewer_cell(reviewers: &[ReviewerState]) -> String {
81 reviewers
82 .iter()
83 .map(|r| format!("{} {}", r.name, r.state.mark()))
84 .collect::<Vec<_>>()
85 .join(", ")
86}
87
88fn state_query(state: &str) -> String {
91 if state.eq_ignore_ascii_case("all") {
92 ALL_STATES.to_string()
93 } else if state.eq_ignore_ascii_case("draft") {
94 "OPEN".to_string()
95 } else {
96 state.to_uppercase()
97 }
98}
99
100async fn my_uuid(ctx: &Ctx) -> Result<Option<String>> {
103 Ok(current_user(&ctx.client).await?.uuid)
104}
105
106fn my_review_state(pr: &PullRequest, my_uuid: Option<&str>) -> Option<ReviewState> {
107 let me = my_uuid?;
108 pr.reviewer_states()
109 .into_iter()
110 .find(|r| r.uuid.as_deref() == Some(me))
111 .map(|r| r.state)
112}
113
114pub async fn list(ctx: &Ctx, args: ListArgs) -> Result<()> {
115 let reviewer_uuid = match args.reviewer.as_deref() {
118 Some(name) => resolve_user(&ctx.client, &ctx.slug, name, &[]).await?.uuid,
119 None => None,
120 };
121
122 let author_is_me = args.author.as_deref() == Some("@me");
126 let me = if author_is_me || args.needs_my_review || args.review_state.is_some() {
127 my_uuid(ctx).await?
128 } else {
129 None
130 };
131
132 let author_uuid = match args.author.as_deref() {
133 Some("@me") => me.clone(),
134 Some(name) => resolve_user(&ctx.client, &ctx.slug, name, &[]).await?.uuid,
135 None => None,
136 };
137
138 let want_draft = args.state.eq_ignore_ascii_case("draft");
139
140 let spinner = output::spinner("fetching pull requests");
141 let prs: Vec<PullRequest> = ctx
142 .client
143 .paginate(&ctx.path(&format!(
144 "/pullrequests?state={}&pagelen=50&fields={REVIEWER_FIELDS}",
145 urlencoding::encode(&state_query(&args.state))
146 )))
147 .await?;
148 spinner.finish_and_clear();
149
150 let rows: Vec<PrRow> = prs
151 .iter()
152 .filter(|pr| match args.destination.as_deref() {
153 Some(branch) => pr.destination_branch() == branch,
154 None => true,
155 })
156 .filter(|pr| !want_draft || pr.draft)
157 .filter(|pr| match reviewer_uuid.as_deref() {
158 Some(uuid) => pr
159 .reviewer_states()
160 .iter()
161 .any(|r| r.uuid.as_deref() == Some(uuid)),
162 None => true,
163 })
164 .filter(|pr| match author_uuid.as_deref() {
165 Some(uuid) => pr.author.as_ref().and_then(|a| a.uuid.as_deref()) == Some(uuid),
166 None => true,
167 })
168 .filter(|pr| match args.review_state {
169 Some(wanted) => my_review_state(pr, me.as_deref()) == Some(wanted.as_state()),
170 None => true,
171 })
172 .filter(|pr| {
173 if !args.needs_my_review {
174 return true;
175 }
176 matches!(
178 my_review_state(pr, me.as_deref()),
179 Some(ReviewState::ChangesRequested) | Some(ReviewState::Pending)
180 )
181 })
182 .map(to_row)
183 .collect();
184
185 render(ctx, &rows)
186}
187
188fn render(ctx: &Ctx, rows: &[PrRow]) -> Result<()> {
189 match ctx.format {
190 Format::Json => output::print_json(&rows)?,
191 Format::Human => output::print_table(
192 &[
193 "ID",
194 "TITLE",
195 "STATE",
196 "SOURCE",
197 "→",
198 "TARGET",
199 "AUTHOR",
200 "REVIEWERS",
201 ],
202 rows.iter()
203 .map(|r| {
204 vec![
205 r.id.to_string(),
206 r.title.clone(),
207 r.display_state.clone(),
208 r.source.clone(),
209 "→".into(),
210 r.destination.clone(),
211 r.author.clone(),
212 reviewer_cell(&r.reviewers),
213 ]
214 })
215 .collect(),
216 ),
217 }
218 Ok(())
219}