1use std::collections::BTreeMap;
2
3use anyhow::Result;
4use clap::ValueEnum;
5
6use crate::commands::Run;
7use crate::providers::{ReviewRequest, ReviewState, detect_provider, review_provider};
8use crate::{git, stack};
9
10fn review_numbers() -> BTreeMap<String, String> {
14 let Some(provider) = detect_provider().ok().map(|p| review_provider(p.kind)) else {
15 return BTreeMap::new();
16 };
17 let Ok(reviews) = provider.open_reviews() else {
18 return BTreeMap::new();
19 };
20 reviews
21 .into_iter()
22 .map(|review| (review.branch, review.id))
23 .collect()
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
28pub enum Format {
29 Markdown,
31 Plain,
34}
35
36#[derive(Debug, clap::Args)]
38pub struct List {
39 #[arg(long, value_enum)]
41 format: Option<Format>,
42 #[arg(long, conflicts_with = "format")]
44 all: bool,
45}
46
47impl Run for List {
48 fn run(self) -> Result<()> {
49 match (self.format, self.all) {
50 (Some(format), _) => list_formatted(format),
51 (None, true) => crate::stack::print_all_stacks(&review_numbers()),
52 (None, false) => crate::stack::print_stack(&review_numbers()),
53 }
54 }
55}
56
57pub fn list_formatted(format: Format) -> Result<()> {
62 let current = git::current_branch()?;
63 let root = stack::stack_root(¤t)?;
64 let branches: Vec<String> = stack::branch_and_descendants(&root)?
65 .into_iter()
66 .skip(1) .collect();
68
69 if branches.is_empty() {
70 println!("no stacked branches");
71 return Ok(());
72 }
73
74 let review_provider = detect_provider().ok().map(|p| review_provider(p.kind));
75 let entries: Vec<(String, Option<ReviewRequest>)> = branches
76 .iter()
77 .map(|branch| {
78 let review = review_provider
79 .as_ref()
80 .and_then(|rp| rp.review_for_branch(branch).ok().flatten())
81 .filter(|review| review.branch == *branch);
82 (branch.clone(), review)
83 })
84 .collect();
85
86 println!("{}", summary(&entries, &root, format));
87 println!();
88 for (index, (branch, review)) in entries.iter().enumerate() {
89 let number = index + 1;
90 match (format, review) {
91 (Format::Markdown, Some(review)) => {
92 println!(
93 "{number}. [{}]({}) - {}",
94 review.label(),
95 review.url,
96 review.state
97 );
98 }
99 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
100 (Format::Plain, Some(review)) => {
102 println!("{number}. {} - {}", review.label(), review.state);
103 println!(" {}", review.url);
104 }
105 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
106 }
107 }
108
109 Ok(())
110}
111
112fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
115 let total = entries.len();
116 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
117 let base = match format {
118 Format::Markdown => format!("`{base}`"),
119 Format::Plain => base.to_owned(),
120 };
121
122 let mut summary = if reviews.is_empty() {
123 format!(
124 "{total} branch{}, base {base}",
125 if total == 1 { "" } else { "es" }
126 )
127 } else if reviews.len() == total {
128 format!(
129 "{total} PR{}, base {base}",
130 if total == 1 { "" } else { "s" }
131 )
132 } else {
133 format!(
134 "{total} branches ({} with reviews), base {base}",
135 reviews.len()
136 )
137 };
138
139 if !reviews.is_empty() {
140 let mut counts = Vec::new();
141 for (state, label) in [
142 (ReviewState::Open, "open"),
143 (ReviewState::Merged, "merged"),
144 (ReviewState::Closed, "closed"),
145 ] {
146 let count = reviews
147 .iter()
148 .filter(|review| review.state == state)
149 .count();
150 if count > 0 {
151 counts.push(format!("{count} {label}"));
152 }
153 }
154 if !counts.is_empty() {
155 summary.push_str(&format!(", {}", counts.join(" / ")));
156 }
157 }
158
159 summary
160}