Skip to content

Commit fc3740f

Browse files
blhsingclaude
andcommitted
Reimplement ort merge engine in pure Python (no git binary, no fallback)
Replace the `git merge-tree --write-tree` subprocess (and the line-based fallback) with a faithful port of Git's ort merge engine, ported from the git v2.44.0 source. Output — result tree oid, conflicted blobs with markers, and conflicted index stages — is byte-for-byte identical to `git merge-tree --write-tree`. New modules: - xdiff.py: port of git's xdiff library — record classification, the histogram diff ort hardcodes for content merges, the classic Myers algorithm as histogram's fallback, xdl_change_compact, and the zealous three-way xdl_merge that emits <<<<<<< / ======= / >>>>>>> markers (CRLF and no-trailing-newline handling included). - diffcore.py: port of diffcore-delta.c (spanhash similarity) and diffcore-rename.c (exact, basename-driven, and inexact NxM matrix matching with git's exact float scoring). - mergeort.py: port of merge-ort.c — recursive three-way tree walk (collect_merge_info), file and directory rename detection/resolution (process_renames, get_provisional_directory_renames, apply_directory_rename_modifications), per-path resolution (process_entry), and streamed result-tree assembly with conflicted index stages. ort.py becomes a thin adapter (OrtResult API unchanged); the merge_base/ ours/theirs arguments double as conflict-marker labels exactly as the corresponding `git merge-tree --merge-base` arguments do. sequencer._apply_patch drops its fallback path; now-dead rename/blob-merge helpers are removed. Validation (vs real git 2.44): ~3.7k randomized blob merges vs `git merge-file --diff-algorithm=histogram` and ~2k randomized whole-tree merges vs `git merge-tree --write-tree -z`, all byte-for-byte identical; plus tests/test_ort_parity.py covering every conflict type. Single merge base only — recursive/virtual-ancestor merge and full submodule fast-forward resolution are out of scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1c64f88 commit fc3740f

9 files changed

Lines changed: 3562 additions & 315 deletions

File tree

README.md

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pure-python-git/ (repo root)
2525
│ ├── workdir.py add/rm/status/checkout, tree↔workdir
2626
│ ├── diff.py Myers diff + unified-diff renderer
2727
│ ├── merge.py merge-base + three-way blob merge
28+
│ ├── xdiff.py xdiff port: histogram/Myers diff + xdl_merge
29+
│ ├── diffcore.py diffcore-rename: spanhash similarity + matching
30+
│ ├── mergeort.py merge-ort tree engine (collect/rename/process)
31+
│ ├── ort.py ort adapter → OrtResult(tree, conflicts, index)
2832
│ ├── sequencer.py cherry-pick / revert / rebase
2933
│ ├── porcelain_merge.py ff + 3-way merge entry point
3034
│ ├── patch.py unified-diff parser + applier
@@ -264,11 +268,29 @@ mechanism.
264268

265269
`merge.merge_bases` mirrors `commit-reach.c`'s `paint_down_to_common`: BFS
266270
from both tips with PARENT1/PARENT2 flags, marking double-flagged commits as
267-
results and pushing STALE to their ancestors. When a real C Git binary is
268-
available, high-level three-way merges run Git's own `ort` engine through
269-
`git merge-tree --write-tree`, then import the exact result tree and conflicted
270-
stage entries. Without C Git, `pygit` falls back to its built-in line-based
271-
three-way merge with conservative rename detection.
271+
results and pushing STALE to their ancestors.
272+
273+
High-level three-way merges run a pure-Python port of Git's own `ort` engine —
274+
no `git` binary and no fallback engine. The port lives in three modules and
275+
reproduces `git merge-tree --write-tree` byte-for-byte (result tree oid,
276+
conflicted blobs with markers, and conflicted index stages):
277+
278+
* `xdiff.py` — Git's xdiff library: record classification, the **histogram**
279+
diff that `ort` hardcodes for content merges (with the classic Myers
280+
algorithm as its documented fallback), change compaction, and the zealous
281+
three-way `xdl_merge` that emits `<<<<<<<` / `=======` / `>>>>>>>` markers.
282+
* `diffcore.py` — rename detection: the `diffcore-delta` spanhash similarity
283+
estimator plus exact, basename-driven, and inexact NxM matrix matching from
284+
`diffcore-rename.c`.
285+
* `mergeort.py` — the `merge-ort.c` tree engine: the recursive three-way tree
286+
walk (`collect_merge_info`), file and **directory** rename detection and
287+
resolution (`process_renames`), per-path resolution (`process_entry`), and
288+
streamed result-tree assembly with conflicted index stages.
289+
290+
`ort.py` is a thin adapter exposing `merge_tree(repo, merge_base, ours,
291+
theirs)`; the `merge_base`/`ours`/`theirs` arguments double as the
292+
conflict-marker labels, exactly as the corresponding `git merge-tree
293+
--merge-base` arguments do.
272294

273295
### Rerere
274296

@@ -348,7 +370,7 @@ pip install pythongit[test]
348370
pytest
349371
```
350372

351-
106 tests pass:
373+
The suite passes:
352374

353375
| File | Coverage |
354376
|-------------------------|----------|
@@ -358,11 +380,18 @@ pytest
358380
| `unit_pack.py` | delta apply, idx v2, build_pack, inbound pack indexing, pack/MIDX bitmaps, binary MIDX, SHA-256 interop |
359381
| `unit_modules.py` | diff/merge/patch/ignore/rerere/SMTP/XOAUTH2/fsmonitor/bisect unit-level |
360382
| `unit_integration.py` | end-to-end CLI flows incl. ort-backed conflicts, rename-aware merge, rerere replay, SHA-256 translation, loose cache, streaming upload-pack, recursive tree diff |
383+
| `test_ort_parity.py` | byte-for-byte `ort` parity vs `git merge-tree --write-tree` across every conflict type (content, modify/delete, add/add, rename/rename, rename/delete, directory rename, distinct-types, exec-bit) |
361384
| `unit_phase_scripts.py` | wraps the script-style phase tests |
362385

363386
Tests that require the real `git` binary are silently skipped when it's not on
364387
PATH, so the suite runs cleanly in containers without one.
365388

389+
The pure-Python `ort` engine is additionally cross-checked against C Git with
390+
the differential fuzzers in `tests/diff_xdiff_harness.py` (blob-level 3-way
391+
merges vs `git merge-file`) and `tests/diff_ort_harness.py` (whole-tree merges
392+
vs `git merge-tree`); both compare results byte-for-byte over thousands of
393+
randomized cases.
394+
366395
## What's intentionally NOT implemented
367396

368397
* `git filter-repo` (it's a separate Python tool anyway, not a git built-in).
@@ -375,10 +404,14 @@ PATH, so the suite runs cleanly in containers without one.
375404
pack generation/indexing. Tree-diff commands skip identical subtrees. The
376405
remaining scale-sensitive cases are commands whose output inherently requires
377406
inspecting every path or blob.
378-
* Byte-for-byte `ort` merge parity uses the real C Git binary when available.
379-
If no usable `git` binary is on PATH, merges fall back to the pure-Python
380-
engine and may differ from Git on obscure rename, directory/file, submodule,
381-
and conflict-presentation edge cases.
407+
* The `ort` merge engine is a pure-Python reimplementation (no `git` binary,
408+
no fallback) and is validated for byte-for-byte parity against
409+
`git merge-tree --write-tree` across content merges, rename detection
410+
(file and directory), and conflict presentation. It targets a single merge
411+
base (as `git merge-tree --merge-base` provides); recursive merge of multiple
412+
merge bases (a virtual ancestor) and full submodule fast-forward resolution
413+
are not modelled, and `merge.conflictStyle`/whitespace merge drivers default
414+
to Git's standard behavior.
382415
* `fsmonitor-daemon run` uses native filesystem notifications on Windows and
383416
Linux (`ReadDirectoryChangesW` / inotify). One-shot `fsmonitor` calls and
384417
unsupported platforms fall back to configurable polling.

0 commit comments

Comments
 (0)