Skip to content

gix-diff: order-independent and file-name-aware rename tracking#2687

Merged
Sebastian Thiel (Byron) merged 6 commits into
GitoxideLabs:mainfrom
ameyypawar:fix/1832-rename-tracker-order-independent
Jul 4, 2026
Merged

gix-diff: order-independent and file-name-aware rename tracking#2687
Sebastian Thiel (Byron) merged 6 commits into
GitoxideLabs:mainfrom
ameyypawar:fix/1832-rename-tracker-order-independent

Conversation

@ameyypawar

@ameyypawar Amey Pawar (ameyypawar) commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #1832.

Problem

The rewrites tracker's pre-match sort broke same-id ties by path.start/path.end — offsets into path_backing that are assigned in push order. Since changes can be pushed in a nondeterministic order (the index-worktree status feeds the tracker from a dirwalk thread and an index-traversal thread), the matcher could select a different source for identical-content candidates between runs. This is what makes gix-status's index_as_worktree_with_renames::changed_and_untracked_and_renamed fail intermittently on CI.

The same order-dependence also made merges direction-sensitive: in the rename-within-rename-2 baseline, one merge direction produced a clean tree while the other produced a spurious content conflict — the asymmetry noted in the fixture itself ("It should of course be reversible…").

Changes

  • Commit 1: sort tracker items by their observable identity — id → location → change-kind → relation → entry-mode — a total order independent of push order. id stays primary so find_match's partition_point identity search is unaffected; items that still compare equal are identical in every observable field and thus interchangeable. Adds permutation tests that run a fixed change-set through every input order, for both the rename and the exhaustive-copy paths, and updates the realistic_renames_2 snapshot where two same-id entries now emit in path order (a pure reorder).
  • Commit 2: prefer sources whose file name matches the destination's when pairing candidates — in both the identity and similarity paths — in the spirit of Git's basename-driven matching. This makes the deterministic choice also the semantically better one; previously the pairing among identical candidates was arbitrary, which tree-baseline.sh had documented: "it finds a rename of a/w to a-renamed/z … because it has no basename based matching like Git".

Tree-merge baseline updates (gix-merge)

114 of 117 baseline cases are byte-identical. The three exceptions are the deviating cases whose hand-crafted expectations had recorded the old order-dependent pairings:

  • rename-within-rename-2-A-B-deviates: both directions now produce the same cleanly merged tree — the result that was present but commented out in the fixture, and content-identical to what Git computes (Git additionally keeps the doubly-renamed file at both rename/rename destinations, while we compose the nested directory renames into one). The expected-reversed workaround branch is no longer needed.
  • conflicting-rename-complex-A-B (+ reversed): contents now follow their true renames (y.f and z pair by name; a/x.f and a/w, which B deleted, conflict with A's directory rename and keep our side). The documented a/wa-renamed/z mismatch no longer happens, and forward/reversed merges now produce the same tree with mirror-image conflict stages.

Fixture archives regenerated for SHA-1 and SHA-256.

Note

In the identity pass, when no file-name match exists the scan now covers the whole same-id range before falling back to the first eligible candidate (previously it returned there). This only matters for degenerate ranges of many identical blobs — Git scans its full same-hash bucket for basename scoring as well — but I'm happy to bound it if preferred.

Validation

  • cargo test -p gix-diff — includes the new permutation tests
  • cargo test -p gix-merge and GIX_TEST_FIXTURE_HASH=sha256 cargo test -p gix-merge tree::run_baseline — all 117 baseline cases in both hashes
  • cargo test -p gix-status — includes the previously-flaky changed_and_untracked_and_renamed
  • cargo test -p gix, cargo test -p gitoxide-core
  • cargo fmt --all -- --check, cargo clippy -p gix-diff -p gix-merge --tests -- -D warnings

@Byron
Sebastian Thiel (Byron) marked this pull request as draft July 2, 2026 09:05
@Byron

Copy link
Copy Markdown
Member

Thanks a lot for contributing. Making the rename tracking more robust and also more aligned with what Git does is definitely appreciated.

For now, I put this PR back into draft mode, as CI is legitimately failing in tests.

@ameyypawar Amey Pawar (ameyypawar) changed the title gix-diff: make rename tracking independent of the input order gix-diff: order-independent and file-name-aware rename tracking Jul 2, 2026
@ameyypawar
Amey Pawar (ameyypawar) marked this pull request as ready for review July 2, 2026 17:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecf6eba5c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment thread gix-diff/src/rewrites/tracker.rs Outdated
@Byron
Sebastian Thiel (Byron) force-pushed the fix/1832-rename-tracker-order-independent branch from ecf6eba to b3a4d0b Compare July 3, 2026 11:27
@Byron

Sebastian Thiel (Byron) commented Jul 3, 2026

Copy link
Copy Markdown
Member

Review Notes

  • benchmarks - great improvements overall, even though it's ~20% slower in the typical cases. But that's a good price to pay for correctness.
  • see if it can even be more conforming with GIt, at least document the differences - Yes, but that's for another time.
  • refackiew
  • refackiew auto-fix
Details

❯ cargo bench -p gix-diff --bench rewrite-tracker
   Compiling gix-diff v0.65.0 (/Users/byron/dev/github.com/GitoxideLabs/gitoxide.fix-1832-rename-tracker-order-independent/gix-diff)
    Finished [`bench` profile [optimized + debuginfo]](https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles) target(s) in 3.28s)
     Running benches/rewrite_tracker.rs (target/release/deps/rewrite_tracker-5ed2430a358b5e51)
Gnuplot not found, using plotters backend
rewrite-tracker/exact-renames/128                                                                                                  time:   [23.572 µs 23.822 µs 24.091 µs]
                        thrpt:  [5.3131 Melem/s 5.3733 Melem/s 5.4301 Melem/s]
                 change:
                        time:   [+19.798% +21.159% +22.450%] (p = 0.00 < 0.05)
                        thrpt:  [−18.334% −17.464% −16.526%]
                        Performance has regressed.
Benchmarking rewrite-tracker/exact-renames/1024: Collecting 100 samples in estimated 5.0490 s (20k iteratiorewrite-tracker/exact-renames/1024
                        time:   [252.28 µs 254.60 µs 257.19 µs]
                        thrpt:  [3.9815 Melem/s 4.0220 Melem/s 4.0590 Melem/s]
                 change:
                        time:   [+17.875% +18.671% +19.630%] (p = 0.00 < 0.05)
                        thrpt:  [−16.409% −15.733% −15.164%]
                        Performance has regressed.
Benchmarking rewrite-tracker/exact-renames/4096: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 6.1s, enable flat sampling, or reduce sample count to 60.
Benchmarking rewrite-tracker/exact-renames/4096: Collecting 100 samples in estimated 6.1054 s (5050 iteratirewrite-tracker/exact-renames/4096
                        time:   [1.2951 ms 1.3199 ms 1.3453 ms]
                        thrpt:  [3.0446 Melem/s 3.1033 Melem/s 3.1628 Melem/s]
                 change:
                        time:   [+19.171% +20.771% +22.664%] (p = 0.00 < 0.05)
                        thrpt:  [−18.476% −17.199% −16.087%]
                        Performance has regressed.

Benchmarking rewrite-tracker/ambiguous-identical-content/16: Collecting 100 samples in estimated 5.0031 s (rewrite-tracker/ambiguous-identical-content/16
                        time:   [2.9963 µs 3.0121 µs 3.0299 µs]
                        thrpt:  [5.2808 Melem/s 5.3119 Melem/s 5.3399 Melem/s]
                 change:
                        time:   [+13.256% +13.779% +14.325%] (p = 0.00 < 0.05)
                        thrpt:  [−12.530% −12.110% −11.705%]
                        Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high mild
Benchmarking rewrite-tracker/ambiguous-identical-content/128: Collecting 100 samples in estimated 5.2219 s rewrite-tracker/ambiguous-identical-content/128
                        time:   [78.367 µs 78.511 µs 78.672 µs]
                        thrpt:  [1.6270 Melem/s 1.6303 Melem/s 1.6333 Melem/s]
                 change:
                        time:   [+10.215% +10.978% +11.648%] (p = 0.00 < 0.05)
                        thrpt:  [−10.433% −9.8923% −9.2683%]
                        Performance has regressed.
Found 8 outliers among 100 measurements (8.00%)
  4 (4.00%) low severe
  2 (2.00%) low mild
  2 (2.00%) high mild
Benchmarking rewrite-tracker/ambiguous-identical-content/512: Warming up for 3.0000 s
Warning: Unable to complete 100 samples in 5.0s. You may wish to increase target time to 5.6s, enable flat sampling, or reduce sample count to 60.
Benchmarking rewrite-tracker/ambiguous-identical-content/512: Collecting 100 samples in estimated 5.5734 s rewrite-tracker/ambiguous-identical-content/512
                        time:   [1.0889 ms 1.0997 ms 1.1108 ms]
                        thrpt:  [460.92 Kelem/s 465.60 Kelem/s 470.20 Kelem/s]
                 change:
                        time:   [+13.928% +14.623% +15.361%] (p = 0.00 < 0.05)
                        thrpt:  [−13.315% −12.758% −12.225%]
                        Performance has regressed.
Found 14 outliers among 100 measurements (14.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
  11 (11.00%) high severe

Benchmarking rewrite-tracker/similarity-renames/16: Collecting 100 samples in estimated 5.1908 s (131k iterrewrite-tracker/similarity-renames/16
                        time:   [39.569 µs 39.668 µs 39.780 µs]
                        thrpt:  [402.21 Kelem/s 403.35 Kelem/s 404.36 Kelem/s]
                 change:
                        time:   [−71.433% −71.344% −71.245%] (p = 0.00 < 0.05)
                        thrpt:  [+247.77% +248.96% +250.05%]
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  4 (4.00%) high mild
Benchmarking rewrite-tracker/similarity-renames/64: Collecting 100 samples in estimated 5.7469 s (35k iterarewrite-tracker/similarity-renames/64
                        time:   [162.03 µs 162.25 µs 162.47 µs]
                        thrpt:  [393.91 Kelem/s 394.46 Kelem/s 395.00 Kelem/s]
                 change:
                        time:   [−89.645% −89.604% −89.565%] (p = 0.00 < 0.05)
                        thrpt:  [+858.29% +861.91% +865.69%]
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) low severe
  2 (2.00%) low mild
  2 (2.00%) high mild
Benchmarking rewrite-tracker/similarity-renames/128: Collecting 100 samples in estimated 5.1309 s (15k iterrewrite-tracker/similarity-renames/128
                        time:   [338.22 µs 338.74 µs 339.28 µs]
                        thrpt:  [377.27 Kelem/s 377.87 Kelem/s 378.45 Kelem/s]
                 change:
                        time:   [−94.136% −94.127% −94.118%] (p = 0.00 < 0.05)
                        thrpt:  [+1600.2% +1602.8% +1605.4%]
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  3 (3.00%) low mild
  2 (2.00%) high mild

Benchmarking rewrite-tracker/exhaustive-copy-sources/16: Collecting 100 samples in estimated 5.0059 s (1.0Mrewrite-tracker/exhaustive-copy-sources/16
                        time:   [4.8722 µs 4.8990 µs 4.9258 µs]
                        thrpt:  [3.2482 Melem/s 3.2660 Melem/s 3.2839 Melem/s]
                 change:
                        time:   [+14.035% +14.928% +15.821%] (p = 0.00 < 0.05)
                        thrpt:  [−13.660% −12.989% −12.308%]
                        Performance has regressed.
Found 12 outliers among 100 measurements (12.00%)
  3 (3.00%) low mild
  9 (9.00%) high severe
Benchmarking rewrite-tracker/exhaustive-copy-sources/128: Collecting 100 samples in estimated 5.6824 s (30krewrite-tracker/exhaustive-copy-sources/128
                        time:   [188.30 µs 188.76 µs 189.35 µs]
                        thrpt:  [676.01 Kelem/s 678.12 Kelem/s 679.78 Kelem/s]
                 change:
                        time:   [+26.314% +27.481% +28.542%] (p = 0.00 < 0.05)
                        thrpt:  [−22.204% −21.557% −20.832%]
                        Performance has regressed.
Found 22 outliers among 100 measurements (22.00%)
  4 (4.00%) low mild
  18 (18.00%) high severe
Benchmarking rewrite-tracker/exhaustive-copy-sources/512: Collecting 100 samples in estimated 5.2723 s (190rewrite-tracker/exhaustive-copy-sources/512
                        time:   [2.7655 ms 2.7754 ms 2.7863 ms]
                        thrpt:  [183.76 Kelem/s 184.48 Kelem/s 185.14 Kelem/s]
                 change:
                        time:   [+26.229% +27.309% +28.372%] (p = 0.00 < 0.05)
                        thrpt:  [−22.102% −21.451% −20.779%]
                        Performance has regressed.
Found 15 outliers among 100 measurements (15.00%)
  15 (15.00%) high severe

Differences to Git

Ultimately, the algorithm should just be rewritten underneath to match Git, completely from scratch, but with the current API, more or less. So far, it wasn't very important to be exactly as Git, nobody complained.
Nonetheless, it seems Git can do more and we can do the same, but maybe with better testing to show why each feature has a problem they solve specifically.

Codex

I have the relevant pieces. The short version is: this patch aligns the score ordering for a single destination, but gix-diff still deliberately does not implement Git’s full rename/copy matrix and basename pre-pass machinery.

One subtlety is worth checking before I answer: Git’s basename pre-pass is conditional and has a raised threshold. I’m reading that caller section so I don’t overstate the remaining difference.

That caller confirms the main remaining semantic gap: Git has a rename-only basename shortcut before the matrix; gix-diff now follows Git’s matrix ordering but does not reproduce that shortcut.

I’m checking gix-diff’s limit code too, because Git’s limit semantics are another real behavioral difference that can affect whether the matrix runs.

The patch only closes one specific gap: in the inexact similarity path, gix-diff now chooses score first and basename only as a tie-breaker, matching Git’s matrix comparator in /Users/byron/dev/github.com/git/git/diffcore-rename.c:242.

Remaining differences:

  • Git has a separate basename pre-pass for rename-only detection.
    In /Users/byron/dev/github.com/git/git/diffcore-rename.c:1521, Git tries unique basename matches before the full matrix using a raised threshold. With default 50% rename threshold, the basename shortcut threshold is 75%. So Git may still intentionally accept a strong-enough basename match before seeing a better non-basename match. gix-diff does not implement that pre-pass.

  • Git ranks candidates globally; gix-diff ranks per destination.
    Git keeps up to NUM_CANDIDATE_PER_DST == 4, sorts the whole candidate matrix by score/name-score, then records pairs while skipping already-used sources/destinations. gix-diff processes one destination at a time and immediately emits the best source for that destination. Competing destinations for the same source can still differ from Git.

  • Limit semantics differ.
    Git compares num_destinations * num_sources against rename_limit * rename_limit. gix-diff currently compares the raw permutations count directly to rewrites.limit in tracker.rs.

  • Copy detection source sets differ.
    The gix-diff module docs still call this out: its copy source set is smaller/different than Git’s. Git has want_copies, find-copies-harder, skip_unmodified, break detection, and relevant-source filtering paths.

  • Directory rename handling is not Git’s implementation.
    Git uses dir_rename_info, relevant_sources, dirs_removed, cached pairs, and basename integration. gix-diff uses its own relation-based parent/child tracking.

  • Similarity scoring itself is not identical.
    gix-diff computes similarity from its blob diff and removed byte count; Git’s estimate_similarity() has its own size-delta and change-count machinery. So equal-looking thresholds can still produce different edge-case scores.

@Byron
Sebastian Thiel (Byron) force-pushed the fix/1832-rename-tracker-order-independent branch 3 times, most recently from 34de22a to 4867912 Compare July 3, 2026 23:38
Codex (codex) and others added 6 commits July 4, 2026 15:08
…memory::Proxy<Never>`.

This makes in-memory databases fully functional and avoids writing duplicate code
everywhere.

Co-authored-by: Sebastian Thiel <[email protected]>
This removes a lot of custom and duplicated code.

Co-authored-by: Sebastian Thiel <[email protected]>
…deLabs#1832)

The rewrites tracker sorted its items with a same-id tiebreak on the path byte-range offsets, which are assigned in the order changes are pushed. As changes can arrive nondeterministically - the index-worktree status feeds the tracker from a dirwalk thread and an index-traversal thread - the matcher could pick different sources for identical-content candidates between runs.

Sort by each item's observable identity instead (id, then location, change-kind, relation and entry-mode), a total order that is independent of push order. Items that still compare equal are identical in every observed field and thus interchangeable for matching.

Adds permutation tests for the rename and exhaustive-copy paths, and updates the realistic_renames_2 snapshot where two same-id entries now emit in path order (a pure reorder).
GitoxideLabs#1832)

When several sources are equally viable for a destination - identical ids in the identity pass, or equal similarity in the similarity pass - prefer the one whose file name matches the destination's, in the spirit of Git's basename-driven matching. This makes the now-deterministic choice also the semantically better one, where previously the pairing among identical candidates was arbitrary.

The tree-merge baseline had recorded the arbitrary pairings in its three deviating cases, along with their consequences. These expectations are updated to the improved outcomes:

- rename-within-rename-2: both merge directions now produce the same cleanly merged tree - the result that was previously present but commented out in favor of the order-dependent conflict, making the merge reversible as intended. The expected-reversed workaround branch is no longer needed.
- conflicting-rename-complex (both directions): contents follow their true renames now that files pair up by name, and the previously documented mismatch of finding a rename of a/w to a-renamed/z no longer happens. Forward and reversed merges produce the same tree and mirror-image conflict stages.

All other 114 baseline cases are unchanged, in SHA-1 and SHA-256. Note that in the identity pass, when no file-name match exists, the scan now covers the whole same-id range before falling back to the first eligible candidate, where it stopped previously.
- prefer candidate system for best matches just like Git
- generate archives from trusted source (after removing them from previous commit)

Co-authored-by: Sebastian Thiel <[email protected]>
@Byron
Sebastian Thiel (Byron) force-pushed the fix/1832-rename-tracker-order-independent branch from 4867912 to 32cb1ad Compare July 4, 2026 08:17
@Byron
Sebastian Thiel (Byron) merged commit a82b492 into GitoxideLabs:main Jul 4, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename tracker shouldn't be order-dependent

3 participants