fix(opencode): handle snapshot paths from subdirectories (#33506)
This commit is contained in:
parent
5152150bfe
commit
dcf7b4e792
@ -82,7 +82,9 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
|
|
||||||
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 encodeNulTerminatedPaths = (files: string[]) => files.join("\0") + "\0"
|
||||||
|
const encodeTopLevelLiteralPathspecs = (files: string[]) =>
|
||||||
|
encodeNulTerminatedPaths(files.map((file) => `:(top,literal)${file}`))
|
||||||
|
|
||||||
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 }) {
|
||||||
@ -107,6 +109,8 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
|
|
||||||
const ignore = Effect.fnUntraced(function* (files: string[]) {
|
const ignore = Effect.fnUntraced(function* (files: string[]) {
|
||||||
if (!files.length) return new Set<string>()
|
if (!files.length) return new Set<string>()
|
||||||
|
// check-ignore treats a leading colon as pathspec magic but accepts and echoes a protective ./ prefix.
|
||||||
|
const checkIgnorePaths = files.map((item) => (item.startsWith(":") ? `./${item}` : item))
|
||||||
const check = yield* git(
|
const check = yield* git(
|
||||||
[
|
[
|
||||||
...quote,
|
...quote,
|
||||||
@ -120,12 +124,17 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
"-z",
|
"-z",
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
cwd: state.directory,
|
cwd: state.worktree,
|
||||||
stdin: feed(files),
|
stdin: encodeNulTerminatedPaths(checkIgnorePaths),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (check.code !== 0 && check.code !== 1) return new Set<string>()
|
if (check.code !== 0 && check.code !== 1) return new Set<string>()
|
||||||
return new Set(check.text.split("\0").filter(Boolean))
|
return new Set(
|
||||||
|
check.text
|
||||||
|
.split("\0")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((item) => (item.startsWith("./:") ? item.slice(2) : item)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const drop = Effect.fnUntraced(function* (files: string[]) {
|
const drop = Effect.fnUntraced(function* (files: string[]) {
|
||||||
@ -136,8 +145,8 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
...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: encodeTopLevelLiteralPathspecs(files),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -147,8 +156,8 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
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: encodeTopLevelLiteralPathspecs(files),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (result.code === 0) return
|
if (result.code === 0) return
|
||||||
@ -238,7 +247,7 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], {
|
git([...quote, ...args(["diff-files", "--name-only", "-z", "--", "."])], {
|
||||||
cwd: state.directory,
|
cwd: state.directory,
|
||||||
}),
|
}),
|
||||||
git([...quote, ...args(["ls-files", "--others", "--exclude-standard", "-z", "--", "."])], {
|
git([...quote, ...args(["ls-files", "--full-name", "--others", "--exclude-standard", "-z", "--", "."])], {
|
||||||
cwd: state.directory,
|
cwd: state.directory,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@ -277,7 +286,7 @@ export const layer: Layer.Layer<Service, never, FSUtil.Service | AppProcess.Serv
|
|||||||
(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) => {
|
||||||
|
|||||||
@ -16,6 +16,8 @@ import {
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
|
const it = testEffect(Layer.mergeAll(Snapshot.defaultLayer, FSUtil.defaultLayer, testInstanceStoreLayer))
|
||||||
|
// Windows forbids both * and : in directory names.
|
||||||
|
const nonWindowsIt = process.platform === "win32" ? it.live.skip : it.live
|
||||||
|
|
||||||
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
// Git always outputs /-separated paths internally. Snapshot.patch() joins them
|
||||||
// with path.join (which produces \ on Windows) then normalizes back to /.
|
// with path.join (which produces \ on Windows) then normalizes back to /.
|
||||||
@ -448,6 +450,97 @@ it.live(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live(
|
||||||
|
"subdirectory snapshots include scoped changes only",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const frontend = path.join(dir, "frontend")
|
||||||
|
yield* write(`${frontend}/tracked.txt`, "initial")
|
||||||
|
yield* write(`${frontend}/deleted.txt`, "initial")
|
||||||
|
yield* write(`${dir}/backend/tracked.txt`, "initial")
|
||||||
|
yield* write(`${dir}/backend/deleted.txt`, "initial")
|
||||||
|
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(`${frontend}/tracked.txt`, "changed")
|
||||||
|
yield* write(`${frontend}/untracked.txt`, "new")
|
||||||
|
yield* rm(`${frontend}/deleted.txt`)
|
||||||
|
yield* write(`${dir}/backend/tracked.txt`, "changed")
|
||||||
|
yield* rm(`${dir}/backend/deleted.txt`)
|
||||||
|
const patch = yield* snapshot.patch(before!)
|
||||||
|
const diff = yield* snapshot.diff(before!)
|
||||||
|
expect(patch.files).toContain(fwd(frontend, "tracked.txt"))
|
||||||
|
expect(patch.files).toContain(fwd(frontend, "untracked.txt"))
|
||||||
|
expect(patch.files).toContain(fwd(frontend, "deleted.txt"))
|
||||||
|
expect(patch.files).not.toContain(fwd(dir, "backend", "tracked.txt"))
|
||||||
|
expect(patch.files).not.toContain(fwd(dir, "backend", "deleted.txt"))
|
||||||
|
expect(diff).not.toContain("backend/tracked.txt")
|
||||||
|
expect(diff).not.toContain("backend/deleted.txt")
|
||||||
|
}).pipe(provideInstance(frontend))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
nonWindowsIt(
|
||||||
|
"subdirectory snapshots treat wildcard characters literally",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = path.join(dir, "src*")
|
||||||
|
yield* write(`${subdir}/file.txt`, "initial")
|
||||||
|
yield* write(`${subdir}/later-ignored.txt`, "initial")
|
||||||
|
yield* write(`${dir}/srca/file.txt`, "initial")
|
||||||
|
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`, "changed")
|
||||||
|
yield* write(`${subdir}/later-ignored.txt`, "changed")
|
||||||
|
yield* write(`${subdir}/.gitignore`, "later-ignored.txt\n")
|
||||||
|
yield* write(`${dir}/srca/file.txt`, "changed")
|
||||||
|
const patch = yield* snapshot.patch(before!)
|
||||||
|
const diff = yield* snapshot.diff(before!)
|
||||||
|
expect(patch.files).toContain(fwd(subdir, "file.txt"))
|
||||||
|
expect(patch.files).toContain(fwd(subdir, ".gitignore"))
|
||||||
|
expect(patch.files).not.toContain(fwd(subdir, "later-ignored.txt"))
|
||||||
|
expect(patch.files).not.toContain(fwd(dir, "srca", "file.txt"))
|
||||||
|
expect(diff).toContain("src*/later-ignored.txt")
|
||||||
|
expect(diff).toContain("deleted file mode")
|
||||||
|
expect(diff).not.toContain("srca/file.txt")
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
nonWindowsIt(
|
||||||
|
"subdirectory snapshots treat leading colons literally",
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* scopedGitTmpdir()
|
||||||
|
const subdir = path.join(dir, ":src")
|
||||||
|
yield* write(`${subdir}/kept.txt`, "initial")
|
||||||
|
yield* write(`${subdir}/later-ignored.txt`, "initial")
|
||||||
|
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}/kept.txt`, "changed")
|
||||||
|
yield* write(`${subdir}/later-ignored.txt`, "changed")
|
||||||
|
yield* write(`${subdir}/.gitignore`, "later-ignored.txt\n")
|
||||||
|
const patch = yield* snapshot.patch(before!)
|
||||||
|
const diff = yield* snapshot.diff(before!)
|
||||||
|
expect(patch.files).toContain(fwd(subdir, "kept.txt"))
|
||||||
|
expect(patch.files).toContain(fwd(subdir, ".gitignore"))
|
||||||
|
expect(patch.files).not.toContain(fwd(subdir, "later-ignored.txt"))
|
||||||
|
expect(diff).toContain(":src/later-ignored.txt")
|
||||||
|
expect(diff).toContain("deleted file mode")
|
||||||
|
}).pipe(provideInstance(subdir))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
it.instance(
|
it.instance(
|
||||||
"gitignore changes",
|
"gitignore changes",
|
||||||
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
withTrackedSnapshot(({ tmp, snapshot, before }) =>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user