git_x/
upstream.rs

1use crate::cli::UpstreamAction;
2use std::collections::HashMap;
3use std::process::Command;
4
5pub fn run(action: UpstreamAction) {
6    match action {
7        UpstreamAction::Set { upstream } => set_upstream(upstream),
8        UpstreamAction::Status => show_upstream_status(),
9        UpstreamAction::SyncAll { dry_run, merge } => sync_all_branches(dry_run, merge),
10    }
11}
12
13fn set_upstream(upstream: String) {
14    // Validate upstream format
15    if let Err(msg) = validate_upstream_format(&upstream) {
16        eprintln!("{}", format_error_message(msg));
17        return;
18    }
19
20    // Check if upstream exists
21    if let Err(msg) = validate_upstream_exists(&upstream) {
22        eprintln!("{}", format_error_message(msg));
23        return;
24    }
25
26    // Get current branch
27    let current_branch = match get_current_branch() {
28        Ok(branch) => branch,
29        Err(msg) => {
30            eprintln!("{}", format_error_message(msg));
31            return;
32        }
33    };
34
35    println!(
36        "{}",
37        format_setting_upstream_message(&current_branch, &upstream)
38    );
39
40    // Set upstream
41    if let Err(msg) = set_branch_upstream(&current_branch, &upstream) {
42        eprintln!("{}", format_error_message(msg));
43        return;
44    }
45
46    println!(
47        "{}",
48        format_upstream_set_message(&current_branch, &upstream)
49    );
50}
51
52fn show_upstream_status() {
53    // Get all local branches
54    let branches = match get_all_local_branches() {
55        Ok(branches) => branches,
56        Err(msg) => {
57            eprintln!("{}", format_error_message(msg));
58            return;
59        }
60    };
61
62    if branches.is_empty() {
63        println!("{}", format_no_branches_message());
64        return;
65    }
66
67    // Get upstream info for each branch
68    let mut branch_upstreams = HashMap::new();
69    for branch in &branches {
70        if let Ok(upstream) = get_branch_upstream(branch) {
71            branch_upstreams.insert(branch.clone(), Some(upstream));
72        } else {
73            branch_upstreams.insert(branch.clone(), None);
74        }
75    }
76
77    // Get current branch for highlighting
78    let current_branch = get_current_branch().unwrap_or_default();
79
80    println!("{}", format_upstream_status_header());
81
82    for branch in &branches {
83        let is_current = branch == &current_branch;
84        let upstream = branch_upstreams.get(branch).unwrap();
85
86        match upstream {
87            Some(upstream_ref) => {
88                // Check sync status
89                let sync_status =
90                    get_branch_sync_status(branch, upstream_ref).unwrap_or(SyncStatus::Unknown);
91
92                println!(
93                    "{}",
94                    format_branch_with_upstream(branch, upstream_ref, &sync_status, is_current)
95                );
96            }
97            None => {
98                println!("{}", format_branch_without_upstream(branch, is_current));
99            }
100        }
101    }
102}
103
104fn sync_all_branches(dry_run: bool, merge: bool) {
105    // Get all branches with upstreams
106    let branches = match get_branches_with_upstreams() {
107        Ok(branches) => branches,
108        Err(msg) => {
109            eprintln!("{}", format_error_message(msg));
110            return;
111        }
112    };
113
114    if branches.is_empty() {
115        println!("{}", format_no_upstream_branches_message());
116        return;
117    }
118
119    println!(
120        "{}",
121        format_sync_all_start_message(branches.len(), dry_run, merge)
122    );
123
124    let mut sync_results = Vec::new();
125
126    for (branch, upstream) in &branches {
127        let sync_status = match get_branch_sync_status(branch, upstream) {
128            Ok(status) => status,
129            Err(_) => {
130                sync_results.push((
131                    branch.clone(),
132                    SyncResult::Error("Failed to get sync status".to_string()),
133                ));
134                continue;
135            }
136        };
137
138        match sync_status {
139            SyncStatus::UpToDate => {
140                sync_results.push((branch.clone(), SyncResult::UpToDate));
141            }
142            SyncStatus::Behind(_) | SyncStatus::Diverged(_, _) => {
143                if dry_run {
144                    sync_results.push((branch.clone(), SyncResult::WouldSync));
145                } else {
146                    match sync_branch_with_upstream(branch, upstream, merge) {
147                        Ok(()) => sync_results.push((branch.clone(), SyncResult::Synced)),
148                        Err(msg) => {
149                            sync_results.push((branch.clone(), SyncResult::Error(msg.to_string())))
150                        }
151                    }
152                }
153            }
154            SyncStatus::Ahead(_) => {
155                sync_results.push((branch.clone(), SyncResult::Ahead));
156            }
157            SyncStatus::Unknown => {
158                sync_results.push((
159                    branch.clone(),
160                    SyncResult::Error("Unknown sync status".to_string()),
161                ));
162            }
163        }
164    }
165
166    // Print results
167    println!("{}", format_sync_results_header());
168    for (branch, result) in &sync_results {
169        println!("{}", format_sync_result_line(branch, result));
170    }
171
172    // Print summary
173    let synced_count = sync_results
174        .iter()
175        .filter(|(_, r)| matches!(r, SyncResult::Synced | SyncResult::WouldSync))
176        .count();
177    println!("{}", format_sync_summary(synced_count, dry_run));
178}
179
180#[derive(Debug, Clone)]
181pub enum SyncStatus {
182    UpToDate,
183    Behind(u32),
184    Ahead(u32),
185    Diverged(u32, u32), // behind, ahead
186    Unknown,
187}
188
189#[derive(Debug, Clone)]
190pub enum SyncResult {
191    UpToDate,
192    Synced,
193    WouldSync,
194    Ahead,
195    Error(String),
196}
197
198// Helper function to validate upstream format
199fn validate_upstream_format(upstream: &str) -> Result<(), &'static str> {
200    if upstream.is_empty() {
201        return Err("Upstream cannot be empty");
202    }
203
204    if !upstream.contains('/') {
205        return Err("Upstream must be in format 'remote/branch' (e.g., origin/main)");
206    }
207
208    let parts: Vec<&str> = upstream.split('/').collect();
209    if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
210        return Err("Invalid upstream format. Use 'remote/branch' format");
211    }
212
213    Ok(())
214}
215
216// Helper function to validate upstream exists
217fn validate_upstream_exists(upstream: &str) -> Result<(), &'static str> {
218    let output = Command::new("git")
219        .args(["rev-parse", "--verify", upstream])
220        .output()
221        .map_err(|_| "Failed to validate upstream")?;
222
223    if !output.status.success() {
224        return Err("Upstream branch does not exist");
225    }
226
227    Ok(())
228}
229
230// Helper function to get current branch
231fn get_current_branch() -> Result<String, &'static str> {
232    let output = Command::new("git")
233        .args(["rev-parse", "--abbrev-ref", "HEAD"])
234        .output()
235        .map_err(|_| "Failed to get current branch")?;
236
237    if !output.status.success() {
238        return Err("Not in a git repository");
239    }
240
241    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
242}
243
244// Helper function to set branch upstream
245fn set_branch_upstream(branch: &str, upstream: &str) -> Result<(), &'static str> {
246    let status = Command::new("git")
247        .args(["branch", "--set-upstream-to", upstream, branch])
248        .status()
249        .map_err(|_| "Failed to set upstream")?;
250
251    if !status.success() {
252        return Err("Failed to set upstream branch");
253    }
254
255    Ok(())
256}
257
258// Helper function to get all local branches
259fn get_all_local_branches() -> Result<Vec<String>, &'static str> {
260    let output = Command::new("git")
261        .args(["branch", "--format=%(refname:short)"])
262        .output()
263        .map_err(|_| "Failed to get local branches")?;
264
265    if !output.status.success() {
266        return Err("Failed to list local branches");
267    }
268
269    let stdout = String::from_utf8_lossy(&output.stdout);
270    let branches: Vec<String> = stdout
271        .lines()
272        .map(|line| line.trim().to_string())
273        .filter(|line| !line.is_empty())
274        .collect();
275
276    Ok(branches)
277}
278
279// Helper function to get branch upstream
280fn get_branch_upstream(branch: &str) -> Result<String, &'static str> {
281    let output = Command::new("git")
282        .args(["rev-parse", "--abbrev-ref", &format!("{branch}@{{u}}")])
283        .output()
284        .map_err(|_| "Failed to get upstream")?;
285
286    if !output.status.success() {
287        return Err("No upstream configured");
288    }
289
290    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
291}
292
293// Helper function to get branch sync status
294fn get_branch_sync_status(branch: &str, upstream: &str) -> Result<SyncStatus, &'static str> {
295    let output = Command::new("git")
296        .args([
297            "rev-list",
298            "--left-right",
299            "--count",
300            &format!("{upstream}...{branch}"),
301        ])
302        .output()
303        .map_err(|_| "Failed to get sync status")?;
304
305    if !output.status.success() {
306        return Err("Failed to compare with upstream");
307    }
308
309    let counts = String::from_utf8_lossy(&output.stdout);
310    let mut parts = counts.split_whitespace();
311
312    let behind: u32 = parts
313        .next()
314        .and_then(|s| s.parse().ok())
315        .ok_or("Invalid sync count format")?;
316
317    let ahead: u32 = parts
318        .next()
319        .and_then(|s| s.parse().ok())
320        .ok_or("Invalid sync count format")?;
321
322    Ok(match (behind, ahead) {
323        (0, 0) => SyncStatus::UpToDate,
324        (b, 0) if b > 0 => SyncStatus::Behind(b),
325        (0, a) if a > 0 => SyncStatus::Ahead(a),
326        (b, a) if b > 0 && a > 0 => SyncStatus::Diverged(b, a),
327        _ => SyncStatus::Unknown,
328    })
329}
330
331// Helper function to get branches with upstreams
332fn get_branches_with_upstreams() -> Result<Vec<(String, String)>, &'static str> {
333    let branches = get_all_local_branches()?;
334    let mut result = Vec::new();
335
336    for branch in branches {
337        if let Ok(upstream) = get_branch_upstream(&branch) {
338            result.push((branch, upstream));
339        }
340    }
341
342    Ok(result)
343}
344
345// Helper function to sync branch with upstream
346fn sync_branch_with_upstream(
347    branch: &str,
348    upstream: &str,
349    merge: bool,
350) -> Result<(), &'static str> {
351    // Switch to the branch first
352    let status = Command::new("git")
353        .args(["checkout", branch])
354        .status()
355        .map_err(|_| "Failed to checkout branch")?;
356
357    if !status.success() {
358        return Err("Failed to checkout branch");
359    }
360
361    // Sync with upstream
362    let args = if merge {
363        ["merge", upstream]
364    } else {
365        ["rebase", upstream]
366    };
367
368    let status = Command::new("git")
369        .args(args)
370        .status()
371        .map_err(|_| "Failed to sync with upstream")?;
372
373    if !status.success() {
374        return Err(if merge {
375            "Merge failed"
376        } else {
377            "Rebase failed"
378        });
379    }
380
381    Ok(())
382}
383
384// Helper function to get git branch set-upstream args
385pub fn get_git_branch_set_upstream_args() -> [&'static str; 2] {
386    ["branch", "--set-upstream-to"]
387}
388
389// Formatting functions
390pub fn format_error_message(msg: &str) -> String {
391    format!("โŒ {msg}")
392}
393
394pub fn format_setting_upstream_message(branch: &str, upstream: &str) -> String {
395    format!("๐Ÿ”— Setting upstream for '{branch}' to '{upstream}'...")
396}
397
398pub fn format_upstream_set_message(branch: &str, upstream: &str) -> String {
399    format!("โœ… Upstream for '{branch}' set to '{upstream}'")
400}
401
402pub fn format_no_branches_message() -> &'static str {
403    "โ„น๏ธ No local branches found"
404}
405
406pub fn format_upstream_status_header() -> &'static str {
407    "๐Ÿ”— Upstream status for all branches:\n"
408}
409
410pub fn format_branch_with_upstream(
411    branch: &str,
412    upstream: &str,
413    sync_status: &SyncStatus,
414    is_current: bool,
415) -> String {
416    let current_indicator = if is_current { "* " } else { "  " };
417    let status_text = match sync_status {
418        SyncStatus::UpToDate => "โœ… up-to-date",
419        SyncStatus::Behind(n) => &format!("โฌ‡๏ธ {n} behind"),
420        SyncStatus::Ahead(n) => &format!("โฌ†๏ธ {n} ahead"),
421        SyncStatus::Diverged(b, a) => &format!("๐Ÿ”€ {b} behind, {a} ahead"),
422        SyncStatus::Unknown => "โ“ unknown",
423    };
424
425    format!("{current_indicator}{branch} -> {upstream} ({status_text})")
426}
427
428pub fn format_branch_without_upstream(branch: &str, is_current: bool) -> String {
429    let current_indicator = if is_current { "* " } else { "  " };
430    format!("{current_indicator}{branch} -> (no upstream)")
431}
432
433pub fn format_no_upstream_branches_message() -> &'static str {
434    "โ„น๏ธ No branches with upstream configuration found"
435}
436
437pub fn format_sync_all_start_message(count: usize, dry_run: bool, merge: bool) -> String {
438    let action = if merge { "merge" } else { "rebase" };
439    if dry_run {
440        format!("๐Ÿงช (dry run) Would sync {count} branch(es) with upstream using {action}:")
441    } else {
442        format!("๐Ÿ”„ Syncing {count} branch(es) with upstream using {action}:")
443    }
444}
445
446pub fn format_sync_results_header() -> &'static str {
447    "\n๐Ÿ“Š Sync results:"
448}
449
450pub fn format_sync_result_line(branch: &str, result: &SyncResult) -> String {
451    match result {
452        SyncResult::UpToDate => format!("  โœ… {branch}: already up-to-date"),
453        SyncResult::Synced => format!("  โœ… {branch}: synced successfully"),
454        SyncResult::WouldSync => format!("  ๐Ÿ”„ {branch}: would be synced"),
455        SyncResult::Ahead => format!("  โฌ†๏ธ {branch}: ahead of upstream (skipped)"),
456        SyncResult::Error(msg) => format!("  โŒ {branch}: {msg}"),
457    }
458}
459
460pub fn format_sync_summary(synced_count: usize, dry_run: bool) -> String {
461    if dry_run {
462        format!(
463            "\n๐Ÿ’ก Would sync {synced_count} branch(es). Run without --dry-run to apply changes."
464        )
465    } else {
466        format!("\nโœ… Synced {synced_count} branch(es) successfully.")
467    }
468}