1use crate::github::PrInfo;
6use crate::output;
7
8use super::{SyncState, WorkingDirState};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum NextAction {
13 StartNewWork,
15 SyncHomeWithUpstream { behind_count: usize },
17 CommitChanges,
19 PushChanges,
21 CreatePr,
23 WaitingForReview { pr_number: u64 },
25 Cleanup,
27 RebaseNeeded,
29 ResolveDivergence,
31 PrClosed { pr_number: u64 },
33 SyncNeeded { base_branch: String },
35}
36
37impl NextAction {
38 pub fn detect(
43 current_branch: &str,
44 home_branch: &str,
45 working_dir: &WorkingDirState,
46 sync_state: &SyncState,
47 pr_info: Option<&PrInfo>,
48 has_remote: bool,
49 base_pr_merged: Option<&str>,
50 ) -> Self {
51 if current_branch == home_branch {
53 if let SyncState::Behind { count } = sync_state {
55 return NextAction::SyncHomeWithUpstream {
56 behind_count: *count,
57 };
58 }
59 return NextAction::StartNewWork;
60 }
61
62 if let Some(pr) = pr_info {
64 if pr.state.is_merged() {
65 return NextAction::Cleanup;
66 }
67 if pr.state.is_closed() {
68 return NextAction::PrClosed {
69 pr_number: pr.number,
70 };
71 }
72 }
73
74 if let Some(base_branch) = base_pr_merged {
76 return NextAction::SyncNeeded {
77 base_branch: base_branch.to_string(),
78 };
79 }
80
81 if !matches!(working_dir, WorkingDirState::Clean) {
83 return NextAction::CommitChanges;
84 }
85
86 if matches!(sync_state, SyncState::Diverged { .. }) {
88 return NextAction::ResolveDivergence;
89 }
90
91 if matches!(sync_state, SyncState::Behind { .. }) {
93 return NextAction::RebaseNeeded;
94 }
95
96 if matches!(
98 sync_state,
99 SyncState::HasUnpushedCommits { .. } | SyncState::NoUpstream
100 ) {
101 return NextAction::PushChanges;
102 }
103
104 if pr_info.is_none() && has_remote {
106 return NextAction::CreatePr;
107 }
108
109 if let Some(pr) = pr_info {
111 if pr.state.is_open() {
112 return NextAction::WaitingForReview {
113 pr_number: pr.number,
114 };
115 }
116 }
117
118 NextAction::WaitingForReview { pr_number: 0 }
120 }
121
122 pub fn display(&self, branch: &str) {
124 println!();
125 output::separator();
126
127 match self {
128 NextAction::StartNewWork => {
129 output::action("Next: start new work");
130 println!();
131 println!(" gw new feature/your-feature");
132 }
133 NextAction::SyncHomeWithUpstream { behind_count } => {
134 output::action(&format!(
135 "Next: sync with upstream ({} commit(s) behind)",
136 behind_count
137 ));
138 println!();
139 println!(" gw home");
140 }
141 NextAction::CommitChanges => {
142 output::action("Next: commit changes");
143 println!();
144 println!(
145 " git add <files> && git commit -m \"feat: ...\" # stage deliberately, not -A"
146 );
147 }
148 NextAction::PushChanges => {
149 output::action("Next: push to remote");
150 println!();
151 println!(" git push -u origin {}", branch);
152 }
153 NextAction::CreatePr => {
154 output::action("Next: create pull request");
155 println!();
156 println!(" gh pr create -a \"@me\" -t \"...\"");
157 }
158 NextAction::WaitingForReview { pr_number } => {
159 if *pr_number > 0 {
160 output::action(&format!("Waiting: PR #{} in review", pr_number));
161 println!();
162 println!(
163 " gw await {} --open # Wait for merge, then cleanup",
164 pr_number
165 );
166 println!(" gw open # Open PR in browser");
167 } else {
168 output::action("Waiting: PR in review");
169 println!();
170 println!(" gw open # Open PR in browser");
171 }
172 }
173 NextAction::Cleanup => {
174 output::action("Next: cleanup merged branch");
175 println!();
176 println!(" gw cleanup");
177 }
178 NextAction::RebaseNeeded => {
179 output::action("Next: rebase on latest main");
180 println!();
181 println!(" git fetch --prune && git rebase origin/main");
182 }
183 NextAction::ResolveDivergence => {
184 output::action("Next: resolve divergence");
185 println!();
186 println!(" # Option 1: Rebase (preferred)");
187 println!(" git fetch --prune && git rebase origin/main");
188 println!();
189 println!(" # Option 2: Force push (if you know what you're doing)");
190 println!(" git push --force-with-lease");
191 }
192 NextAction::PrClosed { pr_number } => {
193 output::action(&format!("PR #{} was closed without merging", pr_number));
194 println!();
195 println!(" # Option 1: Reopen the PR");
196 println!(" gh pr reopen {}", pr_number);
197 println!();
198 println!(" # Option 2: Cleanup and start fresh");
199 println!(" gw cleanup");
200 }
201 NextAction::SyncNeeded { base_branch } => {
202 output::action(&format!("Next: sync (base '{}' was merged)", base_branch));
203 println!();
204 println!(" gw sync");
205 }
206 }
207
208 output::separator();
209 }
210
211 pub fn short_description(&self) -> &'static str {
213 match self {
214 NextAction::StartNewWork => "start new work",
215 NextAction::SyncHomeWithUpstream { .. } => "sync with upstream",
216 NextAction::CommitChanges => "commit changes",
217 NextAction::PushChanges => "push to remote",
218 NextAction::CreatePr => "create PR",
219 NextAction::WaitingForReview { .. } => "waiting for review",
220 NextAction::Cleanup => "cleanup branch",
221 NextAction::RebaseNeeded => "rebase needed",
222 NextAction::ResolveDivergence => "resolve divergence",
223 NextAction::PrClosed { .. } => "PR closed",
224 NextAction::SyncNeeded { .. } => "sync needed",
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::github::{PrInfo, PrState};
233
234 #[test]
235 fn test_on_home_branch_suggests_start_new_work() {
236 let action = NextAction::detect(
237 "main",
238 "main",
239 &WorkingDirState::Clean,
240 &SyncState::Synced,
241 None,
242 false,
243 None,
244 );
245 assert_eq!(action, NextAction::StartNewWork);
246 }
247
248 #[test]
249 fn test_on_home_branch_behind_suggests_sync() {
250 let action = NextAction::detect(
251 "main",
252 "main",
253 &WorkingDirState::Clean,
254 &SyncState::Behind { count: 5 },
255 None,
256 false,
257 None,
258 );
259 assert_eq!(action, NextAction::SyncHomeWithUpstream { behind_count: 5 });
260 }
261
262 #[test]
263 fn test_uncommitted_changes_suggests_commit() {
264 let action = NextAction::detect(
265 "feature/test",
266 "main",
267 &WorkingDirState::HasUnstagedChanges,
268 &SyncState::Synced,
269 None,
270 true,
271 None,
272 );
273 assert_eq!(action, NextAction::CommitChanges);
274 }
275
276 #[test]
277 fn test_unpushed_commits_suggests_push() {
278 let action = NextAction::detect(
279 "feature/test",
280 "main",
281 &WorkingDirState::Clean,
282 &SyncState::HasUnpushedCommits { count: 2 },
283 None,
284 true,
285 None,
286 );
287 assert_eq!(action, NextAction::PushChanges);
288 }
289
290 #[test]
291 fn test_no_upstream_suggests_push() {
292 let action = NextAction::detect(
293 "feature/test",
294 "main",
295 &WorkingDirState::Clean,
296 &SyncState::NoUpstream,
297 None,
298 false,
299 None,
300 );
301 assert_eq!(action, NextAction::PushChanges);
302 }
303
304 #[test]
305 fn test_pushed_no_pr_suggests_create_pr() {
306 let action = NextAction::detect(
307 "feature/test",
308 "main",
309 &WorkingDirState::Clean,
310 &SyncState::Synced,
311 None,
312 true,
313 None,
314 );
315 assert_eq!(action, NextAction::CreatePr);
316 }
317
318 #[test]
319 fn test_open_pr_suggests_waiting() {
320 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "main");
321 let action = NextAction::detect(
322 "feature/test",
323 "main",
324 &WorkingDirState::Clean,
325 &SyncState::Synced,
326 Some(&pr),
327 true,
328 None,
329 );
330 assert_eq!(action, NextAction::WaitingForReview { pr_number: 42 });
331 }
332
333 #[test]
334 fn test_merged_pr_suggests_cleanup() {
335 let pr = PrInfo::new(
336 42,
337 "Test PR",
338 "https://...",
339 PrState::Merged {
340 method: crate::github::MergeMethod::Squash,
341 merge_commit: None,
342 },
343 "main",
344 );
345 let action = NextAction::detect(
346 "feature/test",
347 "main",
348 &WorkingDirState::Clean,
349 &SyncState::Synced,
350 Some(&pr),
351 true,
352 None,
353 );
354 assert_eq!(action, NextAction::Cleanup);
355 }
356
357 #[test]
358 fn test_closed_pr_suggests_reopen_or_cleanup() {
359 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Closed, "main");
360 let action = NextAction::detect(
361 "feature/test",
362 "main",
363 &WorkingDirState::Clean,
364 &SyncState::Synced,
365 Some(&pr),
366 true,
367 None,
368 );
369 assert_eq!(action, NextAction::PrClosed { pr_number: 42 });
370 }
371
372 #[test]
373 fn test_behind_suggests_rebase() {
374 let action = NextAction::detect(
375 "feature/test",
376 "main",
377 &WorkingDirState::Clean,
378 &SyncState::Behind { count: 3 },
379 None,
380 true,
381 None,
382 );
383 assert_eq!(action, NextAction::RebaseNeeded);
384 }
385
386 #[test]
387 fn test_diverged_suggests_resolve() {
388 let action = NextAction::detect(
389 "feature/test",
390 "main",
391 &WorkingDirState::Clean,
392 &SyncState::Diverged {
393 ahead: 2,
394 behind: 3,
395 },
396 None,
397 true,
398 None,
399 );
400 assert_eq!(action, NextAction::ResolveDivergence);
401 }
402
403 #[test]
404 fn test_uncommitted_changes_takes_priority_over_pr_open() {
405 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "main");
406 let action = NextAction::detect(
407 "feature/test",
408 "main",
409 &WorkingDirState::HasStagedChanges,
410 &SyncState::Synced,
411 Some(&pr),
412 true,
413 None,
414 );
415 assert_eq!(action, NextAction::CommitChanges);
416 }
417
418 #[test]
419 fn test_merged_pr_takes_priority_over_uncommitted_changes() {
420 let pr = PrInfo::new(
421 42,
422 "Test PR",
423 "https://...",
424 PrState::Merged {
425 method: crate::github::MergeMethod::Squash,
426 merge_commit: None,
427 },
428 "main",
429 );
430 let action = NextAction::detect(
431 "feature/test",
432 "main",
433 &WorkingDirState::HasUnstagedChanges,
434 &SyncState::Synced,
435 Some(&pr),
436 true,
437 None,
438 );
439 assert_eq!(action, NextAction::Cleanup);
441 }
442
443 #[test]
444 fn test_base_pr_merged_suggests_sync() {
445 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "feature/base");
446 let action = NextAction::detect(
447 "feature/child",
448 "main",
449 &WorkingDirState::Clean,
450 &SyncState::Synced,
451 Some(&pr),
452 true,
453 Some("feature/base"),
454 );
455 assert_eq!(
456 action,
457 NextAction::SyncNeeded {
458 base_branch: "feature/base".to_string()
459 }
460 );
461 }
462
463 #[test]
464 fn test_base_pr_merged_takes_priority_over_waiting() {
465 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "feature/base");
466 let action = NextAction::detect(
467 "feature/child",
468 "main",
469 &WorkingDirState::Clean,
470 &SyncState::Synced,
471 Some(&pr),
472 true,
473 Some("feature/base"),
474 );
475 assert_eq!(
477 action,
478 NextAction::SyncNeeded {
479 base_branch: "feature/base".to_string()
480 }
481 );
482 }
483}