1use anyhow::Result;
2use clap::ArgAction;
3
4use crate::commands::Run;
5use crate::providers::{ReviewRequest, ReviewState, detect_provider, review_provider};
6use crate::{git, stack};
7
8#[derive(Debug, clap::Args)]
10pub struct List {
11 #[arg(long, action = ArgAction::SetTrue)]
13 markdown: bool,
14}
15
16impl Run for List {
17 fn run(self) -> Result<()> {
18 if self.markdown {
19 list_markdown()
20 } else {
21 crate::stack::print_stack()
22 }
23 }
24}
25
26pub fn list_markdown() -> Result<()> {
31 let current = git::current_branch()?;
32 let root = stack::stack_root(¤t)?;
33 let branches: Vec<String> = stack::branch_and_descendants(&root)?
34 .into_iter()
35 .skip(1) .collect();
37
38 if branches.is_empty() {
39 println!("no stacked branches");
40 return Ok(());
41 }
42
43 let review_provider = detect_provider().ok().map(|p| review_provider(p.kind));
44 let entries: Vec<(String, Option<ReviewRequest>)> = branches
45 .iter()
46 .map(|branch| {
47 let review = review_provider
48 .as_ref()
49 .and_then(|rp| rp.review_for_branch(branch).ok().flatten())
50 .filter(|review| review.branch == *branch);
51 (branch.clone(), review)
52 })
53 .collect();
54
55 println!("{}", markdown_summary(&entries, &root));
56 println!();
57 for (index, (branch, review)) in entries.iter().enumerate() {
58 let item = match review {
59 Some(review) => {
60 let label = if review.title.is_empty() {
61 review.id.clone()
62 } else {
63 format!("{} ({})", review.title, review.id)
64 };
65 format!("[{label}]({}) - {}", review.url, review.state)
66 }
67 None => format!("`{branch}` (no review)"),
68 };
69 println!("{}. {item}", index + 1);
70 }
71
72 Ok(())
73}
74
75fn markdown_summary(entries: &[(String, Option<ReviewRequest>)], base: &str) -> String {
77 let total = entries.len();
78 let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
79
80 let mut summary = if reviews.is_empty() {
81 format!(
82 "{total} branch{}, base `{base}`",
83 if total == 1 { "" } else { "es" }
84 )
85 } else if reviews.len() == total {
86 format!(
87 "{total} PR{}, base `{base}`",
88 if total == 1 { "" } else { "s" }
89 )
90 } else {
91 format!(
92 "{total} branches ({} with reviews), base `{base}`",
93 reviews.len()
94 )
95 };
96
97 if !reviews.is_empty() {
98 let mut counts = Vec::new();
99 for (state, label) in [
100 (ReviewState::Open, "open"),
101 (ReviewState::Merged, "merged"),
102 (ReviewState::Closed, "closed"),
103 ] {
104 let count = reviews
105 .iter()
106 .filter(|review| review.state == state)
107 .count();
108 if count > 0 {
109 counts.push(format!("{count} {label}"));
110 }
111 }
112 if !counts.is_empty() {
113 summary.push_str(&format!(", {}", counts.join(" / ")));
114 }
115 }
116
117 summary
118}