diff options
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/list.rs | 584 |
1 files changed, 583 insertions, 1 deletions
diff --git a/src/lib/list.rs b/src/lib/list.rs index 733936a..b4d6a5e 100644 --- a/src/lib/list.rs +++ b/src/lib/list.rs | |||
| @@ -10,9 +10,40 @@ use crate::{ | |||
| 10 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, | 10 | nostr_url::{CloneUrl, NostrUrlDecoded, ServerProtocol}, |
| 11 | }, | 11 | }, |
| 12 | repo_ref::is_grasp_server_clone_url, | 12 | repo_ref::is_grasp_server_clone_url, |
| 13 | utils::{Direction, get_read_protocols_to_try, join_with_and, set_protocol_preference}, | 13 | repo_state::RepoState, |
| 14 | utils::{ | ||
| 15 | Direction, get_read_protocols_to_try, get_short_git_server_name, join_with_and, | ||
| 16 | set_protocol_preference, | ||
| 17 | }, | ||
| 14 | }; | 18 | }; |
| 15 | 19 | ||
| 20 | /// Sync issues identified for a single remote | ||
| 21 | #[derive(Default, Debug, Clone)] | ||
| 22 | pub struct RemoteIssues { | ||
| 23 | pub branches_out_of_sync: Vec<(String, Option<(usize, usize)>)>, // (ref, (ahead, behind)) | ||
| 24 | pub branches_missing: Vec<String>, | ||
| 25 | pub tags_out_of_sync: Vec<String>, | ||
| 26 | pub tags_missing: Vec<String>, | ||
| 27 | } | ||
| 28 | |||
| 29 | impl RemoteIssues { | ||
| 30 | /// Returns true if there are no issues | ||
| 31 | pub fn is_empty(&self) -> bool { | ||
| 32 | self.branches_out_of_sync.is_empty() | ||
| 33 | && self.branches_missing.is_empty() | ||
| 34 | && self.tags_out_of_sync.is_empty() | ||
| 35 | && self.tags_missing.is_empty() | ||
| 36 | } | ||
| 37 | |||
| 38 | /// Returns the total count of all issues | ||
| 39 | pub fn total_count(&self) -> usize { | ||
| 40 | self.branches_out_of_sync.len() | ||
| 41 | + self.branches_missing.len() | ||
| 42 | + self.tags_out_of_sync.len() | ||
| 43 | + self.tags_missing.len() | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 16 | pub fn list_from_remotes( | 47 | pub fn list_from_remotes( |
| 17 | term: &console::Term, | 48 | term: &console::Term, |
| 18 | git_repo: &Repo, | 49 | git_repo: &Repo, |
| @@ -182,3 +213,554 @@ pub fn get_ahead_behind( | |||
| 182 | let latest = git_repo.get_commit_or_tip_of_reference(latest_ref_or_oid)?; | 213 | let latest = git_repo.get_commit_or_tip_of_reference(latest_ref_or_oid)?; |
| 183 | git_repo.get_commits_ahead_behind(&base, &latest) | 214 | git_repo.get_commits_ahead_behind(&base, &latest) |
| 184 | } | 215 | } |
| 216 | |||
| 217 | /// Identify sync discrepancies between nostr state and remote git servers | ||
| 218 | /// | ||
| 219 | /// This function analyzes the differences between the expected state (from | ||
| 220 | /// nostr) and the actual state on each remote git server, categorizing issues | ||
| 221 | /// by type (branches/tags, out of sync/missing). | ||
| 222 | /// | ||
| 223 | /// # Arguments | ||
| 224 | /// * `git_repo` - The local git repository | ||
| 225 | /// * `nostr_state` - The expected state from nostr | ||
| 226 | /// * `remote_states` - Map of remote URLs to their states and whether they're | ||
| 227 | /// grasp servers | ||
| 228 | /// | ||
| 229 | /// # Returns | ||
| 230 | /// A HashMap mapping remote names to their identified sync issues | ||
| 231 | pub fn identify_remote_sync_issues( | ||
| 232 | git_repo: &Repo, | ||
| 233 | nostr_state: &RepoState, | ||
| 234 | remote_states: &HashMap<String, (HashMap<String, String>, bool)>, | ||
| 235 | ) -> HashMap<String, RemoteIssues> { | ||
| 236 | let mut remote_issues: HashMap<String, RemoteIssues> = HashMap::new(); | ||
| 237 | |||
| 238 | for (name, value) in &nostr_state.state { | ||
| 239 | for (url, (remote_state, _is_grasp_server)) in remote_states { | ||
| 240 | let remote_name = get_short_git_server_name(git_repo, url); | ||
| 241 | let issues = remote_issues.entry(remote_name.clone()).or_default(); | ||
| 242 | |||
| 243 | let is_branch = name.starts_with("refs/heads/"); | ||
| 244 | let is_tag = name.starts_with("refs/tags/"); | ||
| 245 | |||
| 246 | if let Some(remote_value) = remote_state.get(name) { | ||
| 247 | if value.ne(remote_value) { | ||
| 248 | if is_branch { | ||
| 249 | // Calculate ahead/behind for branches | ||
| 250 | let ahead_behind = get_ahead_behind(git_repo, value, remote_value) | ||
| 251 | .ok() | ||
| 252 | .map(|(ahead, behind)| (ahead.len(), behind.len())); | ||
| 253 | issues | ||
| 254 | .branches_out_of_sync | ||
| 255 | .push((name.clone(), ahead_behind)); | ||
| 256 | } else if is_tag { | ||
| 257 | issues.tags_out_of_sync.push(name.clone()); | ||
| 258 | } | ||
| 259 | } | ||
| 260 | } else if is_branch { | ||
| 261 | issues.branches_missing.push(name.clone()); | ||
| 262 | } else if is_tag { | ||
| 263 | issues.tags_missing.push(name.clone()); | ||
| 264 | } | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | remote_issues | ||
| 269 | } | ||
| 270 | |||
| 271 | /// Format a list of refs with ahead/behind information into a user-friendly | ||
| 272 | /// issue summary | ||
| 273 | /// | ||
| 274 | /// # Arguments | ||
| 275 | /// * `refs` - List of refs with optional ahead/behind counts | ||
| 276 | /// * `singular` - Singular form of the ref type (e.g., "branch") | ||
| 277 | /// * `plural` - Plural form of the ref type (e.g., "branches") | ||
| 278 | /// * `status` - Status description (e.g., "out of sync", "missing") | ||
| 279 | /// * `is_branch` - Whether these are branches (affects formatting) | ||
| 280 | /// | ||
| 281 | /// # Returns | ||
| 282 | /// A formatted string describing the issue | ||
| 283 | pub fn format_ref_issue( | ||
| 284 | refs: &[(String, Option<(usize, usize)>)], | ||
| 285 | _singular: &str, | ||
| 286 | plural: &str, | ||
| 287 | status: &str, | ||
| 288 | is_branch: bool, | ||
| 289 | ) -> String { | ||
| 290 | let count = refs.len(); | ||
| 291 | |||
| 292 | /// Helper to format branch name with ahead/behind info | ||
| 293 | fn format_branch_with_sync(name: &str, ahead_behind: &Option<(usize, usize)>) -> String { | ||
| 294 | if let Some((ahead, behind)) = ahead_behind { | ||
| 295 | if *ahead > 0 && *behind > 0 { | ||
| 296 | format!("{} ({} behind, {} ahead)", name, behind, ahead) | ||
| 297 | } else if *behind > 0 { | ||
| 298 | format!("{} ({} behind)", name, behind) | ||
| 299 | } else if *ahead > 0 { | ||
| 300 | format!("{} ({} ahead)", name, ahead) | ||
| 301 | } else { | ||
| 302 | name.to_string() | ||
| 303 | } | ||
| 304 | } else { | ||
| 305 | name.to_string() | ||
| 306 | } | ||
| 307 | } | ||
| 308 | |||
| 309 | if count == 1 { | ||
| 310 | // Single item: name the ref with ahead/behind info | ||
| 311 | let clean_ref = refs[0] | ||
| 312 | .0 | ||
| 313 | .strip_prefix("refs/heads/") | ||
| 314 | .or_else(|| refs[0].0.strip_prefix("refs/tags/")) | ||
| 315 | .unwrap_or(&refs[0].0); | ||
| 316 | let formatted = if is_branch { | ||
| 317 | format_branch_with_sync(clean_ref, &refs[0].1) | ||
| 318 | } else { | ||
| 319 | clean_ref.to_string() | ||
| 320 | }; | ||
| 321 | format!("{} {}", formatted, status) | ||
| 322 | } else if is_branch && count <= 3 { | ||
| 323 | // For branches: list up to 3 names with ahead/behind info | ||
| 324 | let names: Vec<_> = refs | ||
| 325 | .iter() | ||
| 326 | .map(|(r, ab)| { | ||
| 327 | let clean = r.strip_prefix("refs/heads/").unwrap_or(r); | ||
| 328 | format_branch_with_sync(clean, ab) | ||
| 329 | }) | ||
| 330 | .collect(); | ||
| 331 | if count == 2 { | ||
| 332 | format!("{} and {} {}", names[0], names[1], status) | ||
| 333 | } else { | ||
| 334 | format!("{}, {} and {} {}", names[0], names[1], names[2], status) | ||
| 335 | } | ||
| 336 | } else if is_branch && count > 3 { | ||
| 337 | // For many branches: list first 2 with ahead/behind and count others | ||
| 338 | let names: Vec<_> = refs | ||
| 339 | .iter() | ||
| 340 | .take(2) | ||
| 341 | .map(|(r, ab)| { | ||
| 342 | let clean = r.strip_prefix("refs/heads/").unwrap_or(r); | ||
| 343 | format_branch_with_sync(clean, ab) | ||
| 344 | }) | ||
| 345 | .collect(); | ||
| 346 | let other_count = count - 2; | ||
| 347 | let other = if other_count == 1 { | ||
| 348 | "1 other".to_string() | ||
| 349 | } else { | ||
| 350 | format!("{} others", other_count) | ||
| 351 | }; | ||
| 352 | format!("{}, {} and {} {}", names[0], names[1], other, status) | ||
| 353 | } else { | ||
| 354 | // For tags: just count | ||
| 355 | format!("{} {} {}", count, plural, status) | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | /// Format a list of refs (String only) into a user-friendly issue summary | ||
| 360 | /// | ||
| 361 | /// # Arguments | ||
| 362 | /// * `refs` - List of ref names | ||
| 363 | /// * `singular` - Singular form of the ref type (e.g., "branch") | ||
| 364 | /// * `plural` - Plural form of the ref type (e.g., "branches") | ||
| 365 | /// * `status` - Status description (e.g., "out of sync", "missing") | ||
| 366 | /// * `is_branch` - Whether these are branches (affects formatting) | ||
| 367 | /// | ||
| 368 | /// # Returns | ||
| 369 | /// A formatted string describing the issue | ||
| 370 | pub fn format_ref_issue_simple( | ||
| 371 | refs: &[String], | ||
| 372 | _singular: &str, | ||
| 373 | plural: &str, | ||
| 374 | status: &str, | ||
| 375 | is_branch: bool, | ||
| 376 | ) -> String { | ||
| 377 | let count = refs.len(); | ||
| 378 | |||
| 379 | if count == 1 { | ||
| 380 | // Single item: name the ref | ||
| 381 | let clean_ref = refs[0] | ||
| 382 | .strip_prefix("refs/heads/") | ||
| 383 | .or_else(|| refs[0].strip_prefix("refs/tags/")) | ||
| 384 | .unwrap_or(&refs[0]); | ||
| 385 | format!("{} {}", clean_ref, status) | ||
| 386 | } else if is_branch && count <= 3 { | ||
| 387 | // For branches: list up to 3 names | ||
| 388 | let names: Vec<_> = refs | ||
| 389 | .iter() | ||
| 390 | .map(|r| r.strip_prefix("refs/heads/").unwrap_or(r)) | ||
| 391 | .collect(); | ||
| 392 | if count == 2 { | ||
| 393 | format!("{} and {} {}", names[0], names[1], status) | ||
| 394 | } else { | ||
| 395 | format!("{}, {} and {} {}", names[0], names[1], names[2], status) | ||
| 396 | } | ||
| 397 | } else if is_branch && count > 3 { | ||
| 398 | // For many branches: list first 2 and count others | ||
| 399 | let names: Vec<_> = refs | ||
| 400 | .iter() | ||
| 401 | .take(2) | ||
| 402 | .map(|r| r.strip_prefix("refs/heads/").unwrap_or(r)) | ||
| 403 | .collect(); | ||
| 404 | let other_count = count - 2; | ||
| 405 | let other = if other_count == 1 { | ||
| 406 | "1 other".to_string() | ||
| 407 | } else { | ||
| 408 | format!("{} others", other_count) | ||
| 409 | }; | ||
| 410 | format!("{}, {} and {} {}", names[0], names[1], other, status) | ||
| 411 | } else { | ||
| 412 | // For tags: just count | ||
| 413 | format!("{} {} {}", count, plural, status) | ||
| 414 | } | ||
| 415 | } | ||
| 416 | |||
| 417 | /// Generate warning messages for remote sync issues | ||
| 418 | pub fn generate_remote_sync_warnings( | ||
| 419 | git_repo: &Repo, | ||
| 420 | remote_issues: &HashMap<String, RemoteIssues>, | ||
| 421 | remote_states: &HashMap<String, (HashMap<String, String>, bool)>, | ||
| 422 | ) -> Vec<String> { | ||
| 423 | let mut warnings = Vec::new(); | ||
| 424 | |||
| 425 | for (remote_name, issues) in remote_issues { | ||
| 426 | if issues.is_empty() { | ||
| 427 | continue; | ||
| 428 | } | ||
| 429 | |||
| 430 | // Find remote state for this remote | ||
| 431 | let remote_state = remote_states | ||
| 432 | .iter() | ||
| 433 | .find(|(url, _)| &get_short_git_server_name(git_repo, url) == remote_name) | ||
| 434 | .map(|(_, (state, _))| state); | ||
| 435 | |||
| 436 | if let Some(state) = remote_state { | ||
| 437 | // Check if remote is completely empty | ||
| 438 | if state.is_empty() { | ||
| 439 | warnings.push(format!("WARNING: {remote_name} has no data.")); | ||
| 440 | continue; | ||
| 441 | } | ||
| 442 | |||
| 443 | // Check if remote only has a few branches and missing many | ||
| 444 | let remote_branches: Vec<_> = state | ||
| 445 | .keys() | ||
| 446 | .filter(|k| k.starts_with("refs/heads/")) | ||
| 447 | .map(|b| b.strip_prefix("refs/heads/").unwrap_or(b)) | ||
| 448 | .collect(); | ||
| 449 | |||
| 450 | if remote_branches.len() <= 3 && issues.branches_missing.len() >= 5 { | ||
| 451 | let sync_status = if issues.branches_out_of_sync.is_empty() { | ||
| 452 | "" | ||
| 453 | } else { | ||
| 454 | " and they are out of sync" | ||
| 455 | }; | ||
| 456 | |||
| 457 | warnings.push(format!( | ||
| 458 | "WARNING: {remote_name} only has {} branches{}", | ||
| 459 | remote_branches.join(", "), | ||
| 460 | sync_status | ||
| 461 | )); | ||
| 462 | continue; | ||
| 463 | } | ||
| 464 | } | ||
| 465 | |||
| 466 | // Build summary message parts | ||
| 467 | let mut parts = Vec::new(); | ||
| 468 | |||
| 469 | if !issues.branches_out_of_sync.is_empty() { | ||
| 470 | parts.push(format_ref_issue( | ||
| 471 | &issues.branches_out_of_sync, | ||
| 472 | "branch", | ||
| 473 | "branches", | ||
| 474 | "out of sync", | ||
| 475 | true, | ||
| 476 | )); | ||
| 477 | } | ||
| 478 | |||
| 479 | if !issues.branches_missing.is_empty() { | ||
| 480 | parts.push(format_ref_issue_simple( | ||
| 481 | &issues.branches_missing, | ||
| 482 | "branch", | ||
| 483 | "branches", | ||
| 484 | "missing", | ||
| 485 | true, | ||
| 486 | )); | ||
| 487 | } | ||
| 488 | |||
| 489 | if !issues.tags_out_of_sync.is_empty() { | ||
| 490 | parts.push(format_ref_issue_simple( | ||
| 491 | &issues.tags_out_of_sync, | ||
| 492 | "tag", | ||
| 493 | "tags", | ||
| 494 | "out of sync", | ||
| 495 | false, | ||
| 496 | )); | ||
| 497 | } | ||
| 498 | |||
| 499 | if !issues.tags_missing.is_empty() { | ||
| 500 | parts.push(format_ref_issue_simple( | ||
| 501 | &issues.tags_missing, | ||
| 502 | "tag", | ||
| 503 | "tags", | ||
| 504 | "missing", | ||
| 505 | false, | ||
| 506 | )); | ||
| 507 | } | ||
| 508 | |||
| 509 | if !parts.is_empty() { | ||
| 510 | warnings.push(format!( | ||
| 511 | "WARNING: {remote_name} is out of sync. {}", | ||
| 512 | parts.join(". ") | ||
| 513 | )); | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 517 | warnings | ||
| 518 | } | ||
| 519 | |||
| 520 | #[cfg(test)] | ||
| 521 | mod tests { | ||
| 522 | use super::*; | ||
| 523 | |||
| 524 | #[test] | ||
| 525 | fn test_format_ref_issue_single_branch_with_ahead_behind() { | ||
| 526 | let refs = vec![("refs/heads/main".to_string(), Some((5, 3)))]; | ||
| 527 | assert_eq!( | ||
| 528 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 529 | "main (3 behind, 5 ahead) out of sync" | ||
| 530 | ); | ||
| 531 | } | ||
| 532 | |||
| 533 | #[test] | ||
| 534 | fn test_format_ref_issue_single_branch_only_behind() { | ||
| 535 | let refs = vec![("refs/heads/feature".to_string(), Some((0, 7)))]; | ||
| 536 | assert_eq!( | ||
| 537 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 538 | "feature (7 behind) out of sync" | ||
| 539 | ); | ||
| 540 | } | ||
| 541 | |||
| 542 | #[test] | ||
| 543 | fn test_format_ref_issue_single_branch_only_ahead() { | ||
| 544 | let refs = vec![("refs/heads/dev".to_string(), Some((4, 0)))]; | ||
| 545 | assert_eq!( | ||
| 546 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 547 | "dev (4 ahead) out of sync" | ||
| 548 | ); | ||
| 549 | } | ||
| 550 | |||
| 551 | #[test] | ||
| 552 | fn test_format_ref_issue_single_branch_no_diff() { | ||
| 553 | let refs = vec![("refs/heads/main".to_string(), Some((0, 0)))]; | ||
| 554 | assert_eq!( | ||
| 555 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 556 | "main out of sync" | ||
| 557 | ); | ||
| 558 | } | ||
| 559 | |||
| 560 | #[test] | ||
| 561 | fn test_format_ref_issue_single_branch_no_ahead_behind_info() { | ||
| 562 | let refs = vec![("refs/heads/main".to_string(), None)]; | ||
| 563 | assert_eq!( | ||
| 564 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 565 | "main out of sync" | ||
| 566 | ); | ||
| 567 | } | ||
| 568 | |||
| 569 | #[test] | ||
| 570 | fn test_format_ref_issue_two_branches() { | ||
| 571 | let refs = vec![ | ||
| 572 | ("refs/heads/main".to_string(), Some((2, 1))), | ||
| 573 | ("refs/heads/dev".to_string(), Some((0, 3))), | ||
| 574 | ]; | ||
| 575 | assert_eq!( | ||
| 576 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 577 | "main (1 behind, 2 ahead) and dev (3 behind) out of sync" | ||
| 578 | ); | ||
| 579 | } | ||
| 580 | |||
| 581 | #[test] | ||
| 582 | fn test_format_ref_issue_three_branches() { | ||
| 583 | let refs = vec![ | ||
| 584 | ("refs/heads/main".to_string(), Some((1, 0))), | ||
| 585 | ("refs/heads/dev".to_string(), Some((0, 2))), | ||
| 586 | ("refs/heads/feature".to_string(), None), | ||
| 587 | ]; | ||
| 588 | assert_eq!( | ||
| 589 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 590 | "main (1 ahead), dev (2 behind) and feature out of sync" | ||
| 591 | ); | ||
| 592 | } | ||
| 593 | |||
| 594 | #[test] | ||
| 595 | fn test_format_ref_issue_many_branches() { | ||
| 596 | let refs = vec![ | ||
| 597 | ("refs/heads/main".to_string(), Some((5, 3))), | ||
| 598 | ("refs/heads/dev".to_string(), Some((0, 1))), | ||
| 599 | ("refs/heads/feature1".to_string(), None), | ||
| 600 | ("refs/heads/feature2".to_string(), Some((2, 0))), | ||
| 601 | ]; | ||
| 602 | assert_eq!( | ||
| 603 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 604 | "main (3 behind, 5 ahead), dev (1 behind) and 2 others out of sync" | ||
| 605 | ); | ||
| 606 | } | ||
| 607 | |||
| 608 | #[test] | ||
| 609 | fn test_format_ref_issue_many_branches_singular_other() { | ||
| 610 | let refs = vec![ | ||
| 611 | ("refs/heads/main".to_string(), Some((1, 1))), | ||
| 612 | ("refs/heads/dev".to_string(), Some((2, 2))), | ||
| 613 | ("refs/heads/feature".to_string(), None), | ||
| 614 | ]; | ||
| 615 | // With 3 branches, it should list all 3 | ||
| 616 | assert_eq!( | ||
| 617 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 618 | "main (1 behind, 1 ahead), dev (2 behind, 2 ahead) and feature out of sync" | ||
| 619 | ); | ||
| 620 | |||
| 621 | // With 4 branches (show 2, then "2 others") | ||
| 622 | let refs = vec![ | ||
| 623 | ("refs/heads/main".to_string(), Some((1, 1))), | ||
| 624 | ("refs/heads/dev".to_string(), Some((2, 2))), | ||
| 625 | ("refs/heads/feature1".to_string(), None), | ||
| 626 | ("refs/heads/feature2".to_string(), None), | ||
| 627 | ]; | ||
| 628 | assert_eq!( | ||
| 629 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 630 | "main (1 behind, 1 ahead), dev (2 behind, 2 ahead) and 2 others out of sync" | ||
| 631 | ); | ||
| 632 | } | ||
| 633 | |||
| 634 | #[test] | ||
| 635 | fn test_format_ref_issue_single_tag() { | ||
| 636 | let refs = vec![("refs/tags/v1.0.0".to_string(), None)]; | ||
| 637 | assert_eq!( | ||
| 638 | format_ref_issue(&refs, "tag", "tags", "out of sync", false), | ||
| 639 | "v1.0.0 out of sync" | ||
| 640 | ); | ||
| 641 | } | ||
| 642 | |||
| 643 | #[test] | ||
| 644 | fn test_format_ref_issue_multiple_tags() { | ||
| 645 | let refs = vec![ | ||
| 646 | ("refs/tags/v1.0.0".to_string(), None), | ||
| 647 | ("refs/tags/v1.0.1".to_string(), None), | ||
| 648 | ("refs/tags/v2.0.0".to_string(), None), | ||
| 649 | ]; | ||
| 650 | assert_eq!( | ||
| 651 | format_ref_issue(&refs, "tag", "tags", "out of sync", false), | ||
| 652 | "3 tags out of sync" | ||
| 653 | ); | ||
| 654 | } | ||
| 655 | |||
| 656 | #[test] | ||
| 657 | fn test_format_ref_issue_simple_single_branch() { | ||
| 658 | let refs = vec!["refs/heads/main".to_string()]; | ||
| 659 | assert_eq!( | ||
| 660 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 661 | "main missing" | ||
| 662 | ); | ||
| 663 | } | ||
| 664 | |||
| 665 | #[test] | ||
| 666 | fn test_format_ref_issue_simple_two_branches() { | ||
| 667 | let refs = vec!["refs/heads/main".to_string(), "refs/heads/dev".to_string()]; | ||
| 668 | assert_eq!( | ||
| 669 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 670 | "main and dev missing" | ||
| 671 | ); | ||
| 672 | } | ||
| 673 | |||
| 674 | #[test] | ||
| 675 | fn test_format_ref_issue_simple_three_branches() { | ||
| 676 | let refs = vec![ | ||
| 677 | "refs/heads/main".to_string(), | ||
| 678 | "refs/heads/dev".to_string(), | ||
| 679 | "refs/heads/feature".to_string(), | ||
| 680 | ]; | ||
| 681 | assert_eq!( | ||
| 682 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 683 | "main, dev and feature missing" | ||
| 684 | ); | ||
| 685 | } | ||
| 686 | |||
| 687 | #[test] | ||
| 688 | fn test_format_ref_issue_simple_many_branches() { | ||
| 689 | let refs = vec![ | ||
| 690 | "refs/heads/main".to_string(), | ||
| 691 | "refs/heads/dev".to_string(), | ||
| 692 | "refs/heads/feature1".to_string(), | ||
| 693 | "refs/heads/feature2".to_string(), | ||
| 694 | ]; | ||
| 695 | assert_eq!( | ||
| 696 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 697 | "main, dev and 2 others missing" | ||
| 698 | ); | ||
| 699 | } | ||
| 700 | |||
| 701 | #[test] | ||
| 702 | fn test_format_ref_issue_simple_many_branches_singular_other() { | ||
| 703 | let refs = vec![ | ||
| 704 | "refs/heads/main".to_string(), | ||
| 705 | "refs/heads/dev".to_string(), | ||
| 706 | "refs/heads/feature".to_string(), | ||
| 707 | "refs/heads/hotfix".to_string(), | ||
| 708 | ]; | ||
| 709 | assert_eq!( | ||
| 710 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 711 | "main, dev and 2 others missing" | ||
| 712 | ); | ||
| 713 | |||
| 714 | // Test with exactly 4 branches (2 shown + 2 others) | ||
| 715 | let refs = vec![ | ||
| 716 | "refs/heads/main".to_string(), | ||
| 717 | "refs/heads/dev".to_string(), | ||
| 718 | "refs/heads/feature".to_string(), | ||
| 719 | ]; | ||
| 720 | // With 3 branches, all should be shown | ||
| 721 | assert_eq!( | ||
| 722 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 723 | "main, dev and feature missing" | ||
| 724 | ); | ||
| 725 | } | ||
| 726 | |||
| 727 | #[test] | ||
| 728 | fn test_format_ref_issue_simple_single_tag() { | ||
| 729 | let refs = vec!["refs/tags/v1.0.0".to_string()]; | ||
| 730 | assert_eq!( | ||
| 731 | format_ref_issue_simple(&refs, "tag", "tags", "missing", false), | ||
| 732 | "v1.0.0 missing" | ||
| 733 | ); | ||
| 734 | } | ||
| 735 | |||
| 736 | #[test] | ||
| 737 | fn test_format_ref_issue_simple_multiple_tags() { | ||
| 738 | let refs = vec![ | ||
| 739 | "refs/tags/v1.0.0".to_string(), | ||
| 740 | "refs/tags/v1.0.1".to_string(), | ||
| 741 | "refs/tags/v2.0.0".to_string(), | ||
| 742 | ]; | ||
| 743 | assert_eq!( | ||
| 744 | format_ref_issue_simple(&refs, "tag", "tags", "missing", false), | ||
| 745 | "3 tags missing" | ||
| 746 | ); | ||
| 747 | } | ||
| 748 | |||
| 749 | #[test] | ||
| 750 | fn test_format_ref_issue_without_refs_prefix() { | ||
| 751 | let refs = vec![("main".to_string(), Some((1, 0)))]; | ||
| 752 | assert_eq!( | ||
| 753 | format_ref_issue(&refs, "branch", "branches", "out of sync", true), | ||
| 754 | "main (1 ahead) out of sync" | ||
| 755 | ); | ||
| 756 | } | ||
| 757 | |||
| 758 | #[test] | ||
| 759 | fn test_format_ref_issue_simple_without_refs_prefix() { | ||
| 760 | let refs = vec!["main".to_string()]; | ||
| 761 | assert_eq!( | ||
| 762 | format_ref_issue_simple(&refs, "branch", "branches", "missing", true), | ||
| 763 | "main missing" | ||
| 764 | ); | ||
| 765 | } | ||
| 766 | } | ||