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::{CheckStatus, 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    // Where the branch actually lives, when that is not here. Best effort: a
33    // failed listing just omits the line.
34    if let Some(path) = git::worktree_holding(&branch).ok().flatten() {
35        anstream::println!("worktree: {}", git::display_path(&path));
36    }
37    match parent.as_deref() {
38        Some(parent) => anstream::println!("parent: {}", style::paint(style::BRANCH, parent)),
39        None => anstream::println!("parent: none"),
40    }
41    if children.is_empty() {
42        anstream::println!("children: none");
43    } else {
44        let children: Vec<String> = children
45            .iter()
46            .map(|child| style::paint(style::BRANCH, child))
47            .collect();
48        anstream::println!("children: {}", children.join(", "));
49    }
50
51    // Provider state is best-effort: a repo with no remote (or no provider
52    // configured) still shows its local stack rather than hard-failing.
53    let detected = detect_review_provider().ok();
54    let review = match &detected {
55        Some((provider, review_provider)) => {
56            anstream::println!("provider: {} ({})", provider.kind, provider.source);
57            // Closed-inclusive: a review closed without merging is part of the
58            // branch's story, not "no review".
59            let review = review_provider.review_for_branch_including_closed(&branch)?;
60            match &review {
61                Some(review) => {
62                    // A queued review shows just the clock (it is waiting to
63                    // land); otherwise the CI dot. Both best-effort - a failed
64                    // lookup just omits the marker. When queued, the CI dot is
65                    // suppressed, so skip fetching it.
66                    let queued = review_provider
67                        .enqueued_branches(std::slice::from_ref(&review.branch))
68                        .map(|set| set.contains(&review.branch))
69                        .unwrap_or(false);
70                    let marker = if queued {
71                        crate::providers::QUEUED_MARK
72                    } else {
73                        review_provider
74                            .check_status(review)
75                            .unwrap_or(CheckStatus::None)
76                            .dot()
77                    };
78                    anstream::println!(
79                        "review: {marker}{} {} {} -> {}",
80                        review.id,
81                        style::state(&review.state),
82                        style::paint(style::BRANCH, &review.branch),
83                        style::paint(style::BRANCH, &review.base)
84                    );
85                    anstream::println!("url: {}", style::paint(style::DIM, &review.url));
86
87                    if let Some(parent) = parent.as_deref()
88                        && parent != review.base
89                    {
90                        anstream::println!(
91                            "{} review base is {}, local parent is {parent} - run `git stk submit`",
92                            style::paint(style::WARN, "warning:"),
93                            review.base
94                        );
95                    }
96                }
97                None => anstream::println!("review: none"),
98            }
99            review
100        }
101        None => {
102            anstream::println!("{}", style::dim("provider: not detected (no review info)"));
103            None
104        }
105    };
106
107    // Teach the loop: the next command, derived from review states and
108    // local drift. A sync covers the restack, so the nudges don't stack.
109    let mut hints = Vec::new();
110    match &review {
111        Some(review) if review.state == ReviewState::Merged => {
112            hints.push(format!(
113                "review {} is merged - run `git stk sync`",
114                review.id
115            ));
116        }
117        Some(review) if review.state == ReviewState::Closed => {
118            hints.push(format!(
119                "review {} was closed without merging - `git stk submit` opens a new review",
120                review.id
121            ));
122        }
123        _ => {}
124    }
125    if let Some(parent) = parent.as_deref() {
126        if let Some((_, review_provider)) = &detected {
127            match review_provider.review_for_branch_including_closed(parent) {
128                Ok(Some(parent_review)) if parent_review.branch == parent => {
129                    match parent_review.state {
130                        ReviewState::Merged => hints.push(format!(
131                            "parent review {} is merged - run `git stk sync`",
132                            parent_review.id
133                        )),
134                        ReviewState::Closed => hints.push(format!(
135                            "parent review {} was closed without merging - \
136                             retarget {branch} with `git stk adopt`",
137                            parent_review.id
138                        )),
139                        _ => {}
140                    }
141                }
142                _ => {}
143            }
144        }
145
146        if hints.is_empty()
147            && let Some(hint) = stack::behind_parent_hint(&branch, parent)
148        {
149            hints.push(hint);
150        }
151    }
152    for hint in hints {
153        anstream::println!("{} {hint}", style::paint(style::HINT, "hint:"));
154    }
155
156    Ok(())
157}