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 #[arg(long, conflicts_with = "format")]
26 all: bool,
27}
28
29impl Run for List {
30 fn run(self) -> Result<()> {
31 match (self.format, self.all) {
32 (Some(format), _) => list_formatted(format),
33 (None, true) => crate::stack::print_all_stacks(),
34 (None, false) => crate::stack::print_stack(),
35 }
36 }
37}
38
39pub fn list_formatted(format: Format) -> Result<()> {
44 let current = git::current_branch()?;
45 let root = stack::stack_root(¤t)?;
46 let branches: Vec<String> = stack::branch_and_descendants(&root)?
47 .into_iter()
48 .skip(1) .collect();
50
51 if branches.is_empty() {
52 println!("no stacked branches");
53 return Ok(());
54 }
55
56 let review_provider = detect_provider().ok().map(|p| review_provider(p.kind));
57 let entries: Vec<(String, Option<ReviewRequest>)> = branches
58 .iter()
59 .map(|branch| {
60 let review = review_provider
61 .as_ref()
62 .and_then(|rp| rp.review_for_branch(branch).ok().flatten())
63 .filter(|review| review.branch == *branch);
64 (branch.clone(), review)
65 })
66 .collect();
67
68 println!("{}", summary(&entries, &root, format));
69 println!();
70 for (index, (branch, review)) in entries.iter().enumerate() {
71 let number = index + 1;
72 match (format, review) {
73 (Format::Markdown, Some(review)) => {
74 println!(
75 "{number}. [{}]({}) - {}",
76 review.label(),
77 review.url,
78 review.state
79 );
80 }
81 (Format::Markdown, None) => println!("{number}. `{branch}` (no review)"),
82 (Format::Plain, Some(review)) => {
84 println!("{number}. {} - {}", review.label(), review.state);
85 println!(" {}", review.url);
86 }
87 (Format::Plain, None) => println!("{number}. {branch} (no review)"),
88 }
89 }
90
91 Ok(())
92}
93
94fn summary(entries: &[(String, Option<ReviewRequest>)], base: &str, format: Format) -> String {
97 let total = entries.len();
98 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
99 let base = match format {
100 Format::Markdown => format!("`{base}`"),
101 Format::Plain => base.to_owned(),
102 };
103
104 let mut summary = if reviews.is_empty() {
105 format!(
106 "{total} branch{}, base {base}",
107 if total == 1 { "" } else { "es" }
108 )
109 } else if reviews.len() == total {
110 format!(
111 "{total} PR{}, base {base}",
112 if total == 1 { "" } else { "s" }
113 )
114 } else {
115 format!(
116 "{total} branches ({} with reviews), base {base}",
117 reviews.len()
118 )
119 };
120
121 if !reviews.is_empty() {
122 let mut counts = Vec::new();
123 for (state, label) in [
124 (ReviewState::Open, "open"),
125 (ReviewState::Merged, "merged"),
126 (ReviewState::Closed, "closed"),
127 ] {
128 let count = reviews
129 .iter()
130 .filter(|review| review.state == state)
131 .count();
132 if count > 0 {
133 counts.push(format!("{count} {label}"));
134 }
135 }
136 if !counts.is_empty() {
137 summary.push_str(&format!(", {}", counts.join(" / ")));
138 }
139 }
140
141 summary
142}