Compare commits
1 commit
dev
...
fix/snapsh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f88ab8f8d |
2 changed files with 191 additions and 7 deletions
|
|
@ -177,8 +177,39 @@ export namespace Snapshot {
|
||||||
const all = Array.from(new Set([...tracked, ...untracked]))
|
const all = Array.from(new Set([...tracked, ...untracked]))
|
||||||
if (!all.length) return
|
if (!all.length) return
|
||||||
|
|
||||||
|
// Filter out files that are now gitignored even if previously tracked
|
||||||
|
// Files may have been tracked before being gitignored, so we need to check
|
||||||
|
// against the source project's current gitignore rules
|
||||||
|
// Use --no-index to check purely against patterns (ignoring whether file is tracked)
|
||||||
|
const checkArgs = [
|
||||||
|
...quote,
|
||||||
|
"--git-dir",
|
||||||
|
path.join(state.worktree, ".git"),
|
||||||
|
"--work-tree",
|
||||||
|
state.worktree,
|
||||||
|
"check-ignore",
|
||||||
|
"--no-index",
|
||||||
|
"--",
|
||||||
|
...all,
|
||||||
|
]
|
||||||
|
const check = yield* git(checkArgs, { cwd: state.directory })
|
||||||
|
const ignored =
|
||||||
|
check.code === 0 ? new Set(check.text.trim().split("\n").filter(Boolean)) : new Set<string>()
|
||||||
|
const filtered = all.filter((item) => !ignored.has(item))
|
||||||
|
|
||||||
|
// Remove newly-ignored files from snapshot index to prevent re-adding
|
||||||
|
if (ignored.size > 0) {
|
||||||
|
const ignoredFiles = Array.from(ignored)
|
||||||
|
log.info("removing gitignored files from snapshot", { count: ignoredFiles.length })
|
||||||
|
yield* git([...cfg, ...args(["rm", "--cached", "-f", "--", ...ignoredFiles])], {
|
||||||
|
cwd: state.directory,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filtered.length) return
|
||||||
|
|
||||||
const large = (yield* Effect.all(
|
const large = (yield* Effect.all(
|
||||||
all.map((item) =>
|
filtered.map((item) =>
|
||||||
fs
|
fs
|
||||||
.stat(path.join(state.directory, item))
|
.stat(path.join(state.directory, item))
|
||||||
.pipe(Effect.catch(() => Effect.void))
|
.pipe(Effect.catch(() => Effect.void))
|
||||||
|
|
@ -259,14 +290,39 @@ export namespace Snapshot {
|
||||||
log.warn("failed to get diff", { hash, exitCode: result.code })
|
log.warn("failed to get diff", { hash, exitCode: result.code })
|
||||||
return { hash, files: [] }
|
return { hash, files: [] }
|
||||||
}
|
}
|
||||||
|
const files = result.text
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
// Filter out files that are now gitignored
|
||||||
|
if (files.length > 0) {
|
||||||
|
const checkArgs = [
|
||||||
|
...quote,
|
||||||
|
"--git-dir",
|
||||||
|
path.join(state.worktree, ".git"),
|
||||||
|
"--work-tree",
|
||||||
|
state.worktree,
|
||||||
|
"check-ignore",
|
||||||
|
"--no-index",
|
||||||
|
"--",
|
||||||
|
...files,
|
||||||
|
]
|
||||||
|
const check = yield* git(checkArgs, { cwd: state.directory })
|
||||||
|
if (check.code === 0) {
|
||||||
|
const ignored = new Set(check.text.trim().split("\n").filter(Boolean))
|
||||||
|
const filtered = files.filter((item) => !ignored.has(item))
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
files: filtered.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hash,
|
hash,
|
||||||
files: result.text
|
files: files.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
|
||||||
.trim()
|
|
||||||
.split("\n")
|
|
||||||
.map((x) => x.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((x) => path.join(state.worktree, x).replaceAll("\\", "/")),
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -616,6 +672,30 @@ export namespace Snapshot {
|
||||||
} satisfies Row,
|
} satisfies Row,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Filter out files that are now gitignored
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const files = rows.map((r) => r.file)
|
||||||
|
const checkArgs = [
|
||||||
|
...quote,
|
||||||
|
"--git-dir",
|
||||||
|
path.join(state.worktree, ".git"),
|
||||||
|
"--work-tree",
|
||||||
|
state.worktree,
|
||||||
|
"check-ignore",
|
||||||
|
"--no-index",
|
||||||
|
"--",
|
||||||
|
...files,
|
||||||
|
]
|
||||||
|
const check = yield* git(checkArgs, { cwd: state.directory })
|
||||||
|
if (check.code === 0) {
|
||||||
|
const ignored = new Set(check.text.trim().split("\n").filter(Boolean))
|
||||||
|
const filtered = rows.filter((r) => !ignored.has(r.file))
|
||||||
|
rows.length = 0
|
||||||
|
rows.push(...filtered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const step = 100
|
const step = 100
|
||||||
const patch = (file: string, before: string, after: string) =>
|
const patch = (file: string, before: string, after: string) =>
|
||||||
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
|
formatPatch(structuredPatch(file, file, before, after, "", "", { context: Number.MAX_SAFE_INTEGER }))
|
||||||
|
|
|
||||||
|
|
@ -511,6 +511,49 @@ test("circular symlinks", async () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("source project gitignore is respected - ignored files are not snapshotted", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
git: true,
|
||||||
|
init: async (dir) => {
|
||||||
|
// Create gitignore BEFORE any tracking
|
||||||
|
await Filesystem.write(`${dir}/.gitignore`, "*.ignored\nbuild/\nnode_modules/\n")
|
||||||
|
await Filesystem.write(`${dir}/tracked.txt`, "tracked content")
|
||||||
|
await Filesystem.write(`${dir}/ignored.ignored`, "ignored content")
|
||||||
|
await $`mkdir -p ${dir}/build`.quiet()
|
||||||
|
await Filesystem.write(`${dir}/build/output.js`, "build output")
|
||||||
|
await Filesystem.write(`${dir}/normal.js`, "normal js")
|
||||||
|
await $`git add .`.cwd(dir).quiet()
|
||||||
|
await $`git commit -m init`.cwd(dir).quiet()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const before = await Snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
|
// Modify tracked files and create new ones - some ignored, some not
|
||||||
|
await Filesystem.write(`${tmp.path}/tracked.txt`, "modified tracked")
|
||||||
|
await Filesystem.write(`${tmp.path}/new.ignored`, "new ignored")
|
||||||
|
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!)
|
||||||
|
|
||||||
|
// Modified and new tracked files should be in snapshot
|
||||||
|
expect(patch.files).toContain(fwd(tmp.path, "new-tracked.txt"))
|
||||||
|
expect(patch.files).toContain(fwd(tmp.path, "tracked.txt"))
|
||||||
|
|
||||||
|
// Ignored files should NOT be in snapshot
|
||||||
|
expect(patch.files).not.toContain(fwd(tmp.path, "new.ignored"))
|
||||||
|
expect(patch.files).not.toContain(fwd(tmp.path, "ignored.ignored"))
|
||||||
|
expect(patch.files).not.toContain(fwd(tmp.path, "build/output.js"))
|
||||||
|
expect(patch.files).not.toContain(fwd(tmp.path, "build/new-build.js"))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("gitignore changes", async () => {
|
test("gitignore changes", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
|
|
@ -535,6 +578,67 @@ test("gitignore changes", async () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("files tracked in snapshot but now gitignored are filtered out", async () => {
|
||||||
|
await using tmp = await bootstrap()
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
// a.txt is already committed from bootstrap - track it in snapshot
|
||||||
|
const before = await Snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
|
// Modify a.txt (so it appears in diff-files)
|
||||||
|
await Filesystem.write(`${tmp.path}/a.txt`, "modified content")
|
||||||
|
|
||||||
|
// Now add gitignore that would exclude a.txt
|
||||||
|
await Filesystem.write(`${tmp.path}/.gitignore`, "a.txt\n")
|
||||||
|
|
||||||
|
// Also modify b.txt which is not gitignored
|
||||||
|
await Filesystem.write(`${tmp.path}/b.txt`, "also modified")
|
||||||
|
|
||||||
|
const patch = await Snapshot.patch(before!)
|
||||||
|
|
||||||
|
// a.txt is now gitignored and should NOT appear in patch
|
||||||
|
expect(patch.files).not.toContain(fwd(tmp.path, "a.txt"))
|
||||||
|
|
||||||
|
// .gitignore itself should appear (it's a new file)
|
||||||
|
expect(patch.files).toContain(fwd(tmp.path, ".gitignore"))
|
||||||
|
|
||||||
|
// b.txt should appear (not gitignored)
|
||||||
|
expect(patch.files).toContain(fwd(tmp.path, "b.txt"))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("gitignore updated between track calls filters from diff", async () => {
|
||||||
|
await using tmp = await bootstrap()
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
// First track - a.txt is tracked in snapshot
|
||||||
|
const before = await Snapshot.track()
|
||||||
|
expect(before).toBeTruthy()
|
||||||
|
|
||||||
|
// Modify a.txt
|
||||||
|
await Filesystem.write(`${tmp.path}/a.txt`, "modified content")
|
||||||
|
|
||||||
|
// Now add a.txt to gitignore
|
||||||
|
await Filesystem.write(`${tmp.path}/.gitignore`, "a.txt\n")
|
||||||
|
|
||||||
|
// Second track - should not include a.txt even though it changed
|
||||||
|
const after = await Snapshot.track()
|
||||||
|
expect(after).toBeTruthy()
|
||||||
|
|
||||||
|
// Verify a.txt is NOT in the diff between snapshots
|
||||||
|
const diffs = await Snapshot.diffFull(before!, after!)
|
||||||
|
expect(diffs.some((x) => x.file === "a.txt")).toBe(false)
|
||||||
|
|
||||||
|
// But .gitignore should be in the diff
|
||||||
|
expect(diffs.some((x) => x.file === ".gitignore")).toBe(true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("git info exclude changes", async () => {
|
test("git info exclude changes", async () => {
|
||||||
await using tmp = await bootstrap()
|
await using tmp = await bootstrap()
|
||||||
await Instance.provide({
|
await Instance.provide({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue