From 9581dc88aac917ccac902ee6eddac87c03c53447 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 11 Jul 2026 12:51:27 +0200 Subject: [PATCH] fix(notes): don't save empty content over a note that hasn't loaded Entering and exiting edit mode while a note was still loading (or after a failed fetch) triggered a save of the empty placeholder, clobbering the real file on GitHub with an empty commit. Gate the dirty check, performSave, and the edit button on a loaded baseline. --- src/components/StackedNote.vue | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/StackedNote.vue b/src/components/StackedNote.vue index ec8bac6..d06ce79 100644 --- a/src/components/StackedNote.vue +++ b/src/components/StackedNote.vue @@ -188,6 +188,19 @@ const loadNote = async () => { onMounted(loadNote) +// A note only has a trustworthy baseline once it has finished loading. While +// it is still loading (or after a failed load) rawContent holds the empty +// placeholder and initialRawContent is null, so a naive +// `rawContent !== initialRawContent` check reads as a huge edit — and saving +// that would clobber the real file on GitHub with an empty commit. Gate every +// "is this dirty?" decision on a loaded baseline. +const isDirty = computed( + () => + loadStatus.value === "ready" && + initialRawContent.value !== null && + rawContent.value !== initialRawContent.value +) + watch( path, (p) => { @@ -215,6 +228,13 @@ const performSave = async (overrideSha?: string) => { return } + // Defence in depth: never push content we didn't successfully load, or we'd + // overwrite the file on GitHub with the empty placeholder. + if (loadStatus.value !== "ready" || initialRawContent.value === null) { + console.warn("refusing to save a note that hasn't finished loading") + return + } + const editedSha = overrideSha ?? (await getEditedSha()) ?? sha.value const { sha: newSha, conflict } = await updateFile({ content: rawContent.value, @@ -287,8 +307,7 @@ watch(mode, async (newMode) => { return } - const hasUserFinishedToEdit = - newMode === "read" && rawContent.value !== initialRawContent.value + const hasUserFinishedToEdit = newMode === "read" && isDirty.value if (!hasUserFinishedToEdit) { return @@ -336,7 +355,7 @@ const onBadgeClick = async () => { return } - const hasUnsavedEdits = rawContent.value !== initialRawContent.value + const hasUnsavedEdits = isDirty.value if (hasUnsavedEdits) { await handleConflict() return @@ -377,7 +396,7 @@ const onBadgeClick = async () => { class="action" />