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 if let Err(msg) = validate_upstream_format(&upstream) {
16 eprintln!("{}", format_error_message(msg));
17 return;
18 }
19
20 if let Err(msg) = validate_upstream_exists(&upstream) {
22 eprintln!("{}", format_error_message(msg));
23 return;
24 }
25
26 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(¤t_branch, &upstream)
38 );
39
40 if let Err(msg) = set_branch_upstream(¤t_branch, &upstream) {
42 eprintln!("{}", format_error_message(msg));
43 return;
44 }
45
46 println!(
47 "{}",
48 format_upstream_set_message(¤t_branch, &upstream)
49 );
50}
51
52fn show_upstream_status() {
53 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 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 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 == ¤t_branch;
84 let upstream = branch_upstreams.get(branch).unwrap();
85
86 match upstream {
87 Some(upstream_ref) => {
88 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 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 println!("{}", format_sync_results_header());
168 for (branch, result) in &sync_results {
169 println!("{}", format_sync_result_line(branch, result));
170 }
171
172 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), Unknown,
187}
188
189#[derive(Debug, Clone)]
190pub enum SyncResult {
191 UpToDate,
192 Synced,
193 WouldSync,
194 Ahead,
195 Error(String),
196}
197
198fn 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
216fn 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
230fn 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
244fn 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
258fn 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
279fn 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
293fn 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
331fn 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
345fn sync_branch_with_upstream(
347 branch: &str,
348 upstream: &str,
349 merge: bool,
350) -> Result<(), &'static str> {
351 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 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
384pub fn get_git_branch_set_upstream_args() -> [&'static str; 2] {
386 ["branch", "--set-upstream-to"]
387}
388
389pub 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}