文件
mynode/readme/dev/spec/conflict_auto_merge.md
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

2.6 KiB

Conflict auto-merge

When the same note is edited on two devices before they sync, if a change is made when one of the devices was not fully synced, Joplin creates a conflict note. Auto-merge avoids this when the edits don't overlap, using a three-way merge. It runs on the client that syncs second; other devices just receive the merged note.

Base version

The merge needs three versions: base (the last version this device and the sync target agreed on), local and remote.

The base is stored per sync target in sync_items.base_body and base_title. It is recorded after an upload, after a download, and after a conflict, whichever version is left in the original note. If a downloaded note is encrypted, the base is cleared until it is decrypted, so that a conflict before decryption creates a normal conflict note.

Merge

The merge is line-based. Regions changed by only one side are taken as-is. Regions changed by both sides are auto-merged if both made the same change, otherwise they become a conflict. Edits on the same or adjacent lines therefore conflict. Word-level merging was tried and dropped because it could silently duplicate text. Titles follow the same rule.

If everything merges, the merged note is saved over the local note, becomes the new base, and its updated_time is moved past the remote time so it gets uploaded. If some regions conflict, a conflict note is still created, but both notes already include the non-conflicting changes, so they differ only where there's a real conflict.

Fallback to a conflict note

The merge is skipped, and a normal conflict note is created, when:

  • sync.autoMergeConflicts setting is off
  • there is no recorded base
  • the item is read-only or either note is locked
  • either note cannot be decrypted
  • an edit which touches identical lines, since the diff can't tell which one changed and may duplicate content.
  • the diff exceeds its bounds (maxEditLength 5000, timeout 1000ms)

E2EE

An encrypted remote note is decrypted in memory for the merge without being saved. If a conflict note is created, the original note is saved with the decrypted (and partially merged) remote content.

Code

  • services/conflict/diffNotes.ts: merge engine (autoMerge()) and the duplicate-line guard
  • services/conflict/boundedDiff3.ts: fork of node-diff3's diff3MergeRegions() using a bounded diff
  • services/conflict/autoMergeNote.ts: merges title and body of a note
  • services/synchronizer/utils/handleConflictAction.ts: runs the merge during sync and applies the result
  • models/BaseItem.ts: stores the base and keeps it when a sync doesn't provide a new one