Compare commits
2 commits
dev
...
oc-snapsho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f7a1c63c |
||
|
|
4a608b2026 |
2 changed files with 155 additions and 32 deletions
|
|
@ -85,6 +85,10 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd]
|
const args = (cmd: string[]) => ["--git-dir", state.gitdir, "--work-tree", state.worktree, ...cmd]
|
||||||
|
|
||||||
const feed = (list: string[]) => list.join("\0") + "\0"
|
const feed = (list: string[]) => list.join("\0") + "\0"
|
||||||
|
const feedSpec = (list: string[]) => feed(list.map((item) => `:(top,literal)${item}`))
|
||||||
|
|
||||||
|
const scope = path.relative(state.worktree, state.directory).replaceAll("\\", "/")
|
||||||
|
const spec = scope ? `:(top,literal)${scope}` : "."
|
||||||
|
|
||||||
const git = Effect.fnUntraced(
|
const git = Effect.fnUntraced(
|
||||||
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string>; stdin?: string }) {
|
function* (cmd: string[], opts?: { cwd?: string; env?: Record<string, string>; stdin?: string }) {
|
||||||
|
|
@ -122,7 +126,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
"-z",
|
"-z",
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
stdin: feed(files),
|
stdin: feed(files),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -138,8 +142,8 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
|
...args(["rm", "--cached", "-f", "--ignore-unmatch", "--pathspec-from-file=-", "--pathspec-file-nul"]),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
stdin: feed(files),
|
stdin: feedSpec(files),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -149,8 +153,8 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
const result = yield* git(
|
const result = yield* git(
|
||||||
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
|
[...cfg, ...args(["add", "--all", "--sparse", "--pathspec-from-file=-", "--pathspec-file-nul"])],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
stdin: feed(files),
|
stdin: feedSpec(files),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (result.code === 0) return
|
if (result.code === 0) return
|
||||||
|
|
@ -197,11 +201,11 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
yield* sync()
|
yield* sync()
|
||||||
const [diff, other] = yield* Effect.all(
|
const [diff, other] = yield* Effect.all(
|
||||||
[
|
[
|
||||||
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], {
|
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", spec])], {
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
}),
|
}),
|
||||||
git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], {
|
git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", spec])], {
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
{ concurrency: 2 },
|
{ concurrency: 2 },
|
||||||
|
|
@ -239,7 +243,7 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
(yield* Effect.all(
|
(yield* Effect.all(
|
||||||
allow.map((item) =>
|
allow.map((item) =>
|
||||||
fs
|
fs
|
||||||
.stat(path.join(state.directory, item))
|
.stat(path.join(state.worktree, item))
|
||||||
.pipe(Effect.catch(() => Effect.void))
|
.pipe(Effect.catch(() => Effect.void))
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.map((stat) => {
|
Effect.map((stat) => {
|
||||||
|
|
@ -306,9 +310,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* add()
|
yield* add()
|
||||||
const result = yield* git(
|
const result = yield* git(
|
||||||
[...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", "."])],
|
[...quote, ...args(["diff", "--cached", "--no-ext-diff", "--name-only", hash, "--", spec])],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
|
|
@ -338,24 +342,47 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
return yield* locked(
|
return yield* locked(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
log.info("restore", { commit: snapshot })
|
log.info("restore", { commit: snapshot })
|
||||||
const result = yield* git([...core, ...args(["read-tree", snapshot])], { cwd: state.worktree })
|
const listed = yield* git([...quote, ...args(["ls-tree", "-r", "-z", "--name-only", snapshot, "--", spec])], {
|
||||||
if (result.code === 0) {
|
cwd: state.worktree,
|
||||||
const checkout = yield* git([...core, ...args(["checkout-index", "-a", "-f"])], {
|
})
|
||||||
cwd: state.worktree,
|
if (listed.code !== 0) {
|
||||||
})
|
log.error("failed to list snapshot files", {
|
||||||
if (checkout.code === 0) return
|
|
||||||
log.error("failed to restore snapshot", {
|
|
||||||
snapshot,
|
snapshot,
|
||||||
exitCode: checkout.code,
|
exitCode: listed.code,
|
||||||
stderr: checkout.stderr,
|
stderr: listed.stderr,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.error("failed to restore snapshot", {
|
const files = listed.text.split("\0").filter(Boolean)
|
||||||
snapshot,
|
if (!files.length) return
|
||||||
exitCode: result.code,
|
|
||||||
stderr: result.stderr,
|
const index = path.join(state.gitdir, "restore.index")
|
||||||
})
|
yield* remove(index)
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const result = yield* git([...core, ...args(["read-tree", `--index-output=${index}`, snapshot])], {
|
||||||
|
cwd: state.worktree,
|
||||||
|
})
|
||||||
|
if (result.code === 0) {
|
||||||
|
const checkout = yield* git([...core, ...args(["checkout-index", "-f", "--stdin", "-z"])], {
|
||||||
|
cwd: state.worktree,
|
||||||
|
env: { GIT_INDEX_FILE: index },
|
||||||
|
stdin: feed(files),
|
||||||
|
})
|
||||||
|
if (checkout.code === 0) return
|
||||||
|
log.error("failed to restore snapshot", {
|
||||||
|
snapshot,
|
||||||
|
exitCode: checkout.code,
|
||||||
|
stderr: checkout.stderr,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.error("failed to restore snapshot", {
|
||||||
|
snapshot,
|
||||||
|
exitCode: result.code,
|
||||||
|
stderr: result.stderr,
|
||||||
|
})
|
||||||
|
}).pipe(Effect.ensuring(remove(index)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -479,9 +506,12 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
return yield* locked(
|
return yield* locked(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* add()
|
yield* add()
|
||||||
const result = yield* git([...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", "."])], {
|
const result = yield* git(
|
||||||
cwd: state.worktree,
|
[...quote, ...args(["diff", "--cached", "--no-ext-diff", hash, "--", spec])],
|
||||||
})
|
{
|
||||||
|
cwd: state.worktree,
|
||||||
|
},
|
||||||
|
)
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
log.warn("failed to get diff", {
|
log.warn("failed to get diff", {
|
||||||
hash,
|
hash,
|
||||||
|
|
@ -637,8 +667,8 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
const status = new Map<string, "added" | "deleted" | "modified">()
|
const status = new Map<string, "added" | "deleted" | "modified">()
|
||||||
|
|
||||||
const statuses = yield* git(
|
const statuses = yield* git(
|
||||||
[...quote, ...args(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."])],
|
[...quote, ...args(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", spec])],
|
||||||
{ cwd: state.directory },
|
{ cwd: state.worktree },
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const line of statuses.text.trim().split("\n")) {
|
for (const line of statuses.text.trim().split("\n")) {
|
||||||
|
|
@ -649,9 +679,9 @@ export const layer: Layer.Layer<Service, never, AppFileSystem.Service | AppProce
|
||||||
}
|
}
|
||||||
|
|
||||||
const numstat = yield* git(
|
const numstat = yield* git(
|
||||||
[...quote, ...args(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])],
|
[...quote, ...args(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", spec])],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -476,6 +476,99 @@ it.instance(
|
||||||
{ git: true },
|
{ git: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"subdirectory instances stage snapshot files relative to the worktree root",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = `${dir}/src`
|
||||||
|
yield* mkdirp(subdir)
|
||||||
|
yield* write(`${subdir}/tracked.txt`, "tracked content")
|
||||||
|
yield* exec(dir, ["git", "add", "."])
|
||||||
|
yield* exec(dir, ["git", "commit", "-m", "add subdir"])
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const snapshot = yield* Snapshot.Service
|
||||||
|
const before = yield* snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
yield* write(`${subdir}/date.txt`, "subdirectory content")
|
||||||
|
const patch = yield* snapshot.patch(before!)
|
||||||
|
expect(patch.files).toContain(fwd(subdir, "date.txt"))
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"subdirectory instances keep gitignored snapshot files out of patches",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = `${dir}/src`
|
||||||
|
yield* mkdirp(subdir)
|
||||||
|
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(`${subdir}/.gitignore`, "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(subdir, ".gitignore"))
|
||||||
|
expect(patch.files).toContain(fwd(subdir, "still-tracked.txt"))
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"subdirectory restore does not overwrite files outside the subdirectory",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = `${dir}/src`
|
||||||
|
yield* write(`${dir}/root.txt`, "original root")
|
||||||
|
yield* write(`${subdir}/file.txt`, "original src")
|
||||||
|
yield* exec(dir, ["git", "add", "."])
|
||||||
|
yield* exec(dir, ["git", "commit", "-m", "init"])
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const snapshot = yield* Snapshot.Service
|
||||||
|
yield* write(`${dir}/root.txt`, "root snapshot")
|
||||||
|
expect(yield* snapshot.track()).toBeTruthy()
|
||||||
|
}).pipe(provideInstance(dir))
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const snapshot = yield* Snapshot.Service
|
||||||
|
yield* write(`${subdir}/file.txt`, "src snapshot")
|
||||||
|
const before = yield* snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
yield* write(`${dir}/root.txt`, "root current")
|
||||||
|
yield* write(`${subdir}/file.txt`, "src current")
|
||||||
|
yield* snapshot.restore(before!)
|
||||||
|
expect(yield* readText(`${dir}/root.txt`)).toBe("root current")
|
||||||
|
expect(yield* readText(`${subdir}/file.txt`)).toBe("src snapshot")
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"subdirectory scope is treated as a literal git pathspec",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = `${dir}/src*`
|
||||||
|
const sibling = `${dir}/srca`
|
||||||
|
yield* write(`${subdir}/file.txt`, "literal original")
|
||||||
|
yield* write(`${sibling}/file.txt`, "sibling original")
|
||||||
|
yield* exec(dir, ["git", "add", "."])
|
||||||
|
yield* exec(dir, ["git", "commit", "-m", "init"])
|
||||||
|
yield* Effect.gen(function* () {
|
||||||
|
const snapshot = yield* Snapshot.Service
|
||||||
|
const before = yield* snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
yield* write(`${subdir}/file.txt`, "literal modified")
|
||||||
|
yield* write(`${sibling}/file.txt`, "sibling modified")
|
||||||
|
const patch = yield* snapshot.patch(before!)
|
||||||
|
expect(patch.files).toContain(fwd(subdir, "file.txt"))
|
||||||
|
expect(patch.files).not.toContain(fwd(sibling, "file.txt"))
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"gitignore updated between track calls filters from diff",
|
"gitignore updated between track calls filters from diff",
|
||||||
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue