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