|
3 | 3 | > Sync your Obsidian vault with a GitHub repository — no `git` binary, |
4 | 4 | > no `isomorphic-git`, identical behaviour on desktop and mobile. |
5 | 5 |
|
6 | | -Version `2.0.0-beta` · AGPL-3.0 · Fork of |
| 6 | +Version `2.0.1-beta` · AGPL-3.0 · Fork of |
7 | 7 | [`github-gitless-sync`](https://github.com/silvanocerza/github-gitless-sync) |
8 | 8 |
|
9 | 9 | --- |
10 | 10 |
|
| 11 | +## What's new in 2.0.1-beta |
| 12 | + |
| 13 | +Conflict resolution rebuilt from the ground up. Full design rationale |
| 14 | +in [docs/PSEUDO-MERGE-MODE.md](./docs/PSEUDO-MERGE-MODE.md); the |
| 15 | +practical highlights: |
| 16 | + |
| 17 | +- **Resolve conflicts with plain file operations — no plugin UI |
| 18 | + required.** Every conflict becomes a pair of ordinary files in |
| 19 | + your vault: the original (your local version) and a sibling |
| 20 | + (`<note>.conflict-from-<device>-<timestamp>.md`) carrying the |
| 21 | + remote version. You resolve by managing those files the same way |
| 22 | + you manage any other file in Obsidian: delete the sibling to keep |
| 23 | + yours, rename it over the base to accept theirs, edit either or |
| 24 | + both to produce a hand-merged result. No modal dialogs, no |
| 25 | + conflict markers leaking into your reading view. *(A dedicated |
| 26 | + side-by-side diff-edit GUI is planned for the next release. For |
| 27 | + now, resolution uses only the native Obsidian file operations |
| 28 | + you already know — file explorer, editor, delete, rename.)* |
| 29 | +- **Keep typing even while a file is in conflict.** Your in-progress |
| 30 | + edits flow to a private GitHub branch that no other device sees |
| 31 | + until you finalise the resolution. The conflict doesn't block |
| 32 | + you, and other devices stay protected from your half-resolved |
| 33 | + state. |
| 34 | +- **Your full edit history is preserved on GitHub — forever.** Every |
| 35 | + commit the plugin ever produces — including every iteration made |
| 36 | + during a conflict-resolution session — remains reachable on the |
| 37 | + GitHub network graph. Nothing is silently squashed or discarded; |
| 38 | + months later you can find the merge-commit for any resolved |
| 39 | + conflict and walk the side-branch to see exactly how you got |
| 40 | + there. |
| 41 | +- **Auto-merge first.** Text files get a real three-way merge against |
| 42 | + the last-synced base; only overlapping line edits surface as |
| 43 | + conflicts. Plugin bundles auto-resolve by semantic version |
| 44 | + (`manifest.json`'s `version` field), so a plugin update on one |
| 45 | + device propagates without manual intervention. Binary files |
| 46 | + always produce a sibling so you can see both versions side by |
| 47 | + side — no silent overwrites of edited images. |
| 48 | +- **Crash-tolerant atomic writes.** Every disk operation that |
| 49 | + touches multiple files (writing a sibling, persisting a conflict |
| 50 | + record, replacing a pulled file) is built on a documented |
| 51 | + multi-step protocol with a recovery sweep on plugin load. An |
| 52 | + interruption — mobile OS suspending the app mid-write, laptop |
| 53 | + losing battery during a push — leaves the vault in either the |
| 54 | + pre-operation state or the post-operation state, never a |
| 55 | + half-applied state. |
| 56 | +- **Three visibility surfaces for pending conflicts.** Status bar |
| 57 | + shows `🔀 N`, the ribbon icon carries a count badge, and the next |
| 58 | + Sync click surfaces a confirmation modal listing exactly which |
| 59 | + files are still pending. Nothing about a conflict can hide from |
| 60 | + you. |
| 61 | +- **Multi-file conflict sessions resolve one file at a time.** When |
| 62 | + several files end up in conflict, each per-file resolution |
| 63 | + publishes its result to `main` as an ordinary commit; the |
| 64 | + conflict branch waits in the background and merges back only |
| 65 | + when the last file is settled. |
| 66 | +- **`Reset` cleanly relabels conflict siblings.** If you wipe the |
| 67 | + plugin state via the settings-tab `Reset` button, your |
| 68 | + `*.conflict-from-*` files are renamed to |
| 69 | + `<file>.unresolved-<original-ts>.<ext>` so they won't collide |
| 70 | + with a future re-enable. |
| 71 | + |
| 72 | +--- |
| 73 | + |
11 | 74 | ## Why another GitHub sync plugin? |
12 | 75 |
|
13 | 76 | Most existing Obsidian-to-Git plugins either rely on the `git` CLI |
@@ -419,46 +482,78 @@ credentials → one Sync click → reload → done. |
419 | 482 |
|
420 | 483 | ## Conflict resolution |
421 | 484 |
|
422 | | -> **Status: works but not exhaustively tested.** The atomic paths |
423 | | -> (binary, plugin files) and the simple 3-way text merge are |
424 | | -> well-covered by integration tests; the diff-edit UI for unresolved |
425 | | -> text conflicts works on desktop and Android but the UX is still |
426 | | -> rough. Treat this section as a beta feature: it won't lose data |
427 | | -> (sibling-file deferral guarantees that), but it may surprise you. |
428 | | -
|
429 | 485 | A conflict happens when the same file changed on both sides since |
430 | 486 | the last sync — for example, you edited a note on your phone, then |
431 | 487 | edited the same note on your laptop, then clicked Sync on the |
432 | | -laptop. The plugin classifies the path and picks a resolver: |
433 | | - |
434 | | -| What changed | Resolver | |
435 | | -|---|---| |
436 | | -| Binary (`.png`, `.pdf`, …) | **Atomic mtime** — the newer side wins. No merge attempt, no prompt. | |
437 | | -| Plugin's `main.js` or `manifest.json` | **Atomic semver** — the higher version from `manifest.json` wins, with mtime as tie-break. Merging minified plugin code would crash Obsidian, so we don't try. | |
438 | | -| Other text (`.md`, `.txt`, …) | **3-way merge** against the last-synced base. If the merge is clean, applied silently. If it produces conflict markers, the diff view opens. | |
439 | | -| Local deleted + remote modified | **Surfaced as a conflict** — pick: keep delete / take remote / merge / defer. | |
440 | | - |
441 | | -When the diff view opens, you get three resolution choices: |
442 | | - |
443 | | -- **Resolve now** — edit the merged content directly in the diff |
444 | | - view, then save. The resolved content lands in the next push. |
445 | | -- **Merge into one** — concatenates both versions with clear |
446 | | - separators. Useful when both edits should be preserved verbatim |
447 | | - (you'll clean up the result later in your normal editor). |
448 | | -- **Defer** — keep your local version as-is and write the remote |
449 | | - version next to it as a sibling file named |
450 | | - `<note>.conflict-from-<deviceLabel>-<timestamp>.md`. Sync |
451 | | - resumes; you resolve at your own pace. Deleting the sibling |
452 | | - closes the conflict. |
453 | | - |
454 | | -<!-- SCREENSHOT: conflict view, both panes visible --> |
455 | | - |
456 | | -Deferred conflicts show up as a 🔀 counter in the status bar |
457 | | -(click it to open the list). |
458 | | - |
459 | | -The `.conflict-from-…` sibling files are gitignored by default — |
460 | | -they stay strictly local. If you want them to sync across devices, |
461 | | -edit `<vault>/.gitignore` and remove the `*.conflict-from-*` line. |
| 488 | +laptop. **Full design rationale and worked examples in |
| 489 | +[docs/PSEUDO-MERGE-MODE.md](./docs/PSEUDO-MERGE-MODE.md).** The |
| 490 | +user-facing summary: |
| 491 | + |
| 492 | +### Auto-merge happens first |
| 493 | + |
| 494 | +Before any conflict surfaces, the plugin tries to reconcile the two |
| 495 | +sides on its own. The strategy depends on the file type: |
| 496 | + |
| 497 | +| File type | Auto-merge strategy | Surfaces as a conflict only when… | |
| 498 | +|---|---|---| |
| 499 | +| Text (`.md`, `.txt`, …) | Three-way merge against the last-synced base. | Edits overlap on the same line. | |
| 500 | +| Plugin bundle (`<plugin>/main.js`, `<plugin>/manifest.json`) | Higher semantic version from `manifest.json` wins; mtime tie-break. | Identical version **and** identical mtime. | |
| 501 | +| Binary (`.png`, `.pdf`, attachments) | None — binary always surfaces. | Always (no silent overwrites of edited images). | |
| 502 | +| Local deleted, remote modified | Auto-resolves in favour of the modification (the more recent intent wins; the file resurrects on remote). | Never. | |
| 503 | +| Local modified, remote deleted | Surfaces as a conflict — your edit is preserved as a sibling. | Always. | |
| 504 | + |
| 505 | +When auto-merge succeeds, the merged result goes straight to GitHub |
| 506 | +and you're never notified — there was nothing for you to decide. |
| 507 | + |
| 508 | +### When a conflict surfaces |
| 509 | + |
| 510 | +The plugin writes the remote version into the vault next to your |
| 511 | +file, named: |
| 512 | + |
| 513 | +``` |
| 514 | +<note>.conflict-from-<remote-device>-<isoTimestamp>.md |
| 515 | +``` |
| 516 | + |
| 517 | +The original file (your local version) is **not** touched. Both |
| 518 | +files are visible in the Obsidian file explorer, openable in the |
| 519 | +editor, indistinguishable from any other Markdown / binary file. |
| 520 | +The status bar shows `🔀 N` for the count; the ribbon icon carries |
| 521 | +a matching badge. |
| 522 | + |
| 523 | +### Resolution — entirely through native Obsidian file operations |
| 524 | + |
| 525 | +> **A dedicated side-by-side diff-edit GUI is planned for the next |
| 526 | +> release.** In this release, conflict resolution is done with the |
| 527 | +> native Obsidian operations you already use every day: open the |
| 528 | +> files in the editor, delete a file from the file explorer, rename |
| 529 | +> a file by long-press (mobile) or right-click → rename (desktop). |
| 530 | +
|
| 531 | +The three resolution moves and what they mean: |
| 532 | + |
| 533 | +- **Delete the sibling** (`<note>.conflict-from-…md`) → keep your |
| 534 | + local version. On the next sync the local content is published to |
| 535 | + GitHub. |
| 536 | +- **Rename the sibling onto the base** (`<note>.conflict-from-…md` |
| 537 | + → `<note>.md`, overwriting your local version) → accept the |
| 538 | + remote version. |
| 539 | +- **Edit the base file by hand**, copying the parts you want from |
| 540 | + the sibling, then **delete the sibling** → publish your |
| 541 | + hand-merged result. |
| 542 | + |
| 543 | +A file may carry several siblings if multiple devices contributed |
| 544 | +conflicting versions — they're distinguished by the device-label |
| 545 | +segment in the filename. The conflict on that path is closed only |
| 546 | +when **every** sibling is gone (or matches the base byte-for-byte). |
| 547 | + |
| 548 | +While a file is in conflict, **you can keep editing the base |
| 549 | +file**. Those edits flow to a private branch on GitHub that no |
| 550 | +other device sees until you finalise. The conflict doesn't block |
| 551 | +you; other devices stay protected from your half-resolved state. |
| 552 | + |
| 553 | +The `*.conflict-from-*` filename pattern is gitignored by default — |
| 554 | +siblings stay strictly local. If you want them to sync across |
| 555 | +devices, edit `<vault>/.gitignore` and remove the |
| 556 | +`*.conflict-from-*` line. |
462 | 557 |
|
463 | 558 | --- |
464 | 559 |
|
|
0 commit comments