Conversation
git's line merge conflicts whenever two edits touch neighbouring lines.
In JSON that is usually noise: two renovate bumps of adjacent
dependencies in a package.json, two new endpoints in a generated
OpenAPI schema. `mergify merge-driver json %A %O %B` is a git merge
driver that merges those files by structure instead, in a new crate,
`mergify-json-merge`.
The bar is "correct whenever it claims success, declines otherwise". A
decline falls back to `git merge-file`, so it costs what having no
driver costs: conflict markers for a person, a conflicted path for the
merge queue. Before claiming success, the driver parses its own output
again and compares it with the value it meant to produce, so a mistake
in assembling the text becomes a decline, never a wrong file. A clean
fallback line merge that produces invalid JSON from three valid inputs
(the same key added at both ends of an object) is reported as a
conflict.
The three decisions:
- Arrays merge element by element (diff3 over elements). A stretch only
one side changed takes that side's version. A stretch both changed
merges only when both edited the same elements in place (same count,
nothing moved), each element then merged recursively. Everything
else declines, including two insertions at the same point. A
JSON-Schema `required` list would take either order, an ordered
pipeline maybe neither, and nothing in the file says which kind of
array it is. The same insertion on both sides is taken once.
- Key order and formatting: the result is ours' text, edited, never
re-serialised. Unchanged bytes are copied, and theirs' changes are
spliced in as theirs wrote them, shifted to ours' indentation. A key
only theirs added goes to its sorted place when both sides keep the
object sorted (package.json dependencies, schema components).
Otherwise it goes after the key that precedes it in theirs. Two
different values for one key decline, and so does a key one side
deleted and the other changed.
- Byte-identical output: met on every pair it merged in the eval below,
generated schemas included. What it cannot know is the order a
generator gives two new fields of one model. Where CI regenerates and
compares, that difference turns the batch red rather than landing.
Eval on monorepo's history (2026-08-01 → 2026-10-01, the census
window). Each pair of changes merged within 72h of each other is
replayed as two branches off the same base and compared with what
landed:
- `dashboard/package.json`: 264 replayable pairs. 23 conflict under
the line merge, and the driver resolves all 23 byte-identical to
what landed. The other 241 merge cleanly, also byte-identical.
Nothing declined, nothing wrong. 137 pairs could not be replayed by
line position (mostly sequential bumps of one pin).
- `engine/schemas/{api-internal,api-public-future,api-public,mergify-config-schema}.json`,
consecutive changes: 189 of 189 separable pairs are byte-identical to
the regenerated file. The pairs that could not be separated show the
declines working as intended: one change editing a key the other
added, and two values appended to one `enum`.
- Cost: about 4 ms of process startup, 64 ms for a full merge of the
2 MB `api-internal.json`, under 1 ms of merge work on a package.json.
This alone does not unblock the `automated updates` queue. There, a
package.json conflict comes with a pnpm-lock.yaml one, and the lockfile
still stops composition. That is MRGFY-9186, not this.
How it reaches the engine. The engine image is python-slim with git and
uv, and has no Rust toolchain. Options:
1. Ship it as a subcommand of the `mergify` CLI, installed as a Python
dependency of the engine. Chosen. `mergify-cli` is on PyPI as a
binary-only wheel (manylinux and musllinux, x86_64 and aarch64), so
one line in the engine's pyproject puts `mergify` in the venv, with
the version pinned in `uv.lock` and bumped by renovate. No new
release artifact, no Dockerfile download, no checksum to maintain.
2. A standalone binary attached to each release and `COPY`ed or
downloaded into the image. That means a second artifact to build and
publish, a version and checksum pinned in the Dockerfile, and a
renovate rule for it, all to save a 12 MB binary.
3. A Python port inside the engine. Python's startup alone is longer
than this driver's whole run on a package.json.
The coupling option 1 creates: the engine depends on the command line
`mergify merge-driver json <ours> <base> <theirs>`. That is why it is a
documented public subcommand rather than an `_internal` one. Engine
behaviour also moves only on a dependency bump, so every change to the
driver reaches the queue through a reviewed PR. The engine-side wiring
(the driver table entry, the strategy name, the dependency, and the
PATH the composer's git sees) is a separate monorepo change.
Fixes MRGFY-9993
Change-Id: I4919b5253e93d7f465c696cb4b9d7db374b2b63e
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Merge Protections🔴 2 of 6 protections blocking · waiting on 👀 reviews
🔴 👀 Review RequirementsWaiting for
This rule is failing.
🔴 🔎 ReviewsWaiting for
This rule is failing.
Show 4 satisfied protections🟢 🤖 Continuous Integration
🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
🟢 📕 PR description
🟢 🚦 Auto-queueWhen all merge protections are satisfied, this pull request will be queued automatically. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Edited array moves can be incorrectly merged by position while claiming structural success.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Adds a lossless structural JSON merge driver and exposes it through the native CLI.
Changes:
- Implements conservative object/array three-way merging with Git fallback.
- Registers and documents
mergify merge-driver json. - Adds parser, merge, driver, and end-to-end Git tests.
| File | Description |
|---|---|
README.md |
Documents merge-driver setup. |
Cargo.lock |
Locks the new crate. |
crates/mergify-json-merge/Cargo.toml |
Defines the merge crate. |
crates/mergify-json-merge/src/lib.rs |
Exposes the public API. |
crates/mergify-json-merge/src/value.rs |
Implements lossless JSON parsing. |
crates/mergify-json-merge/src/sequence.rs |
Implements array diff3 alignment. |
crates/mergify-json-merge/src/merge.rs |
Implements structural merging and rendering. |
crates/mergify-json-merge/src/driver.rs |
Handles files and Git fallback. |
crates/mergify-json-merge/tests/driver.rs |
Tests driver fallback behavior. |
crates/mergify-cli/Cargo.toml |
Adds the merge crate dependency. |
crates/mergify-cli/src/main.rs |
Registers and dispatches the command. |
crates/mergify-cli/src/snapshots/mergify__tests__cli_schema_golden.snap |
Updates the CLI schema golden. |
crates/mergify-cli/tests/merge_driver_json.rs |
Tests real Git integration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+412
to
+415
| } else if bs.len() == os.len() | ||
| && bs.len() == ts.len() | ||
| && !moved(bs, os) | ||
| && !moved(bs, ts) |
| marker_size: Option<u32>, | ||
|
|
||
| /// Path being merged (git's `%P`), named in messages. | ||
| #[arg(long, value_name = "PATH")] |
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


git's line merge conflicts whenever two edits touch neighbouring lines.
In JSON that is usually noise: two renovate bumps of adjacent
dependencies in a package.json, two new endpoints in a generated
OpenAPI schema.
mergify merge-driver json %A %O %Bis a git mergedriver that merges those files by structure instead, in a new crate,
mergify-json-merge.The bar is "correct whenever it claims success, declines otherwise". A
decline falls back to
git merge-file, so it costs what having nodriver costs: conflict markers for a person, a conflicted path for the
merge queue. Before claiming success, the driver parses its own output
again and compares it with the value it meant to produce, so a mistake
in assembling the text becomes a decline, never a wrong file. A clean
fallback line merge that produces invalid JSON from three valid inputs
(the same key added at both ends of an object) is reported as a
conflict.
The three decisions:
one side changed takes that side's version. A stretch both changed
merges only when both edited the same elements in place (same count,
nothing moved), each element then merged recursively. Everything
else declines, including two insertions at the same point. A
JSON-Schema
requiredlist would take either order, an orderedpipeline maybe neither, and nothing in the file says which kind of
array it is. The same insertion on both sides is taken once.
re-serialised. Unchanged bytes are copied, and theirs' changes are
spliced in as theirs wrote them, shifted to ours' indentation. A key
only theirs added goes to its sorted place when both sides keep the
object sorted (package.json dependencies, schema components).
Otherwise it goes after the key that precedes it in theirs. Two
different values for one key decline, and so does a key one side
deleted and the other changed.
generated schemas included. What it cannot know is the order a
generator gives two new fields of one model. Where CI regenerates and
compares, that difference turns the batch red rather than landing.
Eval on monorepo's history (2026-08-01 → 2026-10-01, the census
window). Each pair of changes merged within 72h of each other is
replayed as two branches off the same base and compared with what
landed:
dashboard/package.json: 264 replayable pairs. 23 conflict underthe line merge, and the driver resolves all 23 byte-identical to
what landed. The other 241 merge cleanly, also byte-identical.
Nothing declined, nothing wrong. 137 pairs could not be replayed by
line position (mostly sequential bumps of one pin).
engine/schemas/{api-internal,api-public-future,api-public,mergify-config-schema}.json,consecutive changes: 189 of 189 separable pairs are byte-identical to
the regenerated file. The pairs that could not be separated show the
declines working as intended: one change editing a key the other
added, and two values appended to one
enum.2 MB
api-internal.json, under 1 ms of merge work on a package.json.This alone does not unblock the
automated updatesqueue. There, apackage.json conflict comes with a pnpm-lock.yaml one, and the lockfile
still stops composition. That is MRGFY-9186, not this.
How it reaches the engine. The engine image is python-slim with git and
uv, and has no Rust toolchain. Options:
mergifyCLI, installed as a Pythondependency of the engine. Chosen.
mergify-cliis on PyPI as abinary-only wheel (manylinux and musllinux, x86_64 and aarch64), so
one line in the engine's pyproject puts
mergifyin the venv, withthe version pinned in
uv.lockand bumped by renovate. No newrelease artifact, no Dockerfile download, no checksum to maintain.
COPYed ordownloaded into the image. That means a second artifact to build and
publish, a version and checksum pinned in the Dockerfile, and a
renovate rule for it, all to save a 12 MB binary.
than this driver's whole run on a package.json.
The coupling option 1 creates: the engine depends on the command line
mergify merge-driver json <ours> <base> <theirs>. That is why it is adocumented public subcommand rather than an
_internalone. Enginebehaviour also moves only on a dependency bump, so every change to the
driver reaches the queue through a reviewed PR. The engine-side wiring
(the driver table entry, the strategy name, the dependency, and the
PATH the composer's git sees) is a separate monorepo change.
Fixes MRGFY-9993
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com