diff --git a/packages/studio-server/src/helpers/applyFileMutations.test.ts b/packages/studio-server/src/helpers/applyFileMutations.test.ts index 21ccb4b4d71..aa0940097d8 100644 --- a/packages/studio-server/src/helpers/applyFileMutations.test.ts +++ b/packages/studio-server/src/helpers/applyFileMutations.test.ts @@ -105,6 +105,27 @@ describe("applyFileMutations", () => { expectStaleMutation("after", false); }); + it("leaves a receipt saying which bytes the write started from", () => { + resetFileWriteReceipts(); + const projectDir = mkdtempSync(join(tmpdir(), "hf-mutation-from-")); + const path = join(projectDir, "index.html"); + try { + // Someone else's change lands before the patch reads the file: the receipt names it as the start. + writeFileSync(path, "external", "utf8"); + applyFileMutations( + projectDir, + [{ sourceFile: "index.html", absPath: path, after: "patched" }], + "hand", + ); + expect(identifyFileWrite(path, fileContentVersion("patched"))).toMatchObject({ + writeToken: "hand", + from: fileContentVersion("external"), + }); + } finally { + rmSync(projectDir, { recursive: true, force: true }); + } + }); + it("clears receipts for writes rolled back after a partial batch", () => { resetFileWriteReceipts(); const projectDir = mkdtempSync(join(tmpdir(), "hf-mutation-rollback-")); diff --git a/packages/studio-server/src/helpers/fileVersion.test.ts b/packages/studio-server/src/helpers/fileVersion.test.ts index e203ec804fa..bd3236f33f1 100644 --- a/packages/studio-server/src/helpers/fileVersion.test.ts +++ b/packages/studio-server/src/helpers/fileVersion.test.ts @@ -109,6 +109,29 @@ describe("file versions and write receipts", () => { expect(identifyFileWrite("/project/index.html", first.version)).toEqual(first); }); + it("says which version a write replaced when the writer read it, and nothing when it did not", () => { + const version = fileContentVersion("

Title!

"); + recordFileWriteReceipt("/project/index.html", { + path: "index.html", + version, + writeToken: "hand", + overwrote: "

Title

", + }); + expect(identifyFileWrite("/project/index.html", version)).toEqual({ + path: "index.html", + version, + writeToken: "hand", + from: fileContentVersion("

Title

"), + }); + const restored = fileContentVersion("

Old

"); + recordFileWriteReceipt("/project/index.html", { + path: "index.html", + version: restored, + writeToken: "restore", + }); + expect(identifyFileWrite("/project/index.html", restored)).not.toHaveProperty("from"); + }); + it("labels a repeat of earlier bytes with the newest token, not the spent one", () => { const version = fileContentVersion("same bytes"); const older = { path: "index.html", version, writeToken: "write-older" }; diff --git a/packages/studio-server/src/helpers/fileVersion.ts b/packages/studio-server/src/helpers/fileVersion.ts index d399f9afa34..e06e3b02ebd 100644 --- a/packages/studio-server/src/helpers/fileVersion.ts +++ b/packages/studio-server/src/helpers/fileVersion.ts @@ -5,6 +5,7 @@ export interface FileWriteReceipt { path: string; version: string; writeToken: string; + from?: string; } interface StoredReceipt extends FileWriteReceipt { @@ -70,7 +71,11 @@ export function recordFileWriteReceipt( if (live.length > 0) receipts.set(path, live); else receipts.delete(path); } - receipts.set(absPath, [...(receipts.get(absPath) ?? []), { ...receipt, recordedAt: now }]); + const from = overwrote === undefined ? undefined : fileContentVersion(overwrote); + receipts.set(absPath, [ + ...(receipts.get(absPath) ?? []), + { ...receipt, ...(from && { from }), recordedAt: now }, + ]); } export function clearFileWriteReceipt(filePath: string, version: string, writeToken: string): void { @@ -96,8 +101,8 @@ export function identifyFileWrite( ): FileWriteReceipt | null { const receipt = newestReceipt(realFilePath(filePath), expectedVersion); if (!receipt) return null; - const { path, version, writeToken } = receipt; - return { path, version, writeToken }; + const { path, version, writeToken, from } = receipt; + return { path, version, writeToken, ...(from && { from }) }; } function newestReceipt(absPath: string, expectedVersion: string): StoredReceipt | undefined { diff --git a/packages/studio-server/src/routes/files.test.ts b/packages/studio-server/src/routes/files.test.ts index bb018acb129..ae52f716290 100644 --- a/packages/studio-server/src/routes/files.test.ts +++ b/packages/studio-server/src/routes/files.test.ts @@ -158,6 +158,7 @@ describe("registerFileRoutes", () => { path: "index.html", version: result.version, writeToken: "studio-insert-1", + from: fileContentVersion(before), }); const committed = result.after; @@ -428,6 +429,7 @@ describe("registerFileRoutes", () => { path: "image.png", version: payload.version, writeToken: "binary-write", + from: fileContentVersion(before), }); }); @@ -541,6 +543,7 @@ describe("registerFileRoutes", () => { path: "index.html", version: payload.version, writeToken: "studio-write-1", + from: fileContentVersion("before"), }); expect(payload.backupPath).toMatch(/^\.hyperframes\/backup\//); expect(readFileSync(join(projectDir, payload.backupPath!), "utf-8")).toBe("before"); @@ -734,6 +737,7 @@ describe("registerFileRoutes", () => { path: "index.html", version, writeToken: "studio-patch-1", + from: fileContentVersion('
Before
'), }); }); @@ -780,6 +784,7 @@ describe("registerFileRoutes", () => { path: "index.html", version, writeToken: "studio-layer-order-1", + from: fileContentVersion(original), }); expect(readdirSync(join(projectDir, ".hyperframes", "backup"))).toHaveLength(1); }); @@ -884,6 +889,7 @@ describe("registerFileRoutes", () => { path: file.sourceFile, version, writeToken: "studio-group-drag-1", + from: fileContentVersion(`
Before
`), }); } }); @@ -1105,6 +1111,7 @@ describe("registerFileRoutes", () => { path: "index.html", version: payload.files[0].version, writeToken: "cut-test", + from: fileContentVersion(before), }); });