Myers O(ND) forward/backtrack over line vectors (dict-vec), edit script
{:op eq|del|add :line}, reconstruction invariants both sides, paper example
D=5 verified; unified hunks with context 3, merged ranges, exact header
math for empty sides; tree/commit structural diff over flattened trees;
whole-commit unified render. 27/27, total 159/159.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
165 lines
5.3 KiB
Plaintext
165 lines
5.3 KiB
Plaintext
; Phase 5 — diff: Myers line diff (edit script + reconstruction invariants),
|
|
; unified hunk rendering, structural tree/commit diff.
|
|
|
|
(define gdf-db (persist/mem-backend))
|
|
(define gdf (git/repo gdf-db))
|
|
|
|
; ---- diff-lines ----
|
|
(git-test
|
|
"lines drop the trailing newline slot"
|
|
(= (git/diff-lines "a\nb\n") (list "a" "b"))
|
|
true)
|
|
(git-test
|
|
"lines without trailing newline"
|
|
(= (git/diff-lines "a\nb") (list "a" "b"))
|
|
true)
|
|
(git-test "empty data has no lines" (= (git/diff-lines "") (list)) true)
|
|
|
|
; ---- Myers edit script ----
|
|
(git-test
|
|
"identical inputs are all-eq"
|
|
(git/diff-changes (git/diff-script "a\nb\nc\n" "a\nb\nc\n"))
|
|
0)
|
|
(git-test
|
|
"identical inputs keep every line"
|
|
(len (git/diff-script "a\nb\nc\n" "a\nb\nc\n"))
|
|
3)
|
|
(git-test
|
|
"empty vs empty is the empty script"
|
|
(= (git/diff-script "" "") (list))
|
|
true)
|
|
(git-test
|
|
"single line replacement"
|
|
(= (git/diff-script "a" "b") (list {:op "del" :line "a"} {:op "add" :line "b"}))
|
|
true)
|
|
(git-test
|
|
"pure insertion script"
|
|
(= (git/diff-script "" "a\nb\n") (list {:op "add" :line "a"} {:op "add" :line "b"}))
|
|
true)
|
|
(git-test
|
|
"pure deletion script"
|
|
(= (git/diff-script "a\nb\n" "") (list {:op "del" :line "a"} {:op "del" :line "b"}))
|
|
true)
|
|
(git-test
|
|
"middle change keeps flanks eq"
|
|
(=
|
|
(git/diff-script "a\nb\nc\n" "a\nx\nc\n")
|
|
(list {:op "eq" :line "a"} {:op "del" :line "b"} {:op "add" :line "x"} {:op "eq" :line "c"}))
|
|
true)
|
|
|
|
; Myers' paper example: ABCABBA -> CBABAC has a shortest edit script of 5
|
|
(git-test
|
|
"ABCABBA/CBABAC shortest edit distance is 5"
|
|
(git/diff-changes (git/diff-script "A\nB\nC\nA\nB\nB\nA" "C\nB\nA\nB\nA\nC"))
|
|
5)
|
|
(git-test
|
|
"script reconstructs the old side"
|
|
(=
|
|
(git/diff-old-lines (git/diff-script "A\nB\nC\nA\nB\nB\nA" "C\nB\nA\nB\nA\nC"))
|
|
(list "A" "B" "C" "A" "B" "B" "A"))
|
|
true)
|
|
(git-test
|
|
"script reconstructs the new side"
|
|
(=
|
|
(git/diff-new-lines (git/diff-script "A\nB\nC\nA\nB\nB\nA" "C\nB\nA\nB\nA\nC"))
|
|
(list "C" "B" "A" "B" "A" "C"))
|
|
true)
|
|
(git-test
|
|
"reconstruction holds for asymmetric edits"
|
|
(let
|
|
((a "one\ntwo\nthree\nfour\n") (b "zero\ntwo\nfour\nfive\nsix\n"))
|
|
(and
|
|
(= (git/diff-old-lines (git/diff-script a b)) (git/diff-lines a))
|
|
(= (git/diff-new-lines (git/diff-script a b)) (git/diff-lines b))))
|
|
true)
|
|
|
|
; ---- unified rendering ----
|
|
(git-test
|
|
"unified: middle replacement, full context"
|
|
(git/diff-unified "a\nb\nc\n" "a\nx\nc\n")
|
|
"@@ -1,3 +1,3 @@\n a\n-b\n+x\n c\n")
|
|
(git-test
|
|
"unified: append at end"
|
|
(git/diff-unified "a\n" "a\nb\n")
|
|
"@@ -1,1 +1,2 @@\n a\n+b\n")
|
|
(git-test "unified: identical renders empty" (git/diff-unified "x\n" "x\n") "")
|
|
(git-test
|
|
"unified: creation from empty"
|
|
(git/diff-unified "" "a\nb\n")
|
|
"@@ -0,0 +1,2 @@\n+a\n+b\n")
|
|
(git-test
|
|
"unified: deletion to empty"
|
|
(git/diff-unified "a\nb\n" "")
|
|
"@@ -1,2 +0,0 @@\n-a\n-b\n")
|
|
(git-test
|
|
"unified: context trimmed to 3 lines"
|
|
(git/diff-unified "l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\n" "l1\nl2\nl3\nl4\nX\nl6\nl7\nl8\nl9\n")
|
|
"@@ -2,7 +2,7 @@\n l2\n l3\n l4\n-l5\n+X\n l6\n l7\n l8\n")
|
|
(git-test
|
|
"unified: distant changes split into two hunks"
|
|
(git/diff-unified
|
|
"l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nl12\nl13\nl14\nl15\n"
|
|
"l1\nX\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nl12\nl13\nY\nl15\n")
|
|
(str
|
|
"@@ -1,5 +1,5 @@\n l1\n-l2\n+X\n l3\n l4\n l5\n"
|
|
"@@ -11,5 +11,5 @@\n l11\n l12\n l13\n-l14\n+Y\n l15\n"))
|
|
|
|
; ---- blob diff over the object store ----
|
|
(git-test
|
|
"blob-diff reads both blobs"
|
|
(=
|
|
(git/blob-diff gdf (git/write-blob gdf "a\n") (git/write-blob gdf "b\n"))
|
|
(list {:op "del" :line "a"} {:op "add" :line "b"}))
|
|
true)
|
|
|
|
; ---- structural tree/commit diff ----
|
|
(define
|
|
gdf-t1
|
|
(git/tree-from-files
|
|
gdf
|
|
(assoc
|
|
(assoc (assoc {} "a.txt" "1\n") "b.txt" "2\n")
|
|
"sub/c.txt"
|
|
"3\n")))
|
|
(define
|
|
gdf-t2
|
|
(git/tree-from-files
|
|
gdf
|
|
(assoc
|
|
(assoc (assoc {} "a.txt" "1\n") "b.txt" "2x\n")
|
|
"d.txt"
|
|
"new\n")))
|
|
(define gdf-c1 (git/write gdf (git/commit gdf-t1 (list) {:message "c1"})))
|
|
(define gdf-c2 (git/write gdf (git/commit gdf-t2 (list gdf-c1) {:message "c2"})))
|
|
|
|
(git-test
|
|
"tree-diff classifies added/modified/deleted"
|
|
(= (git/tree-diff gdf gdf-t1 gdf-t2) {:deleted (list "sub/c.txt") :modified (list "b.txt") :added (list "d.txt")})
|
|
true)
|
|
(git-test
|
|
"tree-diff of a tree with itself is empty"
|
|
(= (git/tree-diff gdf gdf-t1 gdf-t1) {:deleted (list) :modified (list) :added (list)})
|
|
true)
|
|
(git-test
|
|
"commit-diff goes through the commit trees"
|
|
(= (git/commit-diff gdf gdf-c1 gdf-c2) {:deleted (list "sub/c.txt") :modified (list "b.txt") :added (list "d.txt")})
|
|
true)
|
|
|
|
; ---- whole-commit unified render ----
|
|
(git-test
|
|
"commit-diff-unified renders adds, deletes, then modifications"
|
|
(let
|
|
((r (git/repo (persist/mem-backend))))
|
|
(let
|
|
((c1 (git/write r (git/commit (git/tree-from-files r (assoc {} "f.txt" "old\n")) (list) {:message "c1"}))))
|
|
(let
|
|
((c2 (git/write r (git/commit (git/tree-from-files r (assoc (assoc {} "f.txt" "new\n") "g.txt" "hi\n")) (list c1) {:message "c2"}))))
|
|
(git/commit-diff-unified r c1 c2))))
|
|
(str
|
|
"diff --sx a/g.txt b/g.txt\n--- /dev/null\n+++ b/g.txt\n@@ -0,0 +1,1 @@\n+hi\n"
|
|
"diff --sx a/f.txt b/f.txt\n--- a/f.txt\n+++ b/f.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n"))
|
|
(git-test
|
|
"commit-diff-unified of identical commits is empty"
|
|
(git/commit-diff-unified gdf gdf-c1 gdf-c1)
|
|
"")
|