Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions packages/opencode/src/snapshot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
const state = yield* InstanceState.make<State>(
Effect.fn("Snapshot.state")(function* (ctx) {
const state = {
directory: ctx.directory,
worktree: ctx.worktree,
gitdir: path.join(Global.Path.data, "snapshot", ctx.project.id, Hash.fast(ctx.worktree)),
vcs: ctx.project.vcs,
Expand Down Expand Up @@ -122,7 +121,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
"-z",
],
{
cwd: state.directory,
cwd: state.worktree,
stdin: feed(files),
},
)
Expand All @@ -138,7 +137,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
],
{
cwd: state.directory,
cwd: state.worktree,
stdin: feed(files),
},
)
Expand All @@ -149,7 +148,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
const result = yield* git(
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
{
cwd: state.directory,
cwd: state.worktree,
stdin: feed(files),
},
)
Expand Down Expand Up @@ -198,10 +197,10 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
const [diff, other] = yield* Effect.all(
[
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], {
cwd: state.directory,
cwd: state.worktree,
}),
git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], {
cwd: state.directory,
cwd: state.worktree,
}),
],
{ concurrency: 2 },
Expand Down Expand Up @@ -239,7 +238,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
(yield* Effect.all(
allow.map((item) =>
fs
.stat(path.join(state.directory, item))
.stat(path.join(state.worktree, item))
.pipe(Effect.catch(() => Effect.void))
.pipe(
Effect.map((stat) => {
Expand All @@ -263,7 +262,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
Effect.gen(function* () {
if (!(yield* enabled())) return
if (!(yield* exists(state.gitdir))) return
const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: state.directory })
const result = yield* git(args(["gc", `--prune=${prune}`]), { cwd: state.worktree })
if (result.code !== 0) {
log.warn("cleanup failed", {
exitCode: result.code,
Expand Down Expand Up @@ -293,9 +292,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
log.info("initialized")
}
yield* add()
const result = yield* git(args(["write-tree"]), { cwd: state.directory })
const result = yield* git(args(["write-tree"]), { cwd: state.worktree })
const hash = result.text.trim()
log.info("tracking", { hash, cwd: state.directory, git: state.gitdir })
log.info("tracking", { hash, cwd: state.worktree, git: state.gitdir })
return hash
}),
)
Expand All @@ -308,7 +307,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
const result = yield* git(
[...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])],
{
cwd: state.directory,
cwd: state.worktree,
},
)
if (result.code !== 0) {
Expand Down Expand Up @@ -557,7 +556,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce

const batch = yield* appProcess.run(
ChildProcess.make("git", [...cfg, ...args(["cat-file", "--batch"])], {
cwd: state.directory,
cwd: state.worktree,
extendEnv: true,
}),
{ stdin: refs.map((item) => item.ref).join("\n") + "\n" },
Expand Down Expand Up @@ -638,7 +637,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce

const statuses = yield* git(
[...quote, ...args(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."])],
{ cwd: state.directory },
{ cwd: state.worktree },
)

for (const line of statuses.text.trim().split("\n")) {
Expand All @@ -651,7 +650,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
const numstat = yield* git(
[...quote, ...args(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])],
{
cwd: state.directory,
cwd: state.worktree,
},
)

Expand Down
207 changes: 207 additions & 0 deletions packages/opencode/test/snapshot/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,213 @@ it.live(
}),
)

it.live(
"patch from subdirectory captures changes inside the subdirectory",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${subdir}/date.txt`, "ORIGINAL")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "add src"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${subdir}/date.txt`, "MODIFIED")
const patch = yield* snapshot.patch(before!)
expect(patch.files).toContain(fwd(subdir, "date.txt"))
yield* snapshot.revert([patch])
expect(yield* readText(`${subdir}/date.txt`)).toBe("ORIGINAL")
}).pipe(provideInstance(subdir))
}),
)

it.live(
"patch from subdirectory captures changes outside the subdirectory",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${subdir}/inside.txt`, "INSIDE")
yield* write(`${tmp.path}/outside.txt`, "OUTSIDE")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${tmp.path}/outside.txt`, "OUTSIDE-MODIFIED")
yield* write(`${tmp.path}/new-outside.txt`, "NEW")
const patch = yield* snapshot.patch(before!)
expect(patch.files).toContain(fwd(tmp.path, "outside.txt"))
expect(patch.files).toContain(fwd(tmp.path, "new-outside.txt"))
}).pipe(provideInstance(subdir))
}),
)

it.live(
"diff and diffFull from subdirectory include the whole worktree with content",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/pkg/inner`
yield* mkdirp(subdir)
yield* write(`${tmp.path}/root.txt`, "root-before\n")
yield* write(`${subdir}/inner.txt`, "inner-before\n")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${tmp.path}/root.txt`, "root-after\n")
yield* write(`${subdir}/inner.txt`, "inner-after\n")
const after = yield* snapshot.track()
expect(after).toBeTruthy()
const diff = yield* snapshot.diff(before!)
expect(diff).toContain("root.txt")
expect(diff).toContain("inner.txt")
const diffs = yield* snapshot.diffFull(before!, after!)
const inner = diffs.find((d) => d.file === "pkg/inner/inner.txt")
expect(inner).toBeDefined()
expect(inner!.status).toBe("modified")
expect(inner!.additions).toBeGreaterThan(0)
expect(inner!.deletions).toBeGreaterThan(0)
expect(inner!.patch).toContain("-inner-before")
expect(inner!.patch).toContain("+inner-after")
const root = diffs.find((d) => d.file === "root.txt")
expect(root).toBeDefined()
expect(root!.patch).toContain("-root-before")
expect(root!.patch).toContain("+root-after")
}).pipe(provideInstance(subdir))
}),
)

it.live(
"gitignore is honored when running from a subdirectory",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${tmp.path}/.gitignore`, "*.ignored\nbuild/\n")
yield* write(`${subdir}/keep.txt`, "KEEP")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${subdir}/keep.txt`, "MODIFIED")
yield* write(`${subdir}/scratch.ignored`, "SHOULD-BE-IGNORED")
yield* mkdirp(`${tmp.path}/build`)
yield* write(`${tmp.path}/build/out.js`, "SHOULD-BE-IGNORED")
const patch = yield* snapshot.patch(before!)
expect(patch.files).toContain(fwd(subdir, "keep.txt"))
expect(patch.files).not.toContain(fwd(subdir, "scratch.ignored"))
expect(patch.files).not.toContain(fwd(tmp.path, "build/out.js"))
}).pipe(provideInstance(subdir))
}),
)

it.live(
"large files inside the subdirectory are skipped when running from a subdirectory",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${subdir}/seed.txt`, "seed")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${subdir}/huge.bin`, new Uint8Array(2 * 1024 * 1024 + 1))
yield* write(`${tmp.path}/small.txt`, "small at root")
const patch = yield* snapshot.patch(before!)
expect(patch.files).toContain(fwd(tmp.path, "small.txt"))
expect(patch.files).not.toContain(fwd(subdir, "huge.bin"))
}).pipe(provideInstance(subdir))
}),
)

it.live(
"files staged then later gitignored are dropped when running from a subdirectory",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${subdir}/seed.txt`, "seed")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
yield* write(`${subdir}/later-ignored.txt`, "initial content")
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${subdir}/later-ignored.txt`, "modified content")
yield* write(`${tmp.path}/.gitignore`, "src/later-ignored.txt\n")
yield* write(`${subdir}/still-tracked.txt`, "new tracked file")
const patch = yield* snapshot.patch(before!)
expect(patch.files).not.toContain(fwd(subdir, "later-ignored.txt"))
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
expect(patch.files).toContain(fwd(subdir, "still-tracked.txt"))
}).pipe(provideInstance(subdir))
}),
)

it.live(
"snapshot operations work from a subdirectory of a linked worktree",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const worktreePath = `${tmp.path}-worktree`
yield* exec(tmp.path, ["git", "worktree", "add", worktreePath, "HEAD"])
yield* Effect.addFinalizer(() => cleanupWorktree(tmp.path, worktreePath))
const subdir = `${worktreePath}/src`
yield* mkdirp(subdir)
yield* write(`${subdir}/inner.txt`, "ORIGINAL")
yield* exec(worktreePath, ["git", "add", "."])
yield* exec(worktreePath, ["git", "commit", "-m", "add src in linked worktree"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${subdir}/inner.txt`, "MODIFIED")
yield* write(`${worktreePath}/root.txt`, "ROOT-NEW")
const patch = yield* snapshot.patch(before!)
expect(patch.files).toContain(fwd(subdir, "inner.txt"))
expect(patch.files).toContain(fwd(worktreePath, "root.txt"))
yield* snapshot.revert([patch])
expect(yield* readText(`${subdir}/inner.txt`)).toBe("ORIGINAL")
expect(yield* exists(`${worktreePath}/root.txt`)).toBe(false)
}).pipe(provideInstance(subdir))
}),
)

it.live(
"restore from subdirectory rewrites files across the worktree",
Effect.gen(function* () {
const tmp = yield* bootstrapScoped()
const subdir = `${tmp.path}/src`
yield* mkdirp(subdir)
yield* write(`${tmp.path}/root.txt`, "ROOT-ORIGINAL")
yield* write(`${subdir}/inner.txt`, "INNER-ORIGINAL")
yield* exec(tmp.path, ["git", "add", "."])
yield* exec(tmp.path, ["git", "commit", "-m", "init"])
yield* Effect.gen(function* () {
const snapshot = yield* Snapshot.Service
const before = yield* snapshot.track()
expect(before).toBeTruthy()
yield* write(`${tmp.path}/root.txt`, "ROOT-MODIFIED")
yield* write(`${subdir}/inner.txt`, "INNER-MODIFIED")
yield* write(`${subdir}/added.txt`, "ADDED")
yield* snapshot.restore(before!)
expect(yield* readText(`${tmp.path}/root.txt`)).toBe("ROOT-ORIGINAL")
expect(yield* readText(`${subdir}/inner.txt`)).toBe("INNER-ORIGINAL")
}).pipe(provideInstance(subdir))
}),
)

it.live(
"patch detects changes in secondary worktree",
Effect.gen(function* () {
Expand Down
Loading