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!("{}", format_setting_upstream_message(¤t_branch, &upstream));
36
37 if let Err(msg) = set_branch_upstream(¤t_branch, &upstream) {
39 eprintln!("{}", format_error_message(msg));
40 return;
41 }
42
43 println!("{}", format_upstream_set_message(¤t_branch, &upstream));
44}
45
46fn show_upstream_status() {
47 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 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 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 == ¤t_branch;
78 let upstream = branch_upstreams.get(branch).unwrap();
79
80 match upstream {
81 Some(upstream_ref) => {
82 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 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 println!("{}", format_sync_results_header());
148 for (branch, result) in &sync_results {
149 println!("{}", format_sync_result_line(branch, result));
150 }
151
152 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), Unknown,
164}
165
166#[derive(Debug, Clone)]
167pub enum SyncResult {
168 UpToDate,
169 Synced,
170 WouldSync,
171 Ahead,
172 Error(String),
173}
174
175fn 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
193fn 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
207fn 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
221fn 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
235fn 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
256fn 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
270fn 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
303fn 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
317fn sync_branch_with_upstream(branch: &str, upstream: &str, merge: bool) -> Result<(), &'static str> {
319 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 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
348pub fn get_git_branch_set_upstream_args() -> [&'static str; 2] {
350 ["branch", "--set-upstream-to"]
351}
352
353pub 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}