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!(" mise run git: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!(" mise run git:home");
140 }
141 NextAction::CommitChanges => {
142 output::action("Next: commit changes");
143 println!();
144 println!(" git add -A && git commit -m \"feat: ...\"");
145 }
146 NextAction::PushChanges => {
147 output::action("Next: push to remote");
148 println!();
149 println!(" git push -u origin {}", branch);
150 }
151 NextAction::CreatePr => {
152 output::action("Next: create pull request");
153 println!();
154 println!(" gh pr create -a \"@me\" -t \"...\"");
155 }
156 NextAction::WaitingForReview { pr_number } => {
157 if *pr_number > 0 {
158 output::action(&format!("Waiting: PR #{} in review", pr_number));
159 } else {
160 output::action("Waiting: PR in review");
161 }
162 println!();
163 println!(" gh pr checks --watch # Wait for CI");
164 println!(" gh pr view --web # Open in browser");
165 }
166 NextAction::Cleanup => {
167 output::action("Next: cleanup merged branch");
168 println!();
169 println!(" mise run git:cleanup");
170 }
171 NextAction::RebaseNeeded => {
172 output::action("Next: rebase on latest main");
173 println!();
174 println!(" git fetch --prune && git rebase origin/main");
175 }
176 NextAction::ResolveDivergence => {
177 output::action("Next: resolve divergence");
178 println!();
179 println!(" # Option 1: Rebase (preferred)");
180 println!(" git fetch --prune && git rebase origin/main");
181 println!();
182 println!(" # Option 2: Force push (if you know what you're doing)");
183 println!(" git push --force-with-lease");
184 }
185 NextAction::PrClosed { pr_number } => {
186 output::action(&format!("PR #{} was closed without merging", pr_number));
187 println!();
188 println!(" # Option 1: Reopen the PR");
189 println!(" gh pr reopen {}", pr_number);
190 println!();
191 println!(" # Option 2: Cleanup and start fresh");
192 println!(" mise run git:cleanup");
193 }
194 NextAction::SyncNeeded { base_branch } => {
195 output::action(&format!("Next: sync (base '{}' was merged)", base_branch));
196 println!();
197 println!(" mise run git:sync");
198 }
199 }
200
201 output::separator();
202 }
203
204 pub fn short_description(&self) -> &'static str {
206 match self {
207 NextAction::StartNewWork => "start new work",
208 NextAction::SyncHomeWithUpstream { .. } => "sync with upstream",
209 NextAction::CommitChanges => "commit changes",
210 NextAction::PushChanges => "push to remote",
211 NextAction::CreatePr => "create PR",
212 NextAction::WaitingForReview { .. } => "waiting for review",
213 NextAction::Cleanup => "cleanup branch",
214 NextAction::RebaseNeeded => "rebase needed",
215 NextAction::ResolveDivergence => "resolve divergence",
216 NextAction::PrClosed { .. } => "PR closed",
217 NextAction::SyncNeeded { .. } => "sync needed",
218 }
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::github::{PrInfo, PrState};
226
227 #[test]
228 fn test_on_home_branch_suggests_start_new_work() {
229 let action = NextAction::detect(
230 "main",
231 "main",
232 &WorkingDirState::Clean,
233 &SyncState::Synced,
234 None,
235 false,
236 None,
237 );
238 assert_eq!(action, NextAction::StartNewWork);
239 }
240
241 #[test]
242 fn test_on_home_branch_behind_suggests_sync() {
243 let action = NextAction::detect(
244 "main",
245 "main",
246 &WorkingDirState::Clean,
247 &SyncState::Behind { count: 5 },
248 None,
249 false,
250 None,
251 );
252 assert_eq!(action, NextAction::SyncHomeWithUpstream { behind_count: 5 });
253 }
254
255 #[test]
256 fn test_uncommitted_changes_suggests_commit() {
257 let action = NextAction::detect(
258 "feature/test",
259 "main",
260 &WorkingDirState::HasUnstagedChanges,
261 &SyncState::Synced,
262 None,
263 true,
264 None,
265 );
266 assert_eq!(action, NextAction::CommitChanges);
267 }
268
269 #[test]
270 fn test_unpushed_commits_suggests_push() {
271 let action = NextAction::detect(
272 "feature/test",
273 "main",
274 &WorkingDirState::Clean,
275 &SyncState::HasUnpushedCommits { count: 2 },
276 None,
277 true,
278 None,
279 );
280 assert_eq!(action, NextAction::PushChanges);
281 }
282
283 #[test]
284 fn test_no_upstream_suggests_push() {
285 let action = NextAction::detect(
286 "feature/test",
287 "main",
288 &WorkingDirState::Clean,
289 &SyncState::NoUpstream,
290 None,
291 false,
292 None,
293 );
294 assert_eq!(action, NextAction::PushChanges);
295 }
296
297 #[test]
298 fn test_pushed_no_pr_suggests_create_pr() {
299 let action = NextAction::detect(
300 "feature/test",
301 "main",
302 &WorkingDirState::Clean,
303 &SyncState::Synced,
304 None,
305 true,
306 None,
307 );
308 assert_eq!(action, NextAction::CreatePr);
309 }
310
311 #[test]
312 fn test_open_pr_suggests_waiting() {
313 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "main");
314 let action = NextAction::detect(
315 "feature/test",
316 "main",
317 &WorkingDirState::Clean,
318 &SyncState::Synced,
319 Some(&pr),
320 true,
321 None,
322 );
323 assert_eq!(action, NextAction::WaitingForReview { pr_number: 42 });
324 }
325
326 #[test]
327 fn test_merged_pr_suggests_cleanup() {
328 let pr = PrInfo::new(
329 42,
330 "Test PR",
331 "https://...",
332 PrState::Merged {
333 method: crate::github::MergeMethod::Squash,
334 merge_commit: None,
335 },
336 "main",
337 );
338 let action = NextAction::detect(
339 "feature/test",
340 "main",
341 &WorkingDirState::Clean,
342 &SyncState::Synced,
343 Some(&pr),
344 true,
345 None,
346 );
347 assert_eq!(action, NextAction::Cleanup);
348 }
349
350 #[test]
351 fn test_closed_pr_suggests_reopen_or_cleanup() {
352 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Closed, "main");
353 let action = NextAction::detect(
354 "feature/test",
355 "main",
356 &WorkingDirState::Clean,
357 &SyncState::Synced,
358 Some(&pr),
359 true,
360 None,
361 );
362 assert_eq!(action, NextAction::PrClosed { pr_number: 42 });
363 }
364
365 #[test]
366 fn test_behind_suggests_rebase() {
367 let action = NextAction::detect(
368 "feature/test",
369 "main",
370 &WorkingDirState::Clean,
371 &SyncState::Behind { count: 3 },
372 None,
373 true,
374 None,
375 );
376 assert_eq!(action, NextAction::RebaseNeeded);
377 }
378
379 #[test]
380 fn test_diverged_suggests_resolve() {
381 let action = NextAction::detect(
382 "feature/test",
383 "main",
384 &WorkingDirState::Clean,
385 &SyncState::Diverged {
386 ahead: 2,
387 behind: 3,
388 },
389 None,
390 true,
391 None,
392 );
393 assert_eq!(action, NextAction::ResolveDivergence);
394 }
395
396 #[test]
397 fn test_uncommitted_changes_takes_priority_over_pr_open() {
398 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "main");
399 let action = NextAction::detect(
400 "feature/test",
401 "main",
402 &WorkingDirState::HasStagedChanges,
403 &SyncState::Synced,
404 Some(&pr),
405 true,
406 None,
407 );
408 assert_eq!(action, NextAction::CommitChanges);
409 }
410
411 #[test]
412 fn test_merged_pr_takes_priority_over_uncommitted_changes() {
413 let pr = PrInfo::new(
414 42,
415 "Test PR",
416 "https://...",
417 PrState::Merged {
418 method: crate::github::MergeMethod::Squash,
419 merge_commit: None,
420 },
421 "main",
422 );
423 let action = NextAction::detect(
424 "feature/test",
425 "main",
426 &WorkingDirState::HasUnstagedChanges,
427 &SyncState::Synced,
428 Some(&pr),
429 true,
430 None,
431 );
432 assert_eq!(action, NextAction::Cleanup);
434 }
435
436 #[test]
437 fn test_base_pr_merged_suggests_sync() {
438 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "feature/base");
439 let action = NextAction::detect(
440 "feature/child",
441 "main",
442 &WorkingDirState::Clean,
443 &SyncState::Synced,
444 Some(&pr),
445 true,
446 Some("feature/base"),
447 );
448 assert_eq!(
449 action,
450 NextAction::SyncNeeded {
451 base_branch: "feature/base".to_string()
452 }
453 );
454 }
455
456 #[test]
457 fn test_base_pr_merged_takes_priority_over_waiting() {
458 let pr = PrInfo::new(42, "Test PR", "https://...", PrState::Open, "feature/base");
459 let action = NextAction::detect(
460 "feature/child",
461 "main",
462 &WorkingDirState::Clean,
463 &SyncState::Synced,
464 Some(&pr),
465 true,
466 Some("feature/base"),
467 );
468 assert_eq!(
470 action,
471 NextAction::SyncNeeded {
472 base_branch: "feature/base".to_string()
473 }
474 );
475 }
476}