Skip to main content

git_workty/commands/
sync.rs

1use crate::git::GitRepo;
2use crate::status::{get_all_statuses, is_worktree_dirty};
3use crate::ui;
4use crate::worktree::list_worktrees;
5use anyhow::{Context, Result};
6use std::process::Command;
7
8pub struct SyncOptions {
9    pub dry_run: bool,
10    pub fetch: bool,
11}
12
13pub fn execute(repo: &GitRepo, opts: SyncOptions) -> Result<()> {
14    // Optionally fetch first
15    if opts.fetch {
16        ui::print_info("Fetching from origin...");
17        let output = Command::new("git")
18            .current_dir(&repo.root)
19            .args(["fetch", "--prune", "origin"])
20            .output()
21            .context("Failed to fetch")?;
22
23        if !output.status.success() {
24            ui::print_warning("Failed to fetch from origin");
25        }
26    }
27
28    let worktrees = list_worktrees(repo)?;
29    let statuses = get_all_statuses(repo, &worktrees);
30
31    let mut synced = 0;
32    let mut skipped_dirty = 0;
33    let mut skipped_no_upstream = 0;
34    let mut failed = 0;
35
36    for (wt, status) in &statuses {
37        // Skip main worktree (usually you don't want to auto-rebase main)
38        if wt.is_main_worktree(repo) {
39            continue;
40        }
41
42        // Skip detached HEAD
43        if wt.detached {
44            continue;
45        }
46
47        // Skip if no upstream
48        if status.upstream.is_none() {
49            skipped_no_upstream += 1;
50            continue;
51        }
52
53        // Skip if nothing to sync
54        let behind = status.behind.unwrap_or(0);
55        if behind == 0 {
56            continue;
57        }
58
59        let branch_name = wt.name();
60
61        // Skip if dirty
62        if is_worktree_dirty(wt) {
63            if !opts.dry_run {
64                ui::print_warning(&format!("{}: skipped (dirty)", branch_name));
65            }
66            skipped_dirty += 1;
67            continue;
68        }
69
70        if opts.dry_run {
71            ui::print_info(&format!(
72                "{}: would rebase ({} commits behind)",
73                branch_name, behind
74            ));
75            synced += 1;
76            continue;
77        }
78
79        // Perform the rebase
80        ui::print_info(&format!("{}: rebasing...", branch_name));
81
82        let output = Command::new("git")
83            .current_dir(&wt.path)
84            .args(["rebase"])
85            .output()
86            .context("Failed to run git rebase")?;
87
88        if output.status.success() {
89            ui::print_success(&format!("{}: rebased", branch_name));
90            synced += 1;
91        } else {
92            // Abort the failed rebase
93            let _ = Command::new("git")
94                .current_dir(&wt.path)
95                .args(["rebase", "--abort"])
96                .output();
97
98            ui::print_warning(&format!("{}: rebase failed, aborted", branch_name));
99            failed += 1;
100        }
101    }
102
103    // Summary
104    println!();
105    if opts.dry_run {
106        ui::print_info(&format!("Would sync {} worktree(s)", synced));
107    } else {
108        ui::print_info(&format!(
109            "Synced: {}, Skipped (dirty): {}, Skipped (no upstream): {}, Failed: {}",
110            synced, skipped_dirty, skipped_no_upstream, failed
111        ));
112    }
113
114    Ok(())
115}