1use anyhow::Result;
2use clap::ValueEnum;
3
4use crate::commands::Run;
5use crate::providers::{ReviewRequest, ReviewState, detect_provider, review_provider};
6use crate::{git, stack};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
10pub enum Format {
11 Markdown,
13 Plain,
16}
17
18#[derive(Debug, clap::Args)]
20pub struct List {
21 #[arg(long, value_enum)]
23 format: Option<Format>,
24}
25
26impl Run for List {
27 fn run(self) -> Result<()> {
28 match self.format {
29 Some(format) => list_formatted(format),
30 None => crate::stack::print_stack(),
31 }
32 }
33}
34
35pub fn list_formatted(format: Format) -> Result<()> {
40 let current = git::current_branch()?;
41 let root = stack::stack_root(¤t)?;
42 let branches: Vec<String> = stack::branch_and_descendants(&root)?
43 .into_iter()
44 .skip(1) .collect();
46
47 if branches.is_empty() {
48 println!("no stacked branches");
49 return Ok(());
50 }
51
52 let review_provider = detect_provider().ok().map(|p| review_provider(p.kind));
53 let entries: Vec<(String, Option<ReviewRequest>)> = branches
54 .iter()
55 .map(|branch| {
56 let review = review_provider
57 .as_ref()
58 .and_then(|rp| rp.review_for_branch(branch).ok().flatten())
59 .filter(|review| review.branch == *branch);
60 (branch.clone(), review)
61 })
62 .collect();
63
64 println!("{}", summary(&entries, &root, format));
65 println!();
66 for (index, (branch, review)) in entries.iter().enumerate() {
67 let number = index + 1;
68 match (format, review) {
69 (Format::Markdown, Some(review)) => {
70 println!(
71 "{number}. [{}]({}) - {}",
72 review.label(),
73 review.url,
74 review.state
75 );
76 }
77 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
78 (Format::Plain, Some(review)) => {
80 println!("{number}. {} - {}", review.label(), review.state);
81 println!(" {}", review.url);
82 }
83 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
84 }
85 }
86
87 Ok(())
88}
89
90fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
93 let total = entries.len();
94 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
95 let base = match format {
96 Format::Markdown => format!("`{base}`"),
97 Format::Plain => base.to_owned(),
98 };
99
100 let mut summary = if reviews.is_empty() {
101 format!(
102 "{total} branch{}, base {base}",
103 if total == 1 { "" } else { "es" }
104 )
105 } else if reviews.len() == total {
106 format!(
107 "{total} PR{}, base {base}",
108 if total == 1 { "" } else { "s" }
109 )
110 } else {
111 format!(
112 "{total} branches ({} with reviews), base {base}",
113 reviews.len()
114 )
115 };
116
117 if !reviews.is_empty() {
118 let mut counts = Vec::new();
119 for (state, label) in [
120 (ReviewState::Open, "open"),
121 (ReviewState::Merged, "merged"),
122 (ReviewState::Closed, "closed"),
123 ] {
124 let count = reviews
125 .iter()
126 .filter(|review| review.state == state)
127 .count();
128 if count > 0 {
129 counts.push(format!("{count} {label}"));
130 }
131 }
132 if !counts.is_empty() {
133 summary.push_str(&format!(", {}", counts.join(" / ")));
134 }
135 }
136
137 summary
138}