Patch Conflict Detector
Apply optimistic multi-file patches atomically while rejecting stale or overlapping edits.
Implement apply_patches(files, patches), an atomic patch applier with optimistic conflict checks.
Requirements
filesmaps file path to text.- Each patch is
(path, start, end, expected_old_text, replacement). - Validate all patches before mutating anything.
- Raise
ValueErrorfor invalid ranges (start < 0,end < start, orend > len(text)), missing files, overlapping patches in the same file, and expected-text mismatches. - Adjacent ranges that only touch at an endpoint are allowed. Multiple empty inserts at the same offset are allowed and preserve input order. Order by
(start, end)stably within each file; an insert at a replacement's start appears before the replacement, and an insert at its end appears after. - An empty insert strictly inside a nonempty patch range is an overlap conflict. An insertion must have
expected_old_text="". - Apply patches from the end of each file so offsets stay stable.
- Return a new dictionary and leave input untouched.
Example
The first patch succeeds without changing files. The stale patch then fails against the same input, which remains untouched.
1files = {"app.py": "print('old')\n"}
2patches = [("app.py", 6, 11, "'old'", "'new'")]
3assert apply_patches(files, patches)["app.py"] == "print('new')\n"
4assert files["app.py"] == "print('old')\n"
5
6try:
7 apply_patches(files, [("app.py", 6, 11, "'stale'", "'new'")])
8except ValueError:
9 pass
10else:
11 raise AssertionError("expected stale patch conflict")
12
13assert files["app.py"] == "print('old')\n"Constraints
- No diff parser is required.
- Offsets address each file's original text, not a result from an earlier patch. Use Python code-point indexes or Java UTF-16 code-unit indexes for the selected language.
What the expected text proves
Expected text checks a particular slice of the input snapshot. If "hello!" replaces an earlier "hello", a patch expecting "hello" at [0, 5) still matches: the changed suffix is outside its checked range. If you need to reject any change to the file, require an expected whole-file hash or version as a follow-up.
All-or-nothing here means returning a fully built new map or raising without changing the caller's map. There are no filesystem writes or concurrent-writer guarantees. A later invalid file must prevent the whole call from returning a partially patched result.
For files={"a": "abc"}, patches [("a", 1, 2, "b", "R"), ("a", 1, 1, "", "L"), ("a", 2, 2, "", "E")] produce {"a": "aLREc"}. Both expected slices are checked against the original abc; right-to-left application avoids shifting the remaining original offsets. Sorting by start alone is insufficient because an input replacement can precede an insert at that same start.