Skip to main content

git_workflow/commands/
status.rs

1//! `gw status` command - Show current repository state
2
3use crate::error::{GwError, Result};
4use crate::git;
5use crate::github::{self, PrInfo, PrState};
6use crate::output;
7use crate::state::{NextAction, RepoType, SyncState, WorkingDirState};
8
9/// Execute the `status` command
10pub fn run() -> Result<()> {
11    // Ensure we're in a git repo
12    if !git::is_git_repo() {
13        return Err(GwError::NotAGitRepository);
14    }
15
16    let repo_type = RepoType::detect()?;
17    let home_branch = repo_type.home_branch();
18    let current = git::current_branch()?;
19    let working_dir = WorkingDirState::detect();
20    let sync_state = SyncState::detect(&current).unwrap_or(SyncState::NoUpstream);
21    // For status display, an unverifiable remote is reported as "not pushed".
22    let has_remote = git::remote_branch_exists(&current).unwrap_or(false);
23
24    println!();
25
26    // Repository type
27    match &repo_type {
28        RepoType::MainRepo { .. } => {
29            output::info("Repository: main repo");
30        }
31        RepoType::Worktree { home_branch } => {
32            output::info(&format!(
33                "Repository: worktree (home: {})",
34                output::bold(home_branch)
35            ));
36        }
37    }
38
39    // Current branch
40    if current == home_branch {
41        output::success(&format!("Branch: {} (home)", output::bold(&current)));
42    } else {
43        output::info(&format!(
44            "Branch: {} (home: {})",
45            output::bold(&current),
46            home_branch
47        ));
48    }
49
50    // Working directory state
51    match working_dir {
52        WorkingDirState::Clean => {
53            output::success("Working directory: clean");
54        }
55        _ => {
56            output::warn(&format!("Working directory: {}", working_dir.description()));
57        }
58    }
59
60    // Sync state
61    match &sync_state {
62        SyncState::NoUpstream => {
63            output::info("Upstream: no tracking branch");
64        }
65        SyncState::Synced => {
66            output::success("Upstream: synced");
67        }
68        SyncState::HasUnpushedCommits { count } => {
69            output::warn(&format!("Upstream: {} unpushed commit(s)", count));
70        }
71        SyncState::Behind { count } => {
72            output::warn(&format!("Upstream: {} commit(s) behind", count));
73        }
74        SyncState::Diverged { ahead, behind } => {
75            output::warn(&format!(
76                "Upstream: diverged ({} ahead, {} behind)",
77                ahead, behind
78            ));
79        }
80    }
81
82    // PR info (only for non-home branches). The default branch (main/master) is
83    // the trunk a stacked PR ultimately targets — distinct from this worktree's
84    // home branch.
85    let default_branch = git::default_branch_name()?;
86    let (pr_info, base_pr_merged) = if current != home_branch {
87        get_and_show_pr_info(&current, &default_branch)
88    } else {
89        (None, None)
90    };
91
92    // Remote branch status
93    if current != home_branch {
94        if has_remote {
95            output::info(&format!("Remote: origin/{} exists", current));
96        } else {
97            output::info("Remote: not pushed");
98        }
99    }
100
101    // Stash count
102    let stash_count = git::stash_count();
103    if stash_count > 0 {
104        output::info(&format!("Stashes: {}", stash_count));
105    }
106
107    // Next action
108    let next_action = NextAction::detect(
109        &current,
110        home_branch,
111        &working_dir,
112        &sync_state,
113        pr_info.as_ref(),
114        has_remote,
115        base_pr_merged.as_deref(),
116    );
117    next_action.display(&current);
118
119    Ok(())
120}
121
122/// Get and show PR information for a branch
123///
124/// Returns:
125/// - (Some(PrInfo), Some(base_branch)) if PR exists and base PR was merged
126/// - (Some(PrInfo), None) if PR exists but base is the default branch or base PR not merged
127/// - (None, None) if no PR found
128fn get_and_show_pr_info(branch: &str, default_branch: &str) -> (Option<PrInfo>, Option<String>) {
129    if !github::is_gh_available() {
130        return (None, None);
131    }
132
133    match github::get_pr_for_branch(branch) {
134        Ok(Some(pr)) => {
135            let state_str = match &pr.state {
136                PrState::Open => "OPEN",
137                PrState::Merged { .. } => "MERGED",
138                PrState::Closed => "CLOSED",
139            };
140
141            let method_str = match &pr.state {
142                PrState::Merged { method, .. } => format!(" ({})", method),
143                _ => String::new(),
144            };
145
146            match &pr.state {
147                PrState::Open => {
148                    output::info(&format!("PR: #{} {} [{}]", pr.number, pr.title, state_str));
149                }
150                PrState::Merged { .. } => {
151                    output::success(&format!(
152                        "PR: #{} {} [{}{}]",
153                        pr.number, pr.title, state_str, method_str
154                    ));
155                }
156                PrState::Closed => {
157                    output::warn(&format!("PR: #{} {} [{}]", pr.number, pr.title, state_str));
158                }
159            }
160
161            // Show base branch info (a base other than the default branch means
162            // this is a stacked PR).
163            if pr.base_branch != default_branch {
164                output::info(&format!(
165                    "Base: {} (not {})",
166                    pr.base_branch, default_branch
167                ));
168            }
169
170            // Check if base PR is merged (only for a stacked, still-open PR)
171            let base_pr_merged = if pr.base_branch != default_branch && pr.state.is_open() {
172                check_base_pr_merged(&pr.base_branch)
173            } else {
174                None
175            };
176
177            (Some(pr), base_pr_merged)
178        }
179        Ok(None) => {
180            output::info("PR: none");
181            (None, None)
182        }
183        Err(e) => {
184            output::warn(&format!("Could not fetch PR info: {}", e));
185            (None, None)
186        }
187    }
188}
189
190/// Check if the base branch's PR has been merged
191fn check_base_pr_merged(base_branch: &str) -> Option<String> {
192    match github::get_pr_for_branch(base_branch) {
193        Ok(Some(base_pr)) => {
194            if base_pr.state.is_merged() {
195                output::success(&format!("Base PR: #{} [MERGED] ✓", base_pr.number));
196                Some(base_branch.to_string())
197            } else {
198                let state_str = if base_pr.state.is_open() {
199                    "OPEN"
200                } else {
201                    "CLOSED"
202                };
203                output::info(&format!("Base PR: #{} [{}]", base_pr.number, state_str));
204                None
205            }
206        }
207        Ok(None) => {
208            output::info(&format!("Base PR: none (for {})", base_branch));
209            None
210        }
211        Err(_) => None,
212    }
213}