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