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),
});
});