Skip to main content

git_stk/commands/
list.rs

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/// Print the current stack.
9#[derive(Debug, clap::Args)]
10pub struct List {
11    /// Print a shareable markdown summary with PR links and states.
12    #[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
26/// Print the stack in a copy-paste markdown format for sharing with
27/// reviewers: a summary line, then the PRs as an ordered bottom-to-top list
28/// (merge order) with title, link, and state. Degrades to plain branch names
29/// when reviews or the provider CLI are unavailable.
30pub fn list_markdown() -> Result<()> {
31    let current = git::current_branch()?;
32    let root = stack::stack_root(&current)?;
33    let branches: Vec<String> = stack::branch_and_descendants(&root)?
34        .into_iter()
35        .skip(1) // the root is the base, not part of the stack
36        .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) => format!("[{}]({}) - {}", review.label(), review.url, review.state),
60            None => format!("`{branch}` (no review)"),
61        };
62        println!("{}. {item}", index + 1);
63    }
64
65    Ok(())
66}
67
68/// One-line stack summary, e.g. "3 PRs, base `main`, 2 open / 1 merged".
69fn markdown_summary(entries: &[(String, Option<ReviewRequest>)], base: &str) -> String {
70    let total = entries.len();
71    let reviews: Vec<&ReviewRequest> = entries.iter().filter_map(|(_, r)| r.as_ref()).collect();
72
73    let mut summary = if reviews.is_empty() {
74        format!(
75            "{total} branch{}, base `{base}`",
76            if total == 1 { "" } else { "es" }
77        )
78    } else if reviews.len() == total {
79        format!(
80            "{total} PR{}, base `{base}`",
81            if total == 1 { "" } else { "s" }
82        )
83    } else {
84        format!(
85            "{total} branches ({} with reviews), base `{base}`",
86            reviews.len()
87        )
88    };
89
90    if !reviews.is_empty() {
91        let mut counts = Vec::new();
92        for (state, label) in [
93            (ReviewState::Open, "open"),
94            (ReviewState::Merged, "merged"),
95            (ReviewState::Closed, "closed"),
96        ] {
97            let count = reviews
98                .iter()
99                .filter(|review| review.state == state)
100                .count();
101            if count > 0 {
102                counts.push(format!("{count} {label}"));
103            }
104        }
105        if !counts.is_empty() {
106            summary.push_str(&format!(", {}", counts.join(" / ")));
107        }
108    }
109
110    summary
111}