1use std::collections::BTreeMap;
2
3use anyhow::Result;
4use clap::ValueEnum;
5
6use crate::commands::Run;
7use crate::providers::{
8 ReviewRequest, ReviewState, detect_review_provider, label, owned_review_for_branch,
9};
10use crate::{git, stack};
11
12fn review_numbers() -> BTreeMap<String, String> {
16 let Some((_, provider)) = detect_review_provider().ok() else {
17 return BTreeMap::new();
18 };
19 let Ok(reviews) = provider.open_reviews() else {
20 return BTreeMap::new();
21 };
22 reviews
23 .into_iter()
24 .map(|review| (review.branch, review.id))
25 .collect()
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
30pub enum Format {
31 Markdown,
33 Plain,
36}
37
38#[derive(Debug, clap::Args)]
40pub struct List {
41 #[arg(long, value_enum)]
43 format: Option<Format>,
44 #[arg(long, conflicts_with = "format")]
46 all: bool,
47 #[arg(long, conflicts_with = "format")]
49 commits: bool,
50}
51
52impl Run for List {
53 fn run(self) -> Result<()> {
54 match (self.format, self.all) {
55 (Some(format), _) => list_formatted(format),
56 (None, true) => crate::stack::print_all_stacks(&review_numbers(), self.commits),
57 (None, false) => crate::stack::print_stack(&review_numbers(), self.commits),
58 }
59 }
60}
61
62pub fn list_formatted(format: Format) -> Result<()> {
67 let current = git::current_branch()?;
68 let root = stack::stack_root(¤t)?;
69 let branches: Vec<String> = stack::current_stack_branches(¤t)?
73 .into_iter()
74 .filter(|branch| *branch != root)
75 .collect();
76
77 if branches.is_empty() {
78 println!("no stacked branches");
79 return Ok(());
80 }
81
82 let review_provider = detect_review_provider().ok().map(|(_, client)| client);
83 let entries: Vec<(String, Option<ReviewRequest>)> = branches
84 .iter()
85 .map(|branch| {
86 let review = review_provider
87 .as_ref()
88 .and_then(|rp| owned_review_for_branch(&**rp, branch).ok().flatten());
89 (branch.clone(), review)
90 })
91 .collect();
92
93 println!("{}", summary(&entries, &root, format));
94 println!();
95 for (index, (branch, review)) in entries.iter().enumerate() {
96 let number = index + 1;
97 match (format, review) {
98 (Format::Markdown, Some(review)) => {
99 println!(
100 "{number}. [{}]({}) - {}",
101 labeled_with_size(review, branch),
102 review.url,
103 review.state
104 );
105 }
106 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
107 (Format::Plain, Some(review)) => {
109 println!(
110 "{number}. {} - {}",
111 labeled_with_size(review, branch),
112 review.state
113 );
114 println!(" {}", review.url);
115 }
116 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
117 }
118 }
119
120 Ok(())
121}
122
123fn labeled_with_size(review: &ReviewRequest, branch: &str) -> String {
127 let id = match branch_diff_size(branch) {
128 Some((added, deleted)) => format!("{}, +{added}/-{deleted}", review.id),
129 None => review.id.clone(),
130 };
131 label(&review.title, &id)
132}
133
134fn branch_diff_size(branch: &str) -> Option<(usize, usize)> {
137 let parent = stack::parent_of(branch).ok().flatten()?;
138 let (added, deleted) = git::diff_numstat(&parent, branch).ok()?;
139 (added > 0 || deleted > 0).then_some((added, deleted))
140}
141
142fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
145 let total = entries.len();
146 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
147 let base = match format {
148 Format::Markdown => format!("`{base}`"),
149 Format::Plain => base.to_owned(),
150 };
151
152 let mut summary = if reviews.is_empty() {
153 format!(
154 "{total} branch{}, base {base}",
155 if total == 1 { "" } else { "es" }
156 )
157 } else if reviews.len() == total {
158 format!(
159 "{total} PR{}, base {base}",
160 if total == 1 { "" } else { "s" }
161 )
162 } else {
163 format!(
164 "{total} branches ({} with reviews), base {base}",
165 reviews.len()
166 )
167 };
168
169 if !reviews.is_empty() {
170 let mut counts = Vec::new();
171 for (state, label) in [
172 (ReviewState::Open, "open"),
173 (ReviewState::Merged, "merged"),
174 (ReviewState::Closed, "closed"),
175 ] {
176 let count = reviews
177 .iter()
178 .filter(|review| review.state == state)
179 .count();
180 if count > 0 {
181 counts.push(format!("{count} {label}"));
182 }
183 }
184 if !counts.is_empty() {
185 summary.push_str(&format!(", {}", counts.join(" / ")));
186 }
187 }
188
189 summary
190}