Login
Documentation
Login

Specification for fragment_patch behaviour

Not-forking no longer uses the system patch(1), which has many implementations and no specification, and embody a great deal of ancient history and workarounds. We replace all that with a small amount of code (<150 lines). The most important and often buggy features of patch(1) that we do not implement are: fuzz, whitespace munging, and offset search. This specification documents the edge cases and decisions that NotFork::Method::Fragment_patch::apply() still needs to make. The applier is deterministic. Its output depends only on the fragment bytes and the hunk bytes.


1. The fragment handling process

The anchor-based extraction in apply() stays as it is. For each fragment, the existing code splits the target file into three parts. before is the lines up to the start-anchor match. fragment is the start-anchor line through the line before the end-anchor match. after is the end-anchor line onward.

This specification governs the middle step only, which transforms fragment in memory. The result is reassembled with before and after as it is now.

The applier takes fragment as an ordered list of lines, each keeping the trailing newline it has on disk (if any), plus the hunk body: the bytes between the end-anchor line and the --- terminator, which is the range apply() already reads. It returns the modified fragment.


2. Hunk parsing

The body holds one or more unified-diff hunks, each starting with a header line matching ^@@ -a[,b] +c[,d] @@.

2.1 — the header numbers are advisory. a, b, c, d never locate, size, or place anything. Diagnostics may quote them, and lint may check them, but they do not drive application. If the header numbers are wrong it doesn't matter because the hunk will apply by content: keyinfo's @@ -9,4 +9,7 @@ has a body that is really 5 context and 3 additions, and we apply it anyway.

2.2 — classify each body line by its first byte:

first byte meaning old-image new-image
' ' (space) context yes yes
'+' addition yes
'-' deletion yes
line is exactly \n or empty blank context yes yes
'\' (\ No newline at end of file) newline directive see §6 see §6

2.3 — marker stripping. For a ' ', '+', or '-' line, drop one leading byte; what remains, trailing newline included, is the content. A line that is exactly \n or empty counts as blank context with content \n. We accept both the · \n (space-marker) and bare \n forms of a blank context line, because fragment-diff emits both. This is what lets us match the blank lines that patch -F0 choked on in the rowsum case.

2.4 — a malformed body is a parse error: a non-empty line whose first byte is none of ' ', '+', '-', '\'. Die naming the recipe, fragment, and the offending line.


3. Old-image and new-image

For each hunk, in body order, the old-image is the content of every context and deletion line, and the new-image is the content of every context and addition line. A hunk with no +/- lines is a no-op: its new-image equals its old-image, and it reproduces the input exactly.


4. Matching

4.1 — exact contiguous match. Find the old-image as a contiguous run of lines in fragment, comparing each line's content for byte equality, trailing newline included (or its documented absence).

4.2 — forward, monotonic window. Hunks apply in body order. The search for hunk k starts at the fragment line right after hunk k−1's matched run ended; hunk 1 starts at index 0, and the window runs to the end of the fragment. Multi-hunk recipes stay order-deterministic, and a later hunk can never reach back into earlier text.

4.3 — die on ambiguity. If the old-image matches more than once in the window, die. A hunk whose context is not unique inside its already-anchored fragment is under-specified, and the cure is more context, not a guess. Anchors usually shrink the fragment to one function, so real ambiguity is rare; when it happens we want to hear about it.

4.4 — die on no match. Zero matches means die. That is the right outcome for upstream drift: regenerate the recipe for this source version instead of forcing it into place.

4.5 — an empty old-image (additions only) has nothing to anchor against, so die. fragment-diff always emits surrounding context, so this guards against a hand-malformed recipe.


5. Splicing

Walk the hunks in order with a cursor pos into fragment, starting at 0. For each hunk: find the match position at (§4); emit fragment[pos .. at-1] unchanged; emit the hunk's new-image; set pos = at + length(old-image). After the last hunk, emit fragment[pos .. end]. Context lines are shared verbatim between old- and new-image, so untouched regions and no-op hunks come out byte for byte.


6. Newlines and end of file

Lines normally carry a trailing \n, but the last line of fragment may not. A \ No newline at end of file directly after a context, addition, or deletion line means that line has no trailing \n; at parse time we record the preceding line's content without it. Matching honours this, so a newline-less old line matches only a newline-less source line. Emission honours it too, so a fragment whose last line lacks a newline round-trips unchanged, and an edit at end-of-fragment neither adds nor drops a final newline unless the hunk says so. This case shows up at the end-anchor / EOF boundary and needs its own test.


7. What this buys

Output is a pure function of the fragment bytes and the hunk bytes, identical on every OS and in every patch or no-patch environment. fragment_patch stops depending on patch(1): the shared patch_common default goes back to -tNsp1 for the plain patch method, and this method drops patch from its prerequisites. A reviewer can read the whole applier — roughly 80 to 120 lines — and know what it will do, because there is no hidden heuristic. Drift and under-specification fail loudly with the diagnostics below, where today they pass silently into a misplaced edit.


8. Errors and diagnostics

Messages speak in not-fork terms — recipe, fragment, anchor — never patch's "Hunk #N". Each one names the recipe path and .mod, the target filename, the start-anchor regex and its value if any, the end-anchor regex, and the @@ header as the author wrote it.

A no-match also shows a closest near-miss: the source line-run with the smallest edit distance to the old-image, as a short expected-vs-found mini-diff. The fuzzy compare builds this message and nothing else; it never affects whether or where a hunk applies. An ambiguity lists the fragment line numbers of the competing matches. An empty old-image and a malformed body state their cause.