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_provider, review_provider};
7use crate::{git, stack};
8
9/// Print local and remote stack status for a branch.
10#[derive(Debug, clap::Args)]
11pub struct Status {
12    #[arg(add = ArgValueCompleter::new(completions::branch_candidates))]
13    branch: Option<String>,
14}
15
16impl Run for Status {
17    fn run(self) -> Result<()> {
18        print_status(self.branch.as_deref())
19    }
20}
21
22pub fn print_status(branch: Option<&str>) -> Result<()> {
23    let branch = branch
24        .map(str::to_owned)
25        .map_or_else(git::current_branch, Ok)?;
26    let parent = stack::parent_for_branch(&branch)?;
27    let children = stack::children_for_branch(&branch)?;
28
29    println!("branch: {branch}");
30    match parent.as_deref() {
31        Some(parent) => println!("parent: {parent}"),
32        None => println!("parent: none"),
33    }
34    if children.is_empty() {
35        println!("children: none");
36    } else {
37        println!("children: {}", children.join(", "));
38    }
39
40    let provider = detect_provider()?;
41    println!("provider: {} ({})", provider.kind, provider.source);
42    let review_provider = review_provider(provider.kind);
43
44    // Closed-inclusive: a review closed without merging is part of the
45    // branch's story, not "no review".
46    let review = review_provider.review_for_branch_including_closed(&branch)?;
47    match &review {
48        Some(review) => {
49            println!(
50                "review: {} {} {} -> {}",
51                review.id, review.state, review.branch, review.base
52            );
53            println!("url: {}", review.url);
54
55            if let Some(parent) = parent.as_deref()
56                && parent != review.base
57            {
58                println!(
59                    "warning: review base is {}, local parent is {parent} - run `git stk submit`",
60                    review.base
61                );
62            }
63        }
64        None => println!("review: none"),
65    }
66
67    // Teach the loop: the next command, derived from review states and
68    // local drift. A sync covers the restack, so the nudges don't stack.
69    let mut hints = Vec::new();
70    match &review {
71        Some(review) if review.state == ReviewState::Merged => {
72            hints.push(format!(
73                "review {} is merged - run `git stk sync`",
74                review.id
75            ));
76        }
77        Some(review) if review.state == ReviewState::Closed => {
78            hints.push(format!(
79                "review {} was closed without merging - `git stk submit` opens a new review",
80                review.id
81            ));
82        }
83        _ => {}
84    }
85    if let Some(parent) = parent.as_deref() {
86        match review_provider.review_for_branch_including_closed(parent) {
87            Ok(Some(parent_review)) if parent_review.branch == parent => {
88                match parent_review.state {
89                    ReviewState::Merged => hints.push(format!(
90                        "parent review {} is merged - run `git stk sync`",
91                        parent_review.id
92                    )),
93                    ReviewState::Closed => hints.push(format!(
94                        "parent review {} was closed without merging - \
95                         retarget {branch} with `git stk adopt`",
96                        parent_review.id
97                    )),
98                    _ => {}
99                }
100            }
101            _ => {}
102        }
103
104        if hints.is_empty()
105            && let Some(hint) = stack::behind_parent_hint(&branch, parent)
106        {
107            hints.push(hint);
108        }
109    }
110    for hint in hints {
111        println!("hint: {hint}");
112    }
113
114    Ok(())
115}