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}
48
49impl Run for List {
50 fn run(self) -> Result<()> {
51 match (self.format, self.all) {
52 (Some(format), _) => list_formatted(format),
53 (None, true) => crate::stack::print_all_stacks(&review_numbers()),
54 (None, false) => crate::stack::print_stack(&review_numbers()),
55 }
56 }
57}
58
59pub fn list_formatted(format: Format) -> Result<()> {
64 let current = git::current_branch()?;
65 let root = stack::stack_root(¤t)?;
66 let branches: Vec<String> = stack::branch_and_descendants(&root)?
67 .into_iter()
68 .skip(1) .collect();
70
71 if branches.is_empty() {
72 println!("no stacked branches");
73 return Ok(());
74 }
75
76 let review_provider = detect_review_provider().ok().map(|(_, client)| client);
77 let entries: Vec<(String, Option<ReviewRequest>)> = branches
78 .iter()
79 .map(|branch| {
80 let review = review_provider
81 .as_ref()
82 .and_then(|rp| owned_review_for_branch(&**rp, branch).ok().flatten());
83 (branch.clone(), review)
84 })
85 .collect();
86
87 println!("{}", summary(&entries, &root, format));
88 println!();
89 for (index, (branch, review)) in entries.iter().enumerate() {
90 let number = index + 1;
91 match (format, review) {
92 (Format::Markdown, Some(review)) => {
93 println!(
94 "{number}. [{}]({}) - {}",
95 labeled_with_size(review, branch),
96 review.url,
97 review.state
98 );
99 }
100 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
101 (Format::Plain, Some(review)) => {
103 println!(
104 "{number}. {} - {}",
105 labeled_with_size(review, branch),
106 review.state
107 );
108 println!(" {}", review.url);
109 }
110 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
111 }
112 }
113
114 Ok(())
115}
116
117fn labeled_with_size(review: &ReviewRequest, branch: &str) -> String {
121 let id = match branch_diff_size(branch) {
122 Some((added, deleted)) => format!("{}, +{added}/-{deleted}", review.id),
123 None => review.id.clone(),
124 };
125 label(&review.title, &id)
126}
127
128fn branch_diff_size(branch: &str) -> Option<(usize, usize)> {
131 let parent = stack::parent_of(branch).ok().flatten()?;
132 let (added, deleted) = git::diff_numstat(&parent, branch).ok()?;
133 (added > 0 || deleted > 0).then_some((added, deleted))
134}
135
136fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
139 let total = entries.len();
140 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
141 let base = match format {
142 Format::Markdown => format!("`{base}`"),
143 Format::Plain => base.to_owned(),
144 };
145
146 let mut summary = if reviews.is_empty() {
147 format!(
148 "{total} branch{}, base {base}",
149 if total == 1 { "" } else { "es" }
150 )
151 } else if reviews.len() == total {
152 format!(
153 "{total} PR{}, base {base}",
154 if total == 1 { "" } else { "s" }
155 )
156 } else {
157 format!(
158 "{total} branches ({} with reviews), base {base}",
159 reviews.len()
160 )
161 };
162
163 if !reviews.is_empty() {
164 let mut counts = Vec::new();
165 for (state, label) in [
166 (ReviewState::Open, "open"),
167 (ReviewState::Merged, "merged"),
168 (ReviewState::Closed, "closed"),
169 ] {
170 let count = reviews
171 .iter()
172 .filter(|review| review.state == state)
173 .count();
174 if count > 0 {
175 counts.push(format!("{count} {label}"));
176 }
177 }
178 if !counts.is_empty() {
179 summary.push_str(&format!(", {}", counts.join(" / ")));
180 }
181 }
182
183 summary
184}