git_x/
sync.rs

1use crate::GitXError;
2use crate::command::Command;
3use crate::core::git::{GitOperations, RemoteOperations};
4use std::process::Command as StdCommand;
5
6pub fn run(merge: bool) -> Result<(), GitXError> {
7    let cmd = SyncCommand;
8    cmd.execute(merge)
9}
10
11/// Command implementation for git sync
12pub struct SyncCommand;
13
14impl Command for SyncCommand {
15    type Input = bool;
16    type Output = ();
17
18    fn execute(&self, merge: bool) -> Result<(), GitXError> {
19        run_sync(merge)
20    }
21
22    fn name(&self) -> &'static str {
23        "sync"
24    }
25
26    fn description(&self) -> &'static str {
27        "Sync current branch with its upstream"
28    }
29}
30
31fn run_sync(merge: bool) -> Result<(), GitXError> {
32    // Get current branch
33    let current_branch = GitOperations::current_branch()
34        .map_err(|e| GitXError::GitCommand(format!("Failed to get current branch: {e}")))?;
35
36    // Get upstream branch
37    let upstream = GitOperations::upstream_branch()
38        .map_err(|e| GitXError::GitCommand(format!("Failed to get upstream branch: {e}")))?;
39
40    println!(
41        "🔄 Syncing branch '{}' with '{}'...",
42        &current_branch, &upstream
43    );
44
45    // Fetch from remote
46    let remote = upstream.split('/').next().unwrap_or("origin");
47    RemoteOperations::fetch(Some(remote))
48        .map_err(|e| GitXError::GitCommand(format!("Failed to fetch from remote: {e}")))?;
49
50    // Check if we're ahead of upstream
51    let (ahead, behind) = GitOperations::ahead_behind_counts()
52        .map_err(|e| GitXError::GitCommand(format!("Failed to get ahead/behind counts: {e}")))?;
53
54    let status = match (behind, ahead) {
55        (0, 0) => SyncStatus::UpToDate,
56        (b, 0) if b > 0 => SyncStatus::Behind(b),
57        (0, a) if a > 0 => SyncStatus::Ahead(a),
58        (b, a) if b > 0 && a > 0 => SyncStatus::Diverged(b, a),
59        _ => SyncStatus::UpToDate,
60    };
61
62    match status {
63        SyncStatus::UpToDate => {
64            println!("✅ Branch is up to date with upstream");
65        }
66        SyncStatus::Behind(count) => {
67            println!("{count}");
68            let args = if merge {
69                vec!["merge", &upstream]
70            } else {
71                vec!["rebase", &upstream]
72            };
73
74            match GitOperations::run_status(&args) {
75                Err(e) => return Err(GitXError::GitCommand(format!("Sync failed: {e}"))),
76                Ok(()) => println!("{}", format_sync_success_message(merge)),
77            }
78        }
79        SyncStatus::Ahead(count) => {
80            println!("{count}");
81        }
82        SyncStatus::Diverged(behind, ahead) => {
83            println!("🔀 Branch has diverged: {behind} behind, {ahead} ahead");
84            if merge {
85                let args = if merge {
86                    vec!["merge", &upstream]
87                } else {
88                    vec!["rebase", &upstream]
89                };
90
91                match GitOperations::run_status(&args) {
92                    Err(e) => return Err(GitXError::GitCommand(format!("Sync failed: {e}"))),
93                    Ok(()) => println!("{}", format_sync_success_message(merge)),
94                }
95            } else {
96                println!("💡 Use --merge flag to merge changes, or handle manually");
97            }
98        }
99    }
100
101    Ok(())
102}
103
104#[derive(Debug, PartialEq)]
105pub enum SyncStatus {
106    UpToDate,
107    Behind(u32),
108    Ahead(u32),
109    Diverged(u32, u32), // behind, ahead
110}
111
112pub fn get_upstream_branch(branch: &str) -> Result<String, &'static str> {
113    let output = StdCommand::new("git")
114        .args(["rev-parse", "--abbrev-ref", &format!("{branch}@{{u}}")])
115        .output()
116        .map_err(|_| "Failed to get upstream branch")?;
117
118    if !output.status.success() {
119        return Err("No upstream branch configured");
120    }
121
122    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
123}
124
125pub fn fetch_upstream(upstream: &str) -> Result<(), &'static str> {
126    let remote = upstream.split('/').next().unwrap_or("origin");
127
128    let status = StdCommand::new("git")
129        .args(["fetch", remote])
130        .status()
131        .map_err(|_| "Failed to execute fetch command")?;
132
133    if !status.success() {
134        return Err("Failed to fetch from remote");
135    }
136
137    Ok(())
138}
139
140pub fn get_sync_status(branch: &str, upstream: &str) -> Result<SyncStatus, &'static str> {
141    let output = StdCommand::new("git")
142        .args([
143            "rev-list",
144            "--left-right",
145            "--count",
146            &format!("{upstream}...{branch}"),
147        ])
148        .output()
149        .map_err(|_| "Failed to get sync status")?;
150
151    if !output.status.success() {
152        return Err("Failed to compare with upstream");
153    }
154
155    let counts = String::from_utf8_lossy(&output.stdout);
156    let (behind, ahead) = parse_sync_counts(&counts)?;
157
158    Ok(match (behind, ahead) {
159        (0, 0) => SyncStatus::UpToDate,
160        (b, 0) if b > 0 => SyncStatus::Behind(b),
161        (0, a) if a > 0 => SyncStatus::Ahead(a),
162        (b, a) if b > 0 && a > 0 => SyncStatus::Diverged(b, a),
163        _ => SyncStatus::UpToDate,
164    })
165}
166
167pub fn parse_sync_counts(output: &str) -> Result<(u32, u32), &'static str> {
168    let mut parts = output.split_whitespace();
169    let behind = parts
170        .next()
171        .and_then(|s| s.parse().ok())
172        .ok_or("Invalid sync count format")?;
173    let ahead = parts
174        .next()
175        .and_then(|s| s.parse().ok())
176        .ok_or("Invalid sync count format")?;
177
178    Ok((behind, ahead))
179}
180
181pub fn sync_with_upstream(upstream: &str, merge: bool) -> Result<(), &'static str> {
182    let args = if merge {
183        ["merge", upstream]
184    } else {
185        ["rebase", upstream]
186    };
187
188    let status = StdCommand::new("git")
189        .args(args)
190        .status()
191        .map_err(|_| "Failed to execute sync command")?;
192
193    if !status.success() {
194        return Err(if merge {
195            "Merge failed"
196        } else {
197            "Rebase failed"
198        });
199    }
200
201    Ok(())
202}
203
204// Helper function to format sync success message
205pub fn format_sync_success_message(merge: bool) -> String {
206    if merge {
207        "✅ Successfully merged upstream changes".to_string()
208    } else {
209        "✅ Successfully rebased onto upstream".to_string()
210    }
211}