Skip to main content

git_stk/commands/
status.rs

1use anyhow::Result;
2use clap_complete::engine::ArgValueCompleter;
3
4use crate::commands::Run;
5use crate::completions;
6use crate::providers::{ReviewState, detect_review_provider};
7use crate::style;
8use crate::{git, stack};
9
10/// Print local and remote stack status for a branch.
11#[derive(Debug, clap::Args)]
12pub struct Status {
13    #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
14    branch: Option<String>,
15}
16
17impl Run for Status {
18    fn run(self) -> Result<()> {
19        print_status(self.branch.as_deref())
20    }
21}
22
23pub fn print_status(branch: Option<&str>) -> Result<()> {
24    let branch = branch
25        .map(str::to_owned)
26        .map_or_else(git::current_branch, Ok)?;
27    let parent = stack::parent_of(&branch)?;
28    let children = stack::children_of(&branch)?;
29
30    anstream::println!("branch: {}", style::paint(style::CURRENT, &branch));
31    match parent.as_deref() {
32        Some(parent) => anstream::println!("parent: {}", style::paint(style::BRANCH, parent)),
33        None => anstream::println!("parent: none"),
34    }
35    if children.is_empty() {
36        anstream::println!("children: none");
37    } else {
38        let children: Vec<String> = children
39            .iter()
40            .map(|child| style::paint(style::BRANCH, child))
41            .collect();
42        anstream::println!("children: {}", children.join(", "));
43    }
44
45    // Provider state is best-effort: a repo with no remote (or no provider
46    // configured) still shows its local stack rather than hard-failing.
47    let detected = detect_review_provider().ok();
48    let review = match &detected {
49        Some((provider, review_provider)) => {
50            anstream::println!("provider: {} ({})", provider.kind, provider.source);
51            // Closed-inclusive: a review closed without merging is part of the
52            // branch's story, not "no review".
53            let review = review_provider.review_for_branch_including_closed(&branch)?;
54            match &review {
55                Some(review) => {
56                    anstream::println!(
57                        "review: {} {} {} -> {}",
58                        review.id,
59                        style::state(&review.state),
60                        style::paint(style::BRANCH, &review.branch),
61                        style::paint(style::BRANCH, &review.base)
62                    );
63                    anstream::println!("url: {}", style::paint(style::DIM, &review.url));
64
65                    if let Some(parent) = parent.as_deref()
66                        && parent != review.base
67                    {
68                        anstream::println!(
69                            "{} review base is {}, local parent is {parent} - run `git stk submit`",
70                            style::paint(style::WARN, "warning:"),
71                            review.base
72                        );
73                    }
74                }
75                None => anstream::println!("review: none"),
76            }
77            review
78        }
79        None => {
80            anstream::println!("{}", style::dim("provider: not detected (no review info)"));
81            None
82        }
83    };
84
85    // Teach the loop: the next command, derived from review states and
86    // local drift. A sync covers the restack, so the nudges don't stack.
87    let mut hints = Vec::new();
88    match &review {
89        Some(review) if review.state == ReviewState::Merged => {
90            hints.push(format!(
91                "review {} is merged - run `git stk sync`",
92                review.id
93            ));
94        }
95        Some(review) if review.state == ReviewState::Closed => {
96            hints.push(format!(
97                "review {} was closed without merging - `git stk submit` opens a new review",
98                review.id
99            ));
100        }
101        _ => {}
102    }
103    if let Some(parent) = parent.as_deref() {
104        if let Some((_, review_provider)) = &detected {
105            match review_provider.review_for_branch_including_closed(parent) {
106                Ok(Some(parent_review)) if parent_review.branch == parent => {
107                    match parent_review.state {
108                        ReviewState::Merged => hints.push(format!(
109                            "parent review {} is merged - run `git stk sync`",
110                            parent_review.id
111                        )),
112                        ReviewState::Closed => hints.push(format!(
113                            "parent review {} was closed without merging - \
114                             retarget {branch} with `git stk adopt`",
115                            parent_review.id
116                        )),
117                        _ => {}
118                    }
119                }
120                _ => {}
121            }
122        }
123
124        if hints.is_empty()
125            && let Some(hint) = stack::behind_parent_hint(&branch, parent)
126        {
127            hints.push(hint);
128        }
129    }
130    for hint in hints {
131        anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
132    }
133
134    Ok(())
135}