Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,72 @@ Uses the given number of context lines around changes.")
(doc unified "Renders a line diff in unified diff format with hunk headers.
Expects the output of `line-diff` or a diff of string arrays.
Uses 3 lines of context around changes.")
(defn unified [d] (unified-with-context d 3)))
(defn unified [d] (unified-with-context d 3))

; parses the 1-based old-file start line from a unified hunk header such as
; `@@ -3,4 +3,5 @@`; the count is optional (`@@ -3 +3 @@` means one line).
(defn- parse-hunk-start [header]
(let-do [tokens (String.split-by header &[\space])
res (the (Maybe Int) (Maybe.Nothing))]
(for [i 0 (Array.length &tokens)]
(let [tok (Array.unsafe-nth &tokens i)]
(when (and (Maybe.nothing? &res)
(String.starts-with? tok "-")
(> (String.length tok) 1))
(let [range (String.suffix tok 1)]
(set! res
(Int.from-string
(Array.unsafe-nth &(String.split-by &range &[\,]) 0)))))))
res))

(doc patch
"Applies a unified diff `patch-text` (as produced by `unified`) to the
original document `original`, returning the patched document. Context and
deletion lines are checked against `original`, so this is a safe applier:
it returns `Nothing` if the patch does not apply — a context or deletion
line that does not match, or a hunk header that is malformed, out of range,
or out of order. Both documents are split and rejoined on newlines, so
`(patch old (unified (line-diff old new)))` reproduces `new`.")
(defn patch [original patch-text]
(let-do [orig (String.split-by original &[\newline])
lines (String.split-by patch-text &[\newline])
out (the (Array String) [])
olen (Array.length &orig)
cursor 0
failed false]
(for [i 0 (Array.length &lines)]
(unless failed
(let [line (Array.unsafe-nth &lines i)]
(cond
(String.starts-with? line "@@")
(match (parse-hunk-start line)
(Maybe.Nothing) (set! failed true)
(Maybe.Just start)
(let [target (if (< start 1) 0 (Int.dec start))]
(if (or (< target cursor) (> target olen))
(set! failed true)
(do
(for [k cursor target]
(Array.push-back! &out @(Array.unsafe-nth &orig k)))
(set! cursor target)))))
(String.starts-with? line " ")
(let [content (String.suffix line 1)]
(if (or (>= cursor olen)
(/= (Array.unsafe-nth &orig cursor) &content))
(set! failed true)
(do
(Array.push-back! &out content)
(set! cursor (Int.inc cursor)))))
(String.starts-with? line "-")
(let [content (String.suffix line 1)]
(if (or (>= cursor olen)
(/= (Array.unsafe-nth &orig cursor) &content))
(set! failed true)
(set! cursor (Int.inc cursor))))
(String.starts-with? line "+")
(Array.push-back! &out (String.suffix line 1))
()))))
(unless failed
(for [k cursor olen] (Array.push-back! &out @(Array.unsafe-nth &orig k))))
(if failed (Maybe.Nothing) (Maybe.Just (String.join "
" &out))))))
122 changes: 121 additions & 1 deletion tests/diff.carp
Original file line number Diff line number Diff line change
Expand Up @@ -378,4 +378,124 @@ c
d
e
FF
g") 1) "unified-with-context 1 splits hunks with enough gap")))
g") 1) "unified-with-context 1 splits hunks with enough gap")

; patch — apply a unified diff to an original document
(assert-equal test &(Maybe.Just @"line1
changed
line3") &(patch "line1
line2
line3" &(unified &(line-diff "line1
line2
line3" "line1
changed
line3"))) "patch round-trips a single-hunk change in the middle")
(assert-equal test &(Maybe.Just @"new
a
b
c") &(patch "old
a
b
c" &(unified &(line-diff "old
a
b
c" "new
a
b
c"))) "patch round-trips a change at the start")
(assert-equal test &(Maybe.Just @"a
b
c
new") &(patch "a
b
c
old" &(unified &(line-diff "a
b
c
old" "a
b
c
new"))) "patch round-trips a change at the end")
(assert-equal test &(Maybe.Just @"a
BB
c
d
e
f
g
h
i
j
KK") &(patch "a
b
c
d
e
f
g
h
i
j
k" &(unified &(line-diff "a
b
c
d
e
f
g
h
i
j
k" "a
BB
c
d
e
f
g
h
i
j
KK"))) "patch round-trips multiple hunks, copying the untouched gap")
(assert-equal test &(Maybe.Just @"a
X
Y
b") &(patch "a
b" &(unified &(line-diff "a
b" "a
X
Y
b"))) "patch round-trips a pure insertion")
(assert-equal test &(Maybe.Just @"a
b") &(patch "a
X
Y
b" &(unified &(line-diff "a
X
Y
b" "a
b"))) "patch round-trips a pure deletion")
(assert-equal test &(Maybe.Just @"same
text") &(patch "same
text" &(unified &(line-diff "same
text" "same
text"))) "patch on an empty diff returns the original unchanged")
(assert-equal test &(Maybe.Just @"a
B
c") &(patch "a
b
c" "@@ -2 +2 @@
-b
+B
") "patch accepts a count-omitted hunk header (@@ -2 +2 @@)")
(assert-equal test &(the (Maybe String) (Maybe.Nothing)) &(patch "x
y
z" &(unified &(line-diff "a
b
c" "a
B
c"))) "patch returns Nothing when a context line does not match the original")
(assert-equal test &(the (Maybe String) (Maybe.Nothing)) &(patch "a
b" "@@ garbage @@
a
") "patch returns Nothing on a malformed hunk header")))
Loading