fix(git-sync): kill spurious marker-leaking conflict, concurrent-edit loss, flapping HEAD
Three more git-sync QA defects from the 2nd live pass on PR #119, plus a callout-fidelity nit: 1. SPURIOUS conflict leaked raw markers into canonical main (root cause). On an ordinary round-trip the only difference between the docmost mirror (normalize- on-write) and a user's raw push is trailing/empty-line normalization, which made git's line-based docmost->main merge CONFLICT, and the wedge fix then committed the file WITH literal <<<<<<< / ======= / >>>>>>> markers onto main (git and the DB silently diverged for cycles). Fix: on a conflict, normalize trailing/empty lines on BOTH sides (showStage :2:/:3:) before comparing — a trailing-only diff is recognized as spurious and resolved to the clean normalized form. A GENUINE same-block conflict is auto-resolved to OURS (git wins, mirroring the live-doc 3-way rule); the docmost side stays on the `docmost` branch + page history. Raw markers NEVER reach main again. 2. Concurrent UI<->git edit silently lost the UI side. The git->Docmost 3-way merge ran against a live Y.Doc that hadn't yet received the user's debounced in-flight edit, so git clean-applied (no conflict detected) and the edit vanished even on a different block. Fix: flush the pending debounced store before the merge so the in-flight edit is drained into the live doc first — a different-block edit is merged, a same-block one is detected and pinned to history (recoverable). 3. Smart-HTTP HEAD flapped to the read-only `docmost` mirror (~1/4 of clones). The engine transiently checks out `docmost` mid-pull and the host advertises whatever HEAD resolves to. Fix: VaultGit.pinHeadToMain(); the cycle restores HEAD->main in a finally; and the upload-pack ref advertisement is served HEAD-pinned under the per-space lock so it can never observe a mid-cycle HEAD. 4. (callout) clampCalloutType now mirrors the editor's GITHUB_ALERT_TYPE_MAP for non-schema aliases (tip->success, caution->danger, important->info) instead of flatly collapsing to info. The editor schema genuinely supports only the six banner types, so unknown types still fall back to info (by design). Tests: deterministic real-git trailing-blank round-trip (no conflict, no markers, in sync over 2 cycles) + genuine-conflict no-marker-leak; HEAD advertisement stability; pre/post-flush concurrent-edit survival; serveReadAdvertisement lock pin; widened callout-alias coverage. Engine vitest + server tsc + collaboration / git-http / orchestrator specs all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,12 +59,43 @@ function getStyleProperty(element: HTMLElement, propertyName: string): string |
|
||||
* `[!note]` / `[!default]` callout authored in the editor would come back as
|
||||
* `[!info]` after a git sync (the QA "callout type -> [!info]" fidelity loss).
|
||||
* `note` and `default` were previously absent and so were being flattened.
|
||||
*
|
||||
* The editor SCHEMA genuinely only supports these six banner types — there is no
|
||||
* `tip`/`caution`/`important`/`question` callout node. So those are NOT first-
|
||||
* class types we can round-trip literally; they are INPUT ALIASES (GitHub/Obsidian
|
||||
* alert syntax). The editor's own paste/import path maps them onto the supported
|
||||
* set (see `GITHUB_ALERT_TYPE_MAP` in
|
||||
* `@docmost/editor-ext` markdown/utils/github-callout.marked.ts:
|
||||
* tip -> success, caution -> danger, important -> info). We mirror that aliasing
|
||||
* here so an ingested `> [!tip]` / `> [!caution]` lands on the closest real banner
|
||||
* (success / danger) instead of flatly collapsing to `info` — matching exactly how
|
||||
* the editor itself would interpret the same alias. A schema type always maps to
|
||||
* itself first (idempotent round-trip); the alias map only rewrites NON-schema
|
||||
* names; anything still unknown falls back to `info`.
|
||||
*/
|
||||
const CALLOUT_TYPES = ["default", "info", "note", "success", "warning", "danger"];
|
||||
export const clampCalloutType = (value: string | null | undefined): string =>
|
||||
value && CALLOUT_TYPES.includes(value.toLowerCase())
|
||||
? value.toLowerCase()
|
||||
: "info";
|
||||
/**
|
||||
* NON-schema callout aliases -> their closest supported banner. Mirrors the
|
||||
* editor's `GITHUB_ALERT_TYPE_MAP` for the names that are NOT already schema
|
||||
* types (a schema type is preserved as-is and never consulted here). Keeping
|
||||
* these in lockstep means git-sync ingest and an editor paste interpret the same
|
||||
* `> [!alias]` identically.
|
||||
*/
|
||||
const CALLOUT_TYPE_ALIASES: Record<string, string> = {
|
||||
tip: "success",
|
||||
caution: "danger",
|
||||
important: "info",
|
||||
};
|
||||
export const clampCalloutType = (value: string | null | undefined): string => {
|
||||
if (!value) return "info";
|
||||
const lower = value.toLowerCase();
|
||||
// A real schema type round-trips to itself (idempotent).
|
||||
if (CALLOUT_TYPES.includes(lower)) return lower;
|
||||
// A known GitHub/Obsidian alias maps to the editor's closest banner.
|
||||
if (CALLOUT_TYPE_ALIASES[lower]) return CALLOUT_TYPE_ALIASES[lower];
|
||||
// Anything else is collapsed to the safe default (matches the editor).
|
||||
return "info";
|
||||
};
|
||||
|
||||
/**
|
||||
* Allowlist guard for CSS color values imported from HTML.
|
||||
|
||||
Reference in New Issue
Block a user