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