From cec50c3ce4ded88dee4e8f60a5b342b3b55193d5 Mon Sep 17 00:00:00 2001 From: agent_qa Date: Fri, 3 Jul 2026 05:41:42 +0300 Subject: [PATCH] fix(git-sync): a git edit that drops the gitmost_id frontmatter is no longer lost (C10-D1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a git-side edit rewrote a page file WITHOUT its `gitmost_id` frontmatter (e.g. a tool that regenerates the whole file), the push planner's M (modify) branch found no pageId in the current meta and SKIPPED the file — then the next Docmost->git push overwrote it with the DB content, silently reverting the edit (data loss, found via web-test). Mirror the D (delete) branch: recover the identity from the PRE-IMAGE meta (the last-pushed version at the same path, which still carried the id) before skipping, and apply the body edit as an update. The pushed-back re-serialize restores the frontmatter next cycle, so the file self-heals. Only when the pre-image ALSO lacks an id (a never-tracked page) is it genuinely skipped. Verified on the stand: editing a synced page's file with the frontmatter removed now applies the edit (was reverted). Unit test: a modified file with no current pageId recovers it from the pre-image -> update. git-sync suite green (705). Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/git-sync/src/engine/push.ts | 25 ++++++++++++++----- .../test/compute-push-actions.test.ts | 18 +++++++++++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/git-sync/src/engine/push.ts b/packages/git-sync/src/engine/push.ts index 7bb82fee..63d28530 100644 --- a/packages/git-sync/src/engine/push.ts +++ b/packages/git-sync/src/engine/push.ts @@ -389,12 +389,25 @@ export function computePushActions(input: PushActionsInput): PushActions { } else if (pageId) { actions.updates.push({ pageId, path: change.path }); } else { - // A modified file with no pageId has no Docmost target to update. - actions.skipped.push({ - path: change.path, - status: "M", - reason: "modified file has no pageId in meta", - }); + // The current file has no `gitmost_id` — but it was MODIFIED, so a prior + // version existed at this path. Recover the identity from the PRE-IMAGE + // (the last-pushed version at the same path, which still carried the id), + // mirroring the `D` branch. Without this, an edit that also dropped the + // frontmatter (e.g. a tool that rewrote the whole file) is silently + // skipped and then reverted by the next Docmost->git push — the edit is + // lost (bug C10-D1). The pushed-back re-serialize restores the frontmatter + // next cycle, so the file self-heals. If the pre-image ALSO lacked an id + // (a page never tracked), there is genuinely nothing to update -> skip. + const prevPageId = metaAt(change.path, "prev")?.pageId; + if (prevPageId && !ghostMove.has(prevPageId)) { + actions.updates.push({ pageId: prevPageId, path: change.path }); + } else { + actions.skipped.push({ + path: change.path, + status: "M", + reason: "modified file has no pageId in meta (nor in pre-image)", + }); + } } break; } diff --git a/packages/git-sync/test/compute-push-actions.test.ts b/packages/git-sync/test/compute-push-actions.test.ts index c1965312..28a2b59e 100644 --- a/packages/git-sync/test/compute-push-actions.test.ts +++ b/packages/git-sync/test/compute-push-actions.test.ts @@ -97,7 +97,7 @@ describe('computePushActions — M (modified)', () => { expect(actions.skipped).toEqual([]); }); - it('modified file with NO pageId -> skipped (no target to update)', () => { + it('modified file with NO pageId in current OR pre-image -> skipped', () => { const changes: DiffEntry[] = [{ status: 'M', path: 'Untracked.md' }]; const actions = computePushActions({ changes, metaAt: metaTable({}) }); expect(actions.updates).toEqual([]); @@ -105,10 +105,24 @@ describe('computePushActions — M (modified)', () => { { path: 'Untracked.md', status: 'M', - reason: 'modified file has no pageId in meta', + reason: 'modified file has no pageId in meta (nor in pre-image)', }, ]); }); + + it('modified file that DROPPED its pageId recovers it from the pre-image -> update (bug C10-D1)', () => { + // The current file lost its `gitmost_id` frontmatter (a tool rewrote the whole + // file), but the last-pushed version at this path still had it. Recover the id + // and apply the body edit instead of silently skipping+reverting it. + const changes: DiffEntry[] = [{ status: 'M', path: 'Doc.md' }]; + const metaAt = metaTable({ + // current side has no pageId; prev (pre-image) side does. + 'Doc.md|prev': meta({ pageId: 'p-doc' }), + }); + const actions = computePushActions({ changes, metaAt }); + expect(actions.updates).toEqual([{ pageId: 'p-doc', path: 'Doc.md' }]); + expect(actions.skipped).toEqual([]); + }); }); describe('computePushActions — D (deleted)', () => {