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, 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 review.label(),
96 review.url,
97 review.state
98 );
99 }
100 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
101 (Format::Plain, Some(review)) => {
103 println!("{number}. {} - {}", review.label(), review.state);
104 println!(" {}", review.url);
105 }
106 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
107 }
108 }
109
110 Ok(())
111}
112
113fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
116 let total = entries.len();
117 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
118 let base = match format {
119 Format::Markdown => format!("`{base}`"),
120 Format::Plain => base.to_owned(),
121 };
122
123 let mut summary = if reviews.is_empty() {
124 format!(
125 "{total} branch{}, base {base}",
126 if total == 1 { "" } else { "es" }
127 )
128 } else if reviews.len() == total {
129 format!(
130 "{total} PR{}, base {base}",
131 if total == 1 { "" } else { "s" }
132 )
133 } else {
134 format!(
135 "{total} branches ({} with reviews), base {base}",
136 reviews.len()
137 )
138 };
139
140 if !reviews.is_empty() {
141 let mut counts = Vec::new();
142 for (state, label) in [
143 (ReviewState::Open, "open"),
144 (ReviewState::Merged, "merged"),
145 (ReviewState::Closed, "closed"),
146 ] {
147 let count = reviews
148 .iter()
149 .filter(|review| review.state == state)
150 .count();
151 if count > 0 {
152 counts.push(format!("{count} {label}"));
153 }
154 }
155 if !counts.is_empty() {
156 summary.push_str(&format!(", {}", counts.join(" / ")));
157 }
158 }
159
160 summary
161}