Skip to content

feat(lockfile): auto-resolve git merge conflicts in deno.lock#34726

Merged
bartlomieju merged 4 commits into
mainfrom
fix/auto-merge-lockfile-conflicts
Jun 14, 2026
Merged

feat(lockfile): auto-resolve git merge conflicts in deno.lock#34726
bartlomieju merged 4 commits into
mainfrom
fix/auto-merge-lockfile-conflicts

Conversation

@bartlomieju

@bartlomieju bartlomieju commented Jun 2, 2026

Copy link
Copy Markdown
Member

Hand-merging deno.lock after a branch merge is painful today: a lockfile
with git conflict markers is not valid JSON, so Deno hard-errors with
Failed parsing. Lockfile may be corrupt and the only recourse is to delete
the lockfile (losing the clean diff) or fix the markers by hand. This
implements automatic resolution of those conflicts.

Closes #27355.

The lockfile is unusually well suited to this. Most of its sections are
identity-keyed maps with deterministic values: npm (name@version ->
info+integrity), jsr, remote (url -> hash), and redirects. A given key
always maps to the same value regardless of which branch wrote it, so a
conflict in those sections is spurious and the correct merge is a plain union.
The only section where two branches can genuinely disagree is specifiers,
which maps a version requirement to a resolved version.

The handling sits entirely on the error arm of the existing
serde_json::from_str, so the common path (a clean lockfile) is completely
untouched and pays no extra cost. Only once parsing has already failed do we
scan for conflict markers. When markers are present, the two sides of each
hunk are reconstructed into standalone lockfile texts (discarding any diff3
base section), each side is parsed with the existing parser, and the two
results are merged: identity-keyed sections are unioned, and for a genuine
specifiers conflict the higher resolved version is kept. The merged content
is flagged as changed so it gets rewritten without markers, and the normal
resolution pass that always runs afterwards validates the result against the
manifest and prunes anything orphaned. If either reconstructed side fails to
parse on its own, the file is corrupt beyond a plain conflict and the original
parse error is reported rather than guessing.

Frozen mode is handled for free: because a resolved conflict marks the
lockfile as changed, the existing changed-lockfile check refuses to silently
rewrite under --frozen and prints a diff, mirroring how Yarn disables
auto-merge under a frozen lockfile.

This is approached the way npm treats package-lock.json (the manifest is the
source of truth and the lockfile is reconciled), but it preserves every entry
both branches agreed on rather than regenerating from scratch, which keeps the
diff and any in-range version churn minimal.

There are a few deliberate cuts worth calling out for reviewers:

  • Conflict resolution runs unconditionally on a parse error. Frozen-mode
    refusal currently rides on the downstream changed-lockfile check rather than
    refusing at parse time, so the error is the generic "lockfile is out of
    date" rather than a conflict-specific message. Plumbing a flag through
    NewLockfileOptions to refuse earlier with a clearer message is a possible
    follow-up.
  • The genuine-conflict policy for specifiers is "higher resolved version
    wins, then let the resolver validate against the manifest." That is a design
    choice; the alternative is to keep "ours" and defer entirely to the
    resolver. Happy to change this if reviewers prefer the other behavior.
  • Coverage is currently unit tests in the deno_lockfile crate
    (resolves_git_merge_conflict, corrupt_lockfile_without_conflict_markers_still_errors)
    plus manual end-to-end verification. A CLI spec test under
    tests/specs/ for the deno install path is not yet included and should be
    added before merge.

@bartlomieju bartlomieju added this to the 2.9.0 milestone Jun 5, 2026

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped, and the design reasoning is sound. The zero-cost-on-happy-path framing (conflict handling only on the serde_json error arm) is the right call, and the line-based reconstruction with diff3-base dropping is correct. CI is green, title conformant, single-file diff.

Gate status

  • CI green (all checks pass)
  • Title format feat(lockfile): … conformant
  • Focused scope (one file)
  • Closes #27355

The one thing holding back approval is the author-acknowledged missing CLI spec test for the deno install path. The PR body says it "should be added before merge."

Code comments

1. split_git_conflict_sides — correct and CRLF-safe. Using split_inclusive('\n') while trimming only for marker detection preserves original line endings in the reconstructed sides. Nested/sequential hunks and the diff3 base drop are handled.

2. Test coverage is thinner than the logic. resolves_git_merge_conflict covers specifiers + npm union, but several branches are untested:

  • diff3 base section dropping (the |||||||======= arm)
  • the corrupt-one-side fallback (parse_content(&ours) fails → original ParseError returned)
  • redirects / remote / jsr union

The fallback path especially is worth a unit test, since it's the safety valve that prevents silently guessing on a genuinely corrupt file. Cheap to add now.

3. remote/npm integrity conflicts silently keep "ours" (design note, not a blocker). The union via .or_insert assumes identity keys map to deterministic values. True in normal operation, but if two branches genuinely disagree on an integrity hash for the same url/name@version, that real divergence is swallowed rather than surfaced. The premise is reasonable and matches the PR's stated model, but it's worth a sentence in the doc comment noting that a same-key/different-value collision in those sections is treated as spurious by design.

4. specifier_version_is_higher "higher wins" (design note). Acknowledged in the PR. Equal base versions with differing __peer-dep suffixes keep "ours" (candidate > current is false), which is correct, and the unioned npm section keeps both entries for the resolver to prune.

Verdict

Solid implementation. I'd want the spec test (and ideally the fallback-path unit test) in before signoff, consistent with the note in the PR body.

…all spec

Address review feedback on PR #34726:
- add unit tests for the corrupt-one-side fallback (markers present but a
  reconstructed side is invalid JSON -> original ParseError), diff3 base
  section dropping, and the jsr/redirects/remote union paths
- add a CLI spec test for the deno install path that resolves merge
  conflict markers in deno.lock and rewrites it cleanly
- document that same-key/different-value collisions in identity-keyed
  sections are treated as spurious by design
Add a frozen-mode spec case: with conflict markers in deno.lock,
`deno install --frozen` errors with the out-of-date message and leaves
the lockfile untouched (markers intact). The lockfile is read with a raw
`cat` rather than `deno task` so the read does not itself load and
rewrite the lockfile, making the assertion attributable to the install.
@bartlomieju
bartlomieju enabled auto-merge (squash) June 14, 2026 11:34
@bartlomieju
bartlomieju merged commit 6dc2158 into main Jun 14, 2026
268 of 270 checks passed
@bartlomieju
bartlomieju deleted the fix/auto-merge-lockfile-conflicts branch June 14, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-merge lockfile conflicts

1 participant