Changed (BREAKING)
-
Refactor tree-merge change matching and resolution.
Breaking because of addedResolutionFailurevariants.The tree-merge implementation previously combined side-diff collection,
path matching, scheduling, and the complete conflict-resolution matrix in a
single function. It also duplicated change collection for both sides and
represented pair outcomes with independent boolean flags.Keep the public tree() entry point as a small facade and move the merge
engine into focused private modules. Collect each ancestor-to-side diff
through one helper that constructs a SideState containing the flat change
list and its matching tree together. Isolate path and rename candidate
matching, including identical-change suppression, from classification into
pairs that the resolution matrix understands.Represent the result of handling each paired change explicitly as a
ChangeDisposition. This preserves the important distinction between a
change that was merely processed and one whose effect is present in the
editor, without duplicating the final state transition in the scheduler.
Replace the type-specific side-picking helpers with generic pick() and
pick_mut() helpers as well.These boundaries make the state machine easier to review and reduce the
chance that a future rename or forced-resolution fix accidentally changes
collection, matching, and application at once. They also remove duplicated
setup while keeping the exhaustive resolution match in one place, where
its symmetry remains visible. The public API and all recorded merge results
remain unchanged.Fixes and Improvements
Tree merging combines a flat change schedule with per-side path indexes. Valid
Git operations can therefore arrive in different orders or expose structural
relationships before the leaf changes that ultimately apply them. Several
resolver branches treated those relationships as physical occupancy or as
content changes for the same identity, leading to hangs, assertions, duplicate
entries, lost siblings, or merge results that depended on diff and side order.Separate unique-path occupancy from PassedRewrittenDirectory scheduling so a
side-qualified name can terminate below directory rewrites. Prune empty path
nodes back to the root, and allow a deferred rewrite to insert only its new
destination because its source is already indexed.Resolve the structural cases at their actual identity boundaries:
- handle an added file blocking an added directory before mode-specific add/add
resolution and defer early descendants until their parent deletion runs; - keep explicit file renames ahead of inferred directory renames, and keep
directory replacements at their explicit sources; - treat file replacements of incompatible non-blob ancestors as additions with
an empty compatible merge base; - pair shared deletions before descendants and allow file renames into paths
vacated by directory renames; - preserve unrelated nested or overlapping rename destinations by keeping the
directory in place and moving only the blocking file; - reject incompatible same-destination rewrites before blob merging, while
collapsing identical rewrites to one clean shared destination; and - defer file-to-directory children until the parent rename/delete decision is
made exactly once.
Forced Ancestor and Ours resolution continues to apply only the selected side.
Git-backed baselines cover both directions, forced policies, modes, symlinks,
gitlinks, nested directories, and documented index-only deviations. The
resulting suite contains 155 directional baseline cases, and the Cartesian
model reaches 210/210 Git/gix agreement for trees and path/mode results.More Hardening
Deferred tree changes may be reconsidered after another conflict has already
consumed or pruned the same path-tree node. This is valid when rename detection
has ambiguous identical sources, when structural conflicts overlap, or when a
change follows a detected directory rename. The editor and conflict records
still contain the required state, but strict bookkeeping removals and older
same-path assertions turned these schedules into debug panics, hangs, or
side-order-dependent duplicate content.Make cleanup idempotent wherever absence is already the required end state:
add/add type conflicts, same-source rewrites, blocking conflict destinations,
delete/rewrite sources, and changes deferred through directory renames. Accept
cross-path structural matches from ambiguous rewrite candidates and let the
existing conservative unknown-conflict fallback handle them.Preserve each rewrite input mode when blob content is identical so executable
mode changes remain visible to the merge. Make unique-path selection respect
childless tracked directories and qualify the first blocking file component,
which guarantees termination instead of varying an ineffective descendant
suffix forever. Finally, when a deferred addition is relocated to a unique
conflict path, remove its temporary original path from the side index before
marking it processed so a later descendant cannot relocate the same content a
second time.The minimized and accumulated fuzz inputs now complete without failure.
Git-backed regressions cover ambiguous sources, consumed nodes, repeated
rename/delete candidates, mode-only rewrite collisions, unique paths below
files, and nested rename destinations in both side orderings. The final tree
baseline contains 165 directional cases with 130 intentionally skipped forced
resolution checks, and reversing the nested relocation case retains exactly
a/a/aanda~Awithout inventinga~A_0. - handle an added file blocking an added directory before mode-specific add/add
Bug Fixes
-
track whether tree changes were actually applied
Replace the ambiguous
was_writtenflag with an explicit change state:Pendingmeans the change still needs processing.Processedmeans conflict handling consumed the change without applying it.Appliedmeans its effect was written to the tree editor.
This distinction matters for forced ancestor resolution. A deletion may participate
in a conflict while the ancestor entry is deliberately retained. Treating that
deletion as written caused later changes below the same path to assume the file
had been removed and replace it with a directory.Only suppress subsequent conflicts for deletions that were actually applied.
Keep applied state monotonic so later bookkeeping cannot downgrade it to merely
processed.The expanded rename baselines exposed related ordering issues, to allow a fix:
- Preserve trie children when removing a change attached to an interior node.
- Associate deferred rewrite additions with the opposing change index so the
same rename content is not merged twice. - Detect different source files renamed onto the same destination.
- Merge converging rename contents as competing additions instead of silently
allowing one rename to win. - Define forced resolution for converging renames: ancestor retains both source
files, while ours applies only the selected side's rename.
Add an inspectable rename/change matrix covering:
- rename versus source modification
- rename versus source deletion
- rename versus destination addition
- rename onto a modified destination
- rename onto a deleted destination
- the same source renamed to different destinations
- additions below a renamed directory
- both sides modifying and choosing the same rename
- different sources renamed onto the same destination
- rename/delete combined with file-to-directory replacement
-
treat adjacent text changes as conflicting.
The text merge previously considered changes from opposing sides to
overlap only when the later hunk started inside the earlier hunk's
half-open base range. For adjacent ranges[a, b)and[b, c), the
second hunk starts exactly at the end of the first and therefore wasn't
considered to overlap.This caused gix to merge edits to neighboring lines cleanly even though
there was no unchanged base line separating them. Git instead combines
such touching changes into one conflicting region.Treat opposing hunks as disjoint only when there is an actual gap
between their base ranges. A later hunk whose start is equal to the
earlier hunk's end now intersects it, while same-side hunks and changes
separated by an unchanged line retain their previous behavior.Add direct text-driver coverage for both merge directions. The tests
verify that adjacent edits produce one conflict with the expected
content and that edits separated by an unchanged base line still merge
cleanly while preserving both changes. -
merge additions below jointly deleted files
When both sides delete a file and one side replaces it with a directory,
the tree merge matched the directory parent instead of the non-tree deletion
and later treated a child addition as a conflict. In debug builds this reached
an assertion for changes at different paths.Prefer non-tree changes when multiple changes occupy the same lookup path.
Mark matching deletions as consumed so their child additions can be applied
without colliding with stale conflict state. Add a bidirectional baseline
covering the file-to-directory replacement.Git baseline: Git 2.50.1 merge-tree resolves the fixture cleanly in both
directions and retains to-be-deleted/a. The reference checkout was at
a23bace963.
Commit Statistics
- 17 commits contributed to the release over the course of 30 calendar days.
- 30 days passed between releases.
- 4 commits were understood as conventional.
- 0 issues like '(#ID)' were seen in commit messages
Commit Details
view details
- Uncategorized
- Update manifests prior to release (ebe9095)
- Merge pull request #2905 from GitoxideLabs/various-improvements (f3bbfad)
- Adapt to changes in
gix-testtools(0cbe539) - Merge pull request #2901 from cruessler/switch-to-gix-odb-at-opts (2a4d996)
- Introduce
Store::at()where possible (17fea2a) - Merge pull request #2843 from GitoxideLabs/deleted-file-added-dir (f33e250)
- Refactor tree-merge change matching and resolution. (43cf77f)
- Add Linux-sized tree merge benchmarks (7ccac01)
- Add structural tree-merge validation. (af2aa23)
- Merge pull request #2867 from GitoxideLabs/fix-url-authority-parsing (cc3ee80)
- Release gix-path v0.12.4, gix-command v0.9.2, gix-config-value v0.19.1, gix-url v0.37.1, gix-credentials v0.39.1, gix-transport v0.58.1 (ab4fcb0)
- Merge pull request #2826 from GitoxideLabs/deleted-file-added-dir (83e074c)
- Add a Cartesian tree-merge correctness baseline. (df845cf)
- Track whether tree changes were actually applied (61b45a8)
- Treat adjacent text changes as conflicting. (195f0b9)
- Merge additions below jointly deleted files (1ee3fc6)
- Merge pull request #2812 from GitoxideLabs/report-july (ae8845a)