|
|
|
@@ -2,10 +2,11 @@ import { afterEach, test, expect } from "bun:test"
|
|
|
|
|
import { $ } from "bun"
|
|
|
|
|
import fs from "fs/promises"
|
|
|
|
|
import path from "path"
|
|
|
|
|
import { Effect } from "effect"
|
|
|
|
|
import { Snapshot } from "../../src/snapshot"
|
|
|
|
|
import { Instance } from "../../src/project/instance"
|
|
|
|
|
import { Filesystem } from "../../src/util/filesystem"
|
|
|
|
|
import { tmpdir } from "../fixture/fixture"
|
|
|
|
|
import { provideInstance, tmpdir } from "../fixture/fixture"
|
|
|
|
|
|
|
|
|
|
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
|
|
|
|
// with path.join (which produces \ on Windows) then normalizes back to /.
|
|
|
|
@@ -35,17 +36,26 @@ async function bootstrap() {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function run<A>(dir: string, body: (snapshot: Snapshot.Interface) => Effect.Effect<A>) {
|
|
|
|
|
return Effect.runPromise(
|
|
|
|
|
Effect.gen(function* () {
|
|
|
|
|
const snapshot = yield* Snapshot.Service
|
|
|
|
|
return yield* body(snapshot)
|
|
|
|
|
}).pipe(provideInstance(dir), Effect.provide(Snapshot.defaultLayer)),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test("tracks deleted files correctly", async () => {
|
|
|
|
|
await using tmp = await bootstrap()
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "a.txt"))
|
|
|
|
|
expect((await run(tmp.path, (snapshot) => snapshot.patch(before!))).files).toContain(fwd(tmp.path, "a.txt"))
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -55,12 +65,14 @@ test("revert should remove new files", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/new.txt`, "NEW")
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -77,13 +89,15 @@ test("revert in subdirectory", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`mkdir -p ${tmp.path}/sub`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/sub/file.txt`, "SUB")
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -102,7 +116,7 @@ test("multiple file operations", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
@@ -111,7 +125,9 @@ test("multiple file operations", async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/dir/d.txt`, "D")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/b.txt`, "MODIFIED")
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe(tmp.extra.aContent)
|
|
|
|
|
expect(
|
|
|
|
@@ -132,12 +148,12 @@ test("empty directory handling", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`mkdir ${tmp.path}/empty`.quiet()
|
|
|
|
|
|
|
|
|
|
expect((await Snapshot.patch(before!)).files.length).toBe(0)
|
|
|
|
|
expect((await run(tmp.path, (snapshot) => snapshot.patch(before!))).files.length).toBe(0)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -147,15 +163,15 @@ test("binary file handling", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/image.png`, new Uint8Array([0x89, 0x50, 0x4e, 0x47]))
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "image.png"))
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
|
.access(`${tmp.path}/image.png`)
|
|
|
|
@@ -171,12 +187,12 @@ test("symlink handling", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await fs.symlink(`${tmp.path}/a.txt`, `${tmp.path}/link.txt`, "file")
|
|
|
|
|
|
|
|
|
|
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "link.txt"))
|
|
|
|
|
expect((await run(tmp.path, (snapshot) => snapshot.patch(before!))).files).toContain(fwd(tmp.path, "link.txt"))
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -186,12 +202,12 @@ test("file under size limit handling", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/large.txt`, "x".repeat(1024 * 1024))
|
|
|
|
|
|
|
|
|
|
expect((await Snapshot.patch(before!)).files).toContain(fwd(tmp.path, "large.txt"))
|
|
|
|
|
expect((await run(tmp.path, (snapshot) => snapshot.patch(before!))).files).toContain(fwd(tmp.path, "large.txt"))
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -201,14 +217,14 @@ test("large added files are skipped", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/huge.txt`, new Uint8Array(2 * 1024 * 1024 + 1))
|
|
|
|
|
|
|
|
|
|
expect((await Snapshot.patch(before!)).files).toEqual([])
|
|
|
|
|
expect(await Snapshot.diff(before!)).toBe("")
|
|
|
|
|
expect(await Snapshot.track()).toBe(before)
|
|
|
|
|
expect((await run(tmp.path, (snapshot) => snapshot.patch(before!))).files).toEqual([])
|
|
|
|
|
expect(await run(tmp.path, (snapshot) => snapshot.diff(before!))).toBe("")
|
|
|
|
|
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBe(before)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -218,13 +234,15 @@ test("nested directory revert", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`mkdir -p ${tmp.path}/level1/level2/level3`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/level1/level2/level3/deep.txt`, "DEEP")
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([await Snapshot.patch(before!)])
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -241,14 +259,14 @@ test("special characters in filenames", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/file with spaces.txt`, "SPACES")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/file-with-dashes.txt`, "DASHES")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/file_with_underscores.txt`, "UNDERSCORES")
|
|
|
|
|
|
|
|
|
|
const files = (await Snapshot.patch(before!)).files
|
|
|
|
|
const files = (await run(tmp.path, (snapshot) => snapshot.patch(before!))).files
|
|
|
|
|
expect(files).toContain(fwd(tmp.path, "file with spaces.txt"))
|
|
|
|
|
expect(files).toContain(fwd(tmp.path, "file-with-dashes.txt"))
|
|
|
|
|
expect(files).toContain(fwd(tmp.path, "file_with_underscores.txt"))
|
|
|
|
@@ -262,10 +280,10 @@ test("revert with empty patches", async () => {
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
// Should not crash with empty patches
|
|
|
|
|
expect(Snapshot.revert([])).resolves.toBeUndefined()
|
|
|
|
|
expect(run(tmp.path, (snapshot) => snapshot.revert([]))).resolves.toBeUndefined()
|
|
|
|
|
|
|
|
|
|
// Should not crash with patches that have empty file lists
|
|
|
|
|
expect(Snapshot.revert([{ hash: "dummy", files: [] }])).resolves.toBeUndefined()
|
|
|
|
|
expect(run(tmp.path, (snapshot) => snapshot.revert([{ hash: "dummy", files: [] }]))).resolves.toBeUndefined()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
@@ -275,14 +293,14 @@ test("patch with invalid hash", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Create a change
|
|
|
|
|
await Filesystem.write(`${tmp.path}/test.txt`, "TEST")
|
|
|
|
|
|
|
|
|
|
// Try to patch with invalid hash - should handle gracefully
|
|
|
|
|
const patch = await Snapshot.patch("invalid-hash-12345")
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch("invalid-hash-12345"))
|
|
|
|
|
expect(patch.files).toEqual([])
|
|
|
|
|
expect(patch.hash).toBe("invalid-hash-12345")
|
|
|
|
|
},
|
|
|
|
@@ -294,18 +312,20 @@ test("revert non-existent file", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Try to revert a file that doesn't exist in the snapshot
|
|
|
|
|
// This should not crash
|
|
|
|
|
expect(
|
|
|
|
|
Snapshot.revert([
|
|
|
|
|
{
|
|
|
|
|
hash: before!,
|
|
|
|
|
files: [`${tmp.path}/nonexistent.txt`],
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
run(tmp.path, (snapshot) =>
|
|
|
|
|
snapshot.revert([
|
|
|
|
|
{
|
|
|
|
|
hash: before!,
|
|
|
|
|
files: [`${tmp.path}/nonexistent.txt`],
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
).resolves.toBeUndefined()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -316,7 +336,7 @@ test("unicode filenames", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const unicodeFiles = [
|
|
|
|
@@ -330,14 +350,14 @@ test("unicode filenames", async () => {
|
|
|
|
|
await Filesystem.write(file.path, file.content)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files.length).toBe(4)
|
|
|
|
|
|
|
|
|
|
for (const file of unicodeFiles) {
|
|
|
|
|
expect(patch.files).toContain(file.path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
for (const file of unicodeFiles) {
|
|
|
|
|
expect(
|
|
|
|
@@ -362,17 +382,17 @@ test.skip("unicode filenames modification and restore", async () => {
|
|
|
|
|
await Filesystem.write(chineseFile, "original chinese")
|
|
|
|
|
await Filesystem.write(cyrillicFile, "original cyrillic")
|
|
|
|
|
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(chineseFile, "modified chinese")
|
|
|
|
|
await Filesystem.write(cyrillicFile, "modified cyrillic")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(chineseFile)
|
|
|
|
|
expect(patch.files).toContain(cyrillicFile)
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(await fs.readFile(chineseFile, "utf-8")).toBe("original chinese")
|
|
|
|
|
expect(await fs.readFile(cyrillicFile, "utf-8")).toBe("original cyrillic")
|
|
|
|
@@ -385,17 +405,17 @@ test("unicode filenames in subdirectories", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`mkdir -p "${tmp.path}/目录/подкаталог"`.quiet()
|
|
|
|
|
const deepFile = fwd(tmp.path, "目录", "подкаталог", "文件.txt")
|
|
|
|
|
await Filesystem.write(deepFile, "deep unicode content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(deepFile)
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
|
.access(deepFile)
|
|
|
|
@@ -411,7 +431,7 @@ test("very long filenames", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const longName = "a".repeat(200) + ".txt"
|
|
|
|
@@ -419,10 +439,10 @@ test("very long filenames", async () => {
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(longFile, "long filename content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(longFile)
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
|
.access(longFile)
|
|
|
|
@@ -438,14 +458,14 @@ test("hidden files", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/.hidden`, "hidden content")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/.gitignore`, "*.log")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/.config`, "config content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, ".hidden"))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, ".config"))
|
|
|
|
@@ -458,7 +478,7 @@ test("nested symlinks", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`mkdir -p ${tmp.path}/sub/dir`.quiet()
|
|
|
|
@@ -466,7 +486,7 @@ test("nested symlinks", async () => {
|
|
|
|
|
await fs.symlink(`${tmp.path}/sub/dir/target.txt`, `${tmp.path}/sub/dir/link.txt`, "file")
|
|
|
|
|
await fs.symlink(`${tmp.path}/sub`, `${tmp.path}/sub-link`, "dir")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "sub", "dir", "link.txt"))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "sub-link"))
|
|
|
|
|
},
|
|
|
|
@@ -478,7 +498,7 @@ test("file permissions and ownership changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Change permissions multiple times
|
|
|
|
@@ -486,7 +506,7 @@ test("file permissions and ownership changes", async () => {
|
|
|
|
|
await $`chmod 755 ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
await $`chmod 644 ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
// Note: git doesn't track permission changes on existing files by default
|
|
|
|
|
// Only tracks executable bit when files are first added
|
|
|
|
|
expect(patch.files.length).toBe(0)
|
|
|
|
@@ -499,13 +519,13 @@ test("circular symlinks", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Create circular symlink
|
|
|
|
|
await fs.symlink(`${tmp.path}/circular`, `${tmp.path}/circular`, "dir").catch(() => {})
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files.length).toBeGreaterThanOrEqual(0) // Should not crash
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -530,7 +550,7 @@ test("source project gitignore is respected - ignored files are not snapshotted"
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Modify tracked files and create new ones - some ignored, some not
|
|
|
|
@@ -539,7 +559,7 @@ test("source project gitignore is respected - ignored files are not snapshotted"
|
|
|
|
|
await Filesystem.write(`${tmp.path}/new-tracked.txt`, "new tracked")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/build/new-build.js`, "new build file")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
// Modified and new tracked files should be in snapshot
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "new-tracked.txt"))
|
|
|
|
@@ -559,14 +579,14 @@ test("gitignore changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/.gitignore`, "*.ignored")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/test.ignored`, "ignored content")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/normal.txt`, "normal content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
// Should track gitignore itself
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
|
|
|
@@ -585,7 +605,7 @@ test("files tracked in snapshot but now gitignored are filtered out", async () =
|
|
|
|
|
fn: async () => {
|
|
|
|
|
// First, create a file and snapshot it
|
|
|
|
|
await Filesystem.write(`${tmp.path}/later-ignored.txt`, "initial content")
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Modify the file (so it appears in diff-files)
|
|
|
|
@@ -597,7 +617,7 @@ test("files tracked in snapshot but now gitignored are filtered out", async () =
|
|
|
|
|
// Also create another tracked file
|
|
|
|
|
await Filesystem.write(`${tmp.path}/still-tracked.txt`, "new tracked file")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
// The file that is now gitignored should NOT appear, even though it was
|
|
|
|
|
// previously tracked and modified
|
|
|
|
@@ -618,7 +638,7 @@ test("gitignore updated between track calls filters from diff", async () => {
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
// a.txt is already committed from bootstrap - track it in snapshot
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Modify a.txt (so it appears in diff-files)
|
|
|
|
@@ -631,11 +651,11 @@ test("gitignore updated between track calls filters from diff", async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/b.txt`, "also modified")
|
|
|
|
|
|
|
|
|
|
// Second track - should not include a.txt even though it changed
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Verify a.txt is NOT in the diff between snapshots
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.some((x) => x.file === "a.txt")).toBe(false)
|
|
|
|
|
|
|
|
|
|
// But .gitignore should be in the diff
|
|
|
|
@@ -652,7 +672,7 @@ test("git info exclude changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const file = `${tmp.path}/.git/info/exclude`
|
|
|
|
@@ -661,12 +681,12 @@ test("git info exclude changes", async () => {
|
|
|
|
|
await Bun.write(`${tmp.path}/ignored.txt`, "ignored content")
|
|
|
|
|
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
|
|
|
|
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.txt"))
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.some((x) => x.file === "normal.txt")).toBe(true)
|
|
|
|
|
expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false)
|
|
|
|
|
},
|
|
|
|
@@ -686,7 +706,7 @@ test("git info exclude keeps global excludes", async () => {
|
|
|
|
|
const prev = process.env.GIT_CONFIG_GLOBAL
|
|
|
|
|
process.env.GIT_CONFIG_GLOBAL = config
|
|
|
|
|
try {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const file = `${tmp.path}/.git/info/exclude`
|
|
|
|
@@ -697,7 +717,7 @@ test("git info exclude keeps global excludes", async () => {
|
|
|
|
|
await Bun.write(`${tmp.path}/info.tmp`, "info content")
|
|
|
|
|
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "normal.txt"))
|
|
|
|
|
expect(patch.files).not.toContain(fwd(tmp.path, "global.tmp"))
|
|
|
|
|
expect(patch.files).not.toContain(fwd(tmp.path, "info.tmp"))
|
|
|
|
@@ -714,7 +734,7 @@ test("concurrent file operations during patch", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Start creating files
|
|
|
|
@@ -727,7 +747,7 @@ test("concurrent file operations during patch", async () => {
|
|
|
|
|
})()
|
|
|
|
|
|
|
|
|
|
// Get patch while files are being created
|
|
|
|
|
const patchPromise = Snapshot.patch(before!)
|
|
|
|
|
const patchPromise = run(tmp.path, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
|
|
|
|
|
await createPromise
|
|
|
|
|
const patch = await patchPromise
|
|
|
|
@@ -746,9 +766,9 @@ test("snapshot state isolation between projects", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp1.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before1 = await Snapshot.track()
|
|
|
|
|
const before1 = await run(tmp1.path, (snapshot) => snapshot.track())
|
|
|
|
|
await Filesystem.write(`${tmp1.path}/project1.txt`, "project1 content")
|
|
|
|
|
const patch1 = await Snapshot.patch(before1!)
|
|
|
|
|
const patch1 = await run(tmp1.path, (snapshot) => snapshot.patch(before1!))
|
|
|
|
|
expect(patch1.files).toContain(fwd(tmp1.path, "project1.txt"))
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -756,9 +776,9 @@ test("snapshot state isolation between projects", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp2.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before2 = await Snapshot.track()
|
|
|
|
|
const before2 = await run(tmp2.path, (snapshot) => snapshot.track())
|
|
|
|
|
await Filesystem.write(`${tmp2.path}/project2.txt`, "project2 content")
|
|
|
|
|
const patch2 = await Snapshot.patch(before2!)
|
|
|
|
|
const patch2 = await run(tmp2.path, (snapshot) => snapshot.patch(before2!))
|
|
|
|
|
expect(patch2.files).toContain(fwd(tmp2.path, "project2.txt"))
|
|
|
|
|
|
|
|
|
|
// Ensure project1 files don't appear in project2
|
|
|
|
@@ -776,20 +796,20 @@ test("patch detects changes in secondary worktree", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
expect(await Snapshot.track()).toBeTruthy()
|
|
|
|
|
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: worktreePath,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
|
|
|
|
await Filesystem.write(worktreeFile, "worktree content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
const patch = await run(worktreePath, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
expect(patch.files).toContain(worktreeFile)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -808,7 +828,7 @@ test("revert only removes files in invoking worktree", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
expect(await Snapshot.track()).toBeTruthy()
|
|
|
|
|
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const primaryFile = `${tmp.path}/worktree.txt`
|
|
|
|
@@ -817,14 +837,14 @@ test("revert only removes files in invoking worktree", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: worktreePath,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const worktreeFile = fwd(worktreePath, "worktree.txt")
|
|
|
|
|
await Filesystem.write(worktreeFile, "worktree content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(before!)
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
const patch = await run(worktreePath, (snapshot) => snapshot.patch(before!))
|
|
|
|
|
await run(worktreePath, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -852,14 +872,14 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
expect(await Snapshot.track()).toBeTruthy()
|
|
|
|
|
expect(await run(tmp.path, (snapshot) => snapshot.track())).toBeTruthy()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: worktreePath,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(worktreePath, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${worktreePath}/worktree-only.txt`, "worktree diff content")
|
|
|
|
@@ -867,7 +887,7 @@ test("diff reports worktree-only/shared edits and ignores primary-only", async (
|
|
|
|
|
await Filesystem.write(`${tmp.path}/shared.txt`, "primary edit")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/primary-only.txt`, "primary change")
|
|
|
|
|
|
|
|
|
|
const diff = await Snapshot.diff(before!)
|
|
|
|
|
const diff = await run(worktreePath, (snapshot) => snapshot.diff(before!))
|
|
|
|
|
expect(diff).toContain("worktree-only.txt")
|
|
|
|
|
expect(diff).toContain("shared.txt")
|
|
|
|
|
expect(diff).not.toContain("primary-only.txt")
|
|
|
|
@@ -886,15 +906,15 @@ test("track with no changes returns same hash", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const hash1 = await Snapshot.track()
|
|
|
|
|
const hash1 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(hash1).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Track again with no changes
|
|
|
|
|
const hash2 = await Snapshot.track()
|
|
|
|
|
const hash2 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(hash2).toBe(hash1!)
|
|
|
|
|
|
|
|
|
|
// Track again
|
|
|
|
|
const hash3 = await Snapshot.track()
|
|
|
|
|
const hash3 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(hash3).toBe(hash1!)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -905,7 +925,7 @@ test("diff function with various changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Make various changes
|
|
|
|
@@ -913,7 +933,7 @@ test("diff function with various changes", async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
|
|
|
|
|
|
|
|
|
const diff = await Snapshot.diff(before!)
|
|
|
|
|
const diff = await run(tmp.path, (snapshot) => snapshot.diff(before!))
|
|
|
|
|
expect(diff).toContain("a.txt")
|
|
|
|
|
expect(diff).toContain("b.txt")
|
|
|
|
|
expect(diff).toContain("new.txt")
|
|
|
|
@@ -926,7 +946,7 @@ test("restore function", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Make changes
|
|
|
|
@@ -935,7 +955,7 @@ test("restore function", async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/b.txt`, "modified")
|
|
|
|
|
|
|
|
|
|
// Restore to original state
|
|
|
|
|
await Snapshot.restore(before!)
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.restore(before!))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -960,20 +980,20 @@ test("revert should not delete files that existed but were deleted in snapshot",
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const snapshot1 = await Snapshot.track()
|
|
|
|
|
const snapshot1 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snapshot1).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
const snapshot2 = await Snapshot.track()
|
|
|
|
|
const snapshot2 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snapshot2).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/a.txt`, "recreated content")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(snapshot2!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(snapshot2!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "a.txt"))
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -992,18 +1012,18 @@ test("revert preserves file that existed in snapshot when deleted then recreated
|
|
|
|
|
fn: async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/existing.txt`, "original content")
|
|
|
|
|
|
|
|
|
|
const snapshot = await Snapshot.track()
|
|
|
|
|
expect(snapshot).toBeTruthy()
|
|
|
|
|
const hash = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(hash).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm ${tmp.path}/existing.txt`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/existing.txt`, "recreated")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/newfile.txt`, "new")
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(snapshot!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(hash!))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "existing.txt"))
|
|
|
|
|
expect(patch.files).toContain(fwd(tmp.path, "newfile.txt"))
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
await fs
|
|
|
|
@@ -1031,7 +1051,7 @@ test("diffFull sets status based on git change type", async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/trim.txt`, "line1\nline2\n")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/delete.txt`, "gone")
|
|
|
|
|
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/grow.txt`, "one\ntwo\n")
|
|
|
|
@@ -1039,10 +1059,10 @@ test("diffFull sets status based on git change type", async () => {
|
|
|
|
|
await $`rm ${tmp.path}/delete.txt`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/added.txt`, "new")
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(4)
|
|
|
|
|
|
|
|
|
|
const added = diffs.find((d) => d.file === "added.txt")
|
|
|
|
@@ -1073,15 +1093,15 @@ test("diffFull with new file additions", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/new.txt`, "new content")
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const newFileDiff = diffs[0]
|
|
|
|
@@ -1111,7 +1131,7 @@ test("diffFull with a large interleaved mixed diff", async () => {
|
|
|
|
|
...bin.map((file, i) => Filesystem.write(file, new Uint8Array([0, i, 255, i % 251]))),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
@@ -1121,10 +1141,10 @@ test("diffFull with a large interleaved mixed diff", async () => {
|
|
|
|
|
...del.map((file) => fs.rm(file)),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs).toHaveLength(ids.length * 4)
|
|
|
|
|
|
|
|
|
|
const map = new Map(diffs.map((item) => [item.file, item]))
|
|
|
|
@@ -1166,17 +1186,17 @@ test("diffFull preserves git diff order across batch boundaries", async () => {
|
|
|
|
|
await $`mkdir -p ${tmp.path}/order`.quiet()
|
|
|
|
|
await Promise.all(ids.map((id) => Filesystem.write(`${tmp.path}/order/${id}.txt`, `before-${id}`)))
|
|
|
|
|
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Promise.all(ids.map((id) => Filesystem.write(`${tmp.path}/order/${id}.txt`, `after-${id}`)))
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const expected = ids.map((id) => `order/${id}.txt`)
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.map((item) => item.file)).toEqual(expected)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -1187,15 +1207,15 @@ test("diffFull with file modifications", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/b.txt`, "modified content")
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const modifiedFileDiff = diffs[0]
|
|
|
|
@@ -1213,15 +1233,15 @@ test("diffFull with file deletions", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const removedFileDiff = diffs[0]
|
|
|
|
@@ -1238,15 +1258,15 @@ test("diffFull with multiple line additions", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/multi.txt`, "line1\nline2\nline3")
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const multiDiff = diffs[0]
|
|
|
|
@@ -1264,16 +1284,16 @@ test("diffFull with addition and deletion", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/added.txt`, "added content")
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(2)
|
|
|
|
|
|
|
|
|
|
const addedFileDiff = diffs.find((d) => d.file === "added.txt")
|
|
|
|
@@ -1296,7 +1316,7 @@ test("diffFull with multiple additions and deletions", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/multi1.txt`, "line1\nline2\nline3")
|
|
|
|
@@ -1304,10 +1324,10 @@ test("diffFull with multiple additions and deletions", async () => {
|
|
|
|
|
await $`rm ${tmp.path}/a.txt`.quiet()
|
|
|
|
|
await $`rm ${tmp.path}/b.txt`.quiet()
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(4)
|
|
|
|
|
|
|
|
|
|
const multi1Diff = diffs.find((d) => d.file === "multi1.txt")
|
|
|
|
@@ -1338,13 +1358,13 @@ test("diffFull with no changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(0)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
@@ -1355,15 +1375,15 @@ test("diffFull with binary file changes", async () => {
|
|
|
|
|
await Instance.provide({
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/binary.bin`, new Uint8Array([0x00, 0x01, 0x02, 0x03]))
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const binaryDiff = diffs[0]
|
|
|
|
@@ -1379,15 +1399,15 @@ test("diffFull with whitespace changes", async () => {
|
|
|
|
|
directory: tmp.path,
|
|
|
|
|
fn: async () => {
|
|
|
|
|
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\nline2")
|
|
|
|
|
const before = await Snapshot.track()
|
|
|
|
|
const before = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(before).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Filesystem.write(`${tmp.path}/whitespace.txt`, "line1\n\nline2\n")
|
|
|
|
|
|
|
|
|
|
const after = await Snapshot.track()
|
|
|
|
|
const after = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(after).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
const diffs = await Snapshot.diffFull(before!, after!)
|
|
|
|
|
const diffs = await run(tmp.path, (snapshot) => snapshot.diffFull(before!, after!))
|
|
|
|
|
expect(diffs.length).toBe(1)
|
|
|
|
|
|
|
|
|
|
const whitespaceDiff = diffs[0]
|
|
|
|
@@ -1404,26 +1424,26 @@ test("revert with overlapping files across patches uses first patch hash", async
|
|
|
|
|
fn: async () => {
|
|
|
|
|
// Write initial content and snapshot
|
|
|
|
|
await Filesystem.write(`${tmp.path}/shared.txt`, "v1")
|
|
|
|
|
const snap1 = await Snapshot.track()
|
|
|
|
|
const snap1 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snap1).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Modify and snapshot again
|
|
|
|
|
await Filesystem.write(`${tmp.path}/shared.txt`, "v2")
|
|
|
|
|
const snap2 = await Snapshot.track()
|
|
|
|
|
const snap2 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snap2).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
// Modify once more so both patches include shared.txt
|
|
|
|
|
await Filesystem.write(`${tmp.path}/shared.txt`, "v3")
|
|
|
|
|
|
|
|
|
|
const patch1 = await Snapshot.patch(snap1!)
|
|
|
|
|
const patch2 = await Snapshot.patch(snap2!)
|
|
|
|
|
const patch1 = await run(tmp.path, (snapshot) => snapshot.patch(snap1!))
|
|
|
|
|
const patch2 = await run(tmp.path, (snapshot) => snapshot.patch(snap2!))
|
|
|
|
|
|
|
|
|
|
// Both patches should include shared.txt
|
|
|
|
|
expect(patch1.files).toContain(fwd(tmp.path, "shared.txt"))
|
|
|
|
|
expect(patch2.files).toContain(fwd(tmp.path, "shared.txt"))
|
|
|
|
|
|
|
|
|
|
// Revert with patch1 first — should use snap1's hash (restoring "v1")
|
|
|
|
|
await Snapshot.revert([patch1, patch2])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch1, patch2]))
|
|
|
|
|
|
|
|
|
|
const content = await fs.readFile(`${tmp.path}/shared.txt`, "utf-8")
|
|
|
|
|
expect(content).toBe("v1")
|
|
|
|
@@ -1440,24 +1460,26 @@ test("revert preserves patch order when the same hash appears again", async () =
|
|
|
|
|
await Filesystem.write(`${tmp.path}/foo/bar`, "v1")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/a.txt`, "v1")
|
|
|
|
|
|
|
|
|
|
const snap1 = await Snapshot.track()
|
|
|
|
|
const snap1 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snap1).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm -rf ${tmp.path}/foo`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/foo`, "v2")
|
|
|
|
|
await Filesystem.write(`${tmp.path}/a.txt`, "v2")
|
|
|
|
|
|
|
|
|
|
const snap2 = await Snapshot.track()
|
|
|
|
|
const snap2 = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snap2).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await $`rm -rf ${tmp.path}/foo`.quiet()
|
|
|
|
|
await Filesystem.write(`${tmp.path}/a.txt`, "v3")
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([
|
|
|
|
|
{ hash: snap1!, files: [fwd(tmp.path, "a.txt")] },
|
|
|
|
|
{ hash: snap2!, files: [fwd(tmp.path, "foo")] },
|
|
|
|
|
{ hash: snap1!, files: [fwd(tmp.path, "foo", "bar")] },
|
|
|
|
|
])
|
|
|
|
|
await run(tmp.path, (snapshot) =>
|
|
|
|
|
snapshot.revert([
|
|
|
|
|
{ hash: snap1!, files: [fwd(tmp.path, "a.txt")] },
|
|
|
|
|
{ hash: snap2!, files: [fwd(tmp.path, "foo")] },
|
|
|
|
|
{ hash: snap1!, files: [fwd(tmp.path, "foo", "bar")] },
|
|
|
|
|
]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
expect(await fs.readFile(`${tmp.path}/a.txt`, "utf-8")).toBe("v1")
|
|
|
|
|
expect((await fs.stat(`${tmp.path}/foo`)).isDirectory()).toBe(true)
|
|
|
|
@@ -1477,16 +1499,16 @@ test("revert handles large mixed batches across chunk boundaries", async () => {
|
|
|
|
|
await $`mkdir -p ${tmp.path}/batch ${tmp.path}/fresh`.quiet()
|
|
|
|
|
await Promise.all(base.map((file, i) => Filesystem.write(file, `base-${i}`)))
|
|
|
|
|
|
|
|
|
|
const snap = await Snapshot.track()
|
|
|
|
|
const snap = await run(tmp.path, (snapshot) => snapshot.track())
|
|
|
|
|
expect(snap).toBeTruthy()
|
|
|
|
|
|
|
|
|
|
await Promise.all(base.map((file, i) => Filesystem.write(file, `next-${i}`)))
|
|
|
|
|
await Promise.all(fresh.map((file, i) => Filesystem.write(file, `fresh-${i}`)))
|
|
|
|
|
|
|
|
|
|
const patch = await Snapshot.patch(snap!)
|
|
|
|
|
const patch = await run(tmp.path, (snapshot) => snapshot.patch(snap!))
|
|
|
|
|
expect(patch.files.length).toBe(base.length + fresh.length)
|
|
|
|
|
|
|
|
|
|
await Snapshot.revert([patch])
|
|
|
|
|
await run(tmp.path, (snapshot) => snapshot.revert([patch]))
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
base.map(async (file, i) => {
|
|
|
|
|