git_workflow/commands/
status.rs1use crate::error::{GwError, Result};
4use crate::git;
5use crate::github::{self, PrInfo, PrState};
6use crate::output;
7use crate::state::{DetectContext, NextAction, RepoType, SyncState, WorkingDirState};
8
9pub fn run() -> Result<()> {
11 if !git::is_git_repo() {
13 return Err(GwError::NotAGitRepository);
14 }
15
16 let _ = git::fetch_prune(false);
20
21 let repo_type = RepoType::detect()?;
22 let home_branch = repo_type.home_branch();
23 let current = git::current_branch()?;
24 let working_dir = WorkingDirState::detect();
25 let sync_state = SyncState::detect(¤t).unwrap_or(SyncState::NoUpstream);
26 let has_remote = git::remote_branch_exists(¤t).unwrap_or(false);
28
29 println!();
30
31 match &repo_type {
33 RepoType::MainRepo { .. } => {
34 output::info("Repository: main repo");
35 }
36 RepoType::Worktree { home_branch } => {
37 output::info(&format!(
38 "Repository: worktree (home: {})",
39 output::bold(home_branch)
40 ));
41 }
42 }
43
44 if current == home_branch {
46 output::success(&format!("Branch: {} (home)", output::bold(¤t)));
47 } else {
48 output::info(&format!(
49 "Branch: {} (home: {})",
50 output::bold(¤t),
51 home_branch
52 ));
53 }
54
55 match working_dir {
57 WorkingDirState::Clean => {
58 output::success("Working directory: clean");
59 }
60 _ => {
61 output::warn(&format!("Working directory: {}", working_dir.description()));
62 }
63 }
64
65 match &sync_state {
67 SyncState::NoUpstream => {
68 output::info("Upstream: no tracking branch");
69 }
70 SyncState::Synced => {
71 output::success("Upstream: synced");
72 }
73 SyncState::HasUnpushedCommits { count } => {
74 output::warn(&format!("Upstream: {} unpushed commit(s)", count));
75 }
76 SyncState::Behind { count } => {
77 output::warn(&format!("Upstream: {} commit(s) behind", count));
78 }
79 SyncState::Diverged { ahead, behind } => {
80 output::warn(&format!(
81 "Upstream: diverged ({} ahead, {} behind)",
82 ahead, behind
83 ));
84 }
85 }
86
87 let default_branch = git::default_branch_name()?;
91 let (pr_info, base_pr_merged) = if current != home_branch {
92 get_and_show_pr_info(¤t, &default_branch)
93 } else {
94 (None, None)
95 };
96
97 if current != home_branch {
99 if has_remote {
100 output::info(&format!("Remote: origin/{} exists", current));
101 } else {
102 output::info("Remote: not pushed");
103 }
104 }
105
106 let recorded_base = if current != home_branch {
110 git::branch_base(¤t).filter(|b| b != &default_branch && b != ¤t)
111 } else {
112 None
113 };
114 let mut recorded_base_merged = false;
119 if pr_info.is_none() {
120 if let Some(base) = &recorded_base {
121 output::info(&format!("Base: {} (stacked, PR not created yet)", base));
122 recorded_base_merged = check_base_pr_merged(base).is_some();
123 }
124 }
125 let recorded_base_sha = recorded_base
128 .as_ref()
129 .and_then(|_| git::branch_base_sha(¤t));
130
131 let (base_ref, behind_base) = if current != home_branch {
135 let base_ref = base_ref_for(
136 &default_branch,
137 pr_info.as_ref(),
138 recorded_base.as_deref(),
139 recorded_base_merged,
140 );
141 let behind = if git::ref_exists(&base_ref) {
142 git::behind_base_count("HEAD", &base_ref)
143 } else {
144 0
145 };
146 if behind > 0 {
147 output::warn(&format!(
148 "Behind {}: {} commit(s) (gw sync to catch up)",
149 base_ref, behind
150 ));
151 }
152 (base_ref, behind)
153 } else {
154 (format!("origin/{default_branch}"), 0)
155 };
156
157 let stash_count = git::stash_count();
159 if stash_count > 0 {
160 output::info(&format!("Stashes: {}", stash_count));
161 }
162
163 let next_action = NextAction::detect(&DetectContext {
165 current_branch: ¤t,
166 home_branch,
167 working_dir: &working_dir,
168 sync_state: &sync_state,
169 pr_info: pr_info.as_ref(),
170 has_remote,
171 base_pr_merged: base_pr_merged.as_deref(),
172 recorded_base: recorded_base.as_deref(),
173 recorded_base_merged,
174 recorded_base_sha: recorded_base_sha.as_deref(),
175 base_ref: &base_ref,
176 behind_base,
177 });
178 next_action.display(¤t);
179
180 Ok(())
181}
182
183fn base_ref_for(
191 default_branch: &str,
192 pr_info: Option<&PrInfo>,
193 recorded_base: Option<&str>,
194 recorded_base_merged: bool,
195) -> String {
196 if let Some(pr) = pr_info {
197 if pr.state.is_open() && pr.base_branch != default_branch {
198 return format!("origin/{}", pr.base_branch);
199 }
200 return format!("origin/{default_branch}");
201 }
202 if let Some(base) = recorded_base {
203 if !recorded_base_merged {
204 let remote_ref = format!("origin/{base}");
205 if git::ref_exists(&remote_ref) {
206 return remote_ref;
207 }
208 return base.to_string();
209 }
210 }
211 format!("origin/{default_branch}")
212}
213
214fn get_and_show_pr_info(branch: &str, default_branch: &str) -> (Option<PrInfo>, Option<String>) {
221 if !github::is_gh_available() {
222 return (None, None);
223 }
224
225 match github::get_pr_for_branch(branch) {
226 Ok(Some(pr)) => {
227 let state_str = match &pr.state {
228 PrState::Open => "OPEN",
229 PrState::Merged { .. } => "MERGED",
230 PrState::Closed => "CLOSED",
231 };
232
233 let method_str = match &pr.state {
234 PrState::Merged { method, .. } => format!(" ({})", method),
235 _ => String::new(),
236 };
237
238 match &pr.state {
239 PrState::Open => {
240 output::info(&format!("PR: #{} {} [{}]", pr.number, pr.title, state_str));
241 }
242 PrState::Merged { .. } => {
243 output::success(&format!(
244 "PR: #{} {} [{}{}]",
245 pr.number, pr.title, state_str, method_str
246 ));
247 }
248 PrState::Closed => {
249 output::warn(&format!("PR: #{} {} [{}]", pr.number, pr.title, state_str));
250 }
251 }
252
253 if pr.base_branch != default_branch {
256 output::info(&format!(
257 "Base: {} (not {})",
258 pr.base_branch, default_branch
259 ));
260 }
261
262 let base_pr_merged = if pr.base_branch != default_branch && pr.state.is_open() {
264 check_base_pr_merged(&pr.base_branch)
265 } else {
266 None
267 };
268
269 (Some(pr), base_pr_merged)
270 }
271 Ok(None) => {
272 output::info("PR: none");
273 (None, None)
274 }
275 Err(e) => {
276 output::warn(&format!("Could not fetch PR info: {}", e));
277 (None, None)
278 }
279 }
280}
281
282fn check_base_pr_merged(base_branch: &str) -> Option<String> {
284 match github::get_pr_for_branch(base_branch) {
285 Ok(Some(base_pr)) => {
286 if base_pr.state.is_merged() {
287 output::success(&format!("Base PR: #{} [MERGED] ✓", base_pr.number));
288 Some(base_branch.to_string())
289 } else {
290 let state_str = if base_pr.state.is_open() {
291 "OPEN"
292 } else {
293 "CLOSED"
294 };
295 output::info(&format!("Base PR: #{} [{}]", base_pr.number, state_str));
296 None
297 }
298 }
299 Ok(None) => {
300 output::info(&format!("Base PR: none (for {})", base_branch));
301 None
302 }
303 Err(_) => None,
304 }
305}