|
1 | 1 | use but_graph::Graph; |
2 | 2 | use but_testsupport::{graph_tree, graph_workspace, visualize_commit_graph_all}; |
| 3 | +use gix::refs::Category; |
3 | 4 |
|
4 | 5 | #[test] |
5 | 6 | fn unborn() -> anyhow::Result<()> { |
@@ -410,111 +411,100 @@ fn four_diamond() -> anyhow::Result<()> { |
410 | 411 | Ok(()) |
411 | 412 | } |
412 | 413 |
|
413 | | -/// This test demonstrates a bug in `first_merge_base`: it finds the first common |
414 | | -/// ancestor encountered during traversal, but that's not necessarily the merge-base |
415 | | -/// with "no paths around it." |
| 414 | +/// This test demonstrates the difference between `find_first_merge_base` and `find_lowest_merge_base` |
| 415 | +/// where the former finds the first common ancestor encountered during traversal, |
| 416 | +/// but that's not necessarily the merge-base with "no paths around it. |
416 | 417 | /// |
417 | 418 | /// The `merge-base-path-around` fixture creates this git commit graph: |
418 | 419 | /// ```text |
419 | | -/// A B |
420 | | -/// │ │ |
421 | | -/// ▼ ▼ |
422 | | -/// mid_a ◄───────────────── M (merge of mid_a and mid_c) |
423 | | -/// │ / \ |
424 | | -/// │ ▼ ▼ |
425 | | -/// ▼ mid_c |
426 | | -/// base ◄─────────────────────┘ |
| 420 | +/// A B |
| 421 | +/// │ │ |
| 422 | +/// mid_a ─────────────── M (merge commit) |
| 423 | +/// │ ╱ ╲ |
| 424 | +/// │ ╱ ╲ |
| 425 | +/// │ ╱ ╲ |
| 426 | +/// │ ╱ mid_c |
| 427 | +/// │ ╱ │ |
| 428 | +/// │ ╱ │ |
| 429 | +/// main ◄─────────────────────┘ |
427 | 430 | /// ``` |
428 | 431 | /// |
429 | | -/// B is a merge commit with two parents: mid_a and mid_c. |
430 | | -/// A sits on top of mid_a, which sits on base. |
431 | | -/// mid_c is a sibling branch that also connects to base. |
| 432 | +/// `B` is a merge commit with two parents: `mid_a` and `mid_c`. |
| 433 | +/// A sits on top of `mid_a`, which sits on `main`. |
| 434 | +/// `mid_c` is a sibling branch that also connects to `main`. |
432 | 435 | /// |
433 | | -/// When finding merge-base of segments A and B: |
434 | | -/// - Current (buggy) behavior: finds mid_a (first common ancestor encountered) |
435 | | -/// - Correct behavior: finds base (the only point with no path around it) |
| 436 | +/// When finding merge-base of segments `A` and `B`: |
| 437 | +/// - undesired behavior: finds `mid_a` (first common ancestor encountered) |
| 438 | +/// - Desired behavior: finds `main` (the only point with no path around it) |
436 | 439 | /// |
437 | | -/// The issue is that mid_a has a "path around it": B can reach base via mid_c |
438 | | -/// without going through mid_a. Therefore, mid_a is not the true merge-base |
| 440 | +/// The issue is that `mid_a` has a "path around it": `B` can reach `main` via `mid_c` |
| 441 | +/// without going through `mid_a`. Therefore, `mid_a` is not the true merge-base |
439 | 442 | /// where ALL paths from both segments converge. |
440 | 443 | #[test] |
441 | | -fn first_merge_base_bug_path_around() -> anyhow::Result<()> { |
| 444 | +fn lowest_vs_first_merge_base() -> anyhow::Result<()> { |
442 | 445 | let (repo, meta) = read_only_in_memory_scenario("merge-base-path-around")?; |
443 | | - insta::assert_snapshot!(visualize_commit_graph_all(&repo)?); |
| 446 | + insta::assert_snapshot!(visualize_commit_graph_all(&repo)?, @" |
| 447 | + * dceb229 (HEAD -> A) A |
| 448 | + | * a9cdc20 (B) B merges mid_a and mid_c |
| 449 | + |/| |
| 450 | + | * dcc619e (mid_c) mid_c |
| 451 | + * | 9e44c07 (mid_a) mid_a |
| 452 | + |/ |
| 453 | + * ce1ecf3 (main) base |
| 454 | + * bce0c5e M2 |
| 455 | + * 3183e43 M1 |
| 456 | + "); |
444 | 457 |
|
445 | 458 | // Use B as an extra target so it's included in the graph |
446 | 459 | let graph = Graph::from_head( |
447 | 460 | &repo, |
448 | 461 | &*meta, |
449 | 462 | standard_options_with_extra_target(&repo, "B"), |
450 | 463 | )?; |
451 | | - insta::assert_snapshot!(graph_tree(&graph)); |
| 464 | + insta::assert_snapshot!(graph_tree(&graph), @" |
| 465 | +
|
| 466 | + ├── 👉►:0[0]:A[🌳] |
| 467 | + │ └── ·dceb229 (⌂|1) |
| 468 | + │ └── ►:2[1]:mid_a |
| 469 | + │ └── ·9e44c07 (⌂|✓|1) |
| 470 | + │ └── ►:4[2]:main |
| 471 | + │ ├── ·ce1ecf3 (⌂|✓|1) |
| 472 | + │ └── ✂·bce0c5e (⌂|✓|1) |
| 473 | + └── ►:1[0]:B |
| 474 | + └── 🟣a9cdc20 (✓) |
| 475 | + ├── →:2: (mid_a) |
| 476 | + └── ►:3[1]:mid_c |
| 477 | + └── 🟣dcc619e (✓) |
| 478 | + └── →:4: (main) |
| 479 | + "); |
452 | 480 |
|
453 | 481 | // Find segments by looking for their ref names in the segment list |
454 | 482 | let find_segment = |name: &str| -> but_graph::SegmentIndex { |
455 | 483 | graph |
456 | | - .segments() |
457 | | - .find(|&sidx| { |
458 | | - graph[sidx] |
459 | | - .ref_name() |
460 | | - .is_some_and(|rn| rn.as_bstr().to_string().ends_with(name)) |
461 | | - }) |
| 484 | + .named_segment_by_ref_name( |
| 485 | + Category::LocalBranch |
| 486 | + .to_full_name(name) |
| 487 | + .expect("statically known good ref names") |
| 488 | + .as_ref(), |
| 489 | + ) |
462 | 490 | .unwrap_or_else(|| panic!("segment {name} not found")) |
| 491 | + .id |
463 | 492 | }; |
464 | 493 |
|
465 | | - let seg_a = find_segment("/A"); |
466 | | - let seg_b = find_segment("/B"); |
467 | | - let seg_mid_a = find_segment("/mid_a"); |
468 | | - let seg_main = find_segment("/main"); // base |
469 | | - |
470 | | - // Verify graph connectivity using edges |
471 | | - // A should connect to mid_a |
472 | | - // B should connect to both mid_a and mid_c (it's a merge commit) |
473 | | - use petgraph::Direction; |
474 | | - let a_edges: Vec<_> = graph.edges_directed(seg_a, Direction::Outgoing).collect(); |
475 | | - let b_edges: Vec<_> = graph.edges_directed(seg_b, Direction::Outgoing).collect(); |
476 | | - |
477 | | - assert_eq!(a_edges.len(), 1, "A should have 1 outgoing edge (to mid_a)"); |
478 | | - assert_eq!( |
479 | | - b_edges.len(), |
480 | | - 2, |
481 | | - "B should have 2 outgoing edges (to mid_a and mid_c) - this creates the 'path around'" |
482 | | - ); |
| 494 | + let seg_a = find_segment("A"); |
| 495 | + let seg_b = find_segment("B"); |
| 496 | + let seg_mid_a = find_segment("mid_a"); |
| 497 | + let seg_main = find_segment("main"); |
483 | 498 |
|
484 | 499 | // Now test first_merge_base |
485 | | - let merge_base = graph.first_merge_base(seg_a, seg_b); |
486 | | - |
487 | | - // Common ancestors of A and B are: {mid_a, deep_a, base} |
488 | | - // - mid_a: reachable from A (directly) and B (first parent) |
489 | | - // - deep_a: reachable from A (via mid_a) and B (via mid_a) |
490 | | - // - base: reachable from A (via mid_a -> deep_a) and B (via mid_a OR mid_c) |
491 | | - // |
492 | | - // The BUG: first_merge_base returns mid_a because it's encountered first |
493 | | - // when walking from A. But mid_a has a "path around it" - B can reach base |
494 | | - // via mid_c without going through mid_a. |
495 | | - // |
496 | | - // The CORRECT merge-base is base, because ALL paths from both A and B |
497 | | - // must pass through base. There's no way around base. |
498 | | - |
499 | | - // First verify we got one of the common ancestors |
500 | | - assert!( |
501 | | - merge_base == Some(seg_mid_a) || merge_base == Some(seg_main), |
502 | | - "merge_base should be one of the common ancestors (mid_a or main), got {:?}", |
503 | | - merge_base |
504 | | - ); |
505 | | - |
506 | | - // Document the buggy behavior of first_merge_base - this assertion shows it |
507 | | - // returns mid_a, which has a "path around it" via mid_c |
| 500 | + let first_merge_base = graph.first_first_merge_base(seg_a, seg_b); |
508 | 501 | assert_eq!( |
509 | | - merge_base, |
| 502 | + first_merge_base, |
510 | 503 | Some(seg_mid_a), |
511 | | - "first_merge_base returns mid_a, but there's a path around it. \ |
512 | | - Use lowest_merge_base for correct results." |
| 504 | + "the first merge-base is always one with a young generation", |
513 | 505 | ); |
514 | 506 |
|
515 | | - // Now test lowest_merge_base - this should return the correct answer |
516 | | - let lowest_merge_base = graph.lowest_merge_base(seg_a, seg_b); |
517 | | - |
| 507 | + let lowest_merge_base = graph.find_lowest_merge_base(seg_a, seg_b); |
518 | 508 | assert_eq!( |
519 | 509 | lowest_merge_base, |
520 | 510 | Some(seg_main), |
|
0 commit comments