mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-06 00:44:07 +01:00
fix: write the graph's tree copy blob by blob so .gitattributes can't rewrite or hide files
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
8750d33ef0
commit
89d4728ecf
3 files changed
+25
-10
No files matched your search
+1
-1
@@ -18,7 +18,7 @@ The recorder runs `strace -f -y -qq -v` with `openat, open, execve, execveat, ge
|
||||
|
||||
## What the key covers
|
||||
|
||||
`lib/test-impact/key.ts` reads everything at the given tree through git, never from the working tree. The graph runs over a checkout of that tree made through a private index, so the tree's own `.gitattributes` can't hide files the way `export-ignore` would in `git archive`. The key is a sha256 over:
|
||||
`lib/test-impact/key.ts` reads everything at the given tree through git, never from the working tree. The graph runs over a copy of that tree written blob by blob, with no attribute processing, so the tree's own `.gitattributes` (`export-ignore`, `working-tree-encoding`, filters) can't hide an import from it. The key is a sha256 over:
|
||||
|
||||
- each traced path's bytes (read with `git cat-file --batch`), or that it is a directory, or that it is absent;
|
||||
- the names in each traced directory listing, at the tree;
|
||||
|
||||
+18
-8
@@ -105,21 +105,31 @@ export function catText(repoRoot: string, treeish: string, rel: string): string
|
||||
}
|
||||
|
||||
/**
|
||||
* A tree-ish checked out into a fresh temp directory with every file staged, so the import graph (git ls-files) sees exactly
|
||||
* that tree. It goes through a private index and checkout-index, never `git archive`, whose export-ignore and export-subst
|
||||
* attributes would let the tree itself hide files from the graph.
|
||||
* A tree-ish written to a fresh temp directory blob by blob from `git cat-file --batch`, with a git index loaded by read-tree,
|
||||
* so the import graph (git ls-files) sees exactly that tree's paths and bytes. Nothing applies attributes: `git archive`
|
||||
* honors export-ignore and checkout-index honors working-tree-encoding and filters, and either would let the tree hide
|
||||
* an import from the graph. Submodules are skipped; symlinks become symlinks.
|
||||
*/
|
||||
export function materializeTree(repoRoot: string, treeish: string): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-test-impact-tree-'));
|
||||
const env = { ...process.env, GIT_INDEX_FILE: `${dir}.index` };
|
||||
try {
|
||||
git(repoRoot, ['read-tree', treeish], undefined, env);
|
||||
git(repoRoot, ['checkout-index', '-a', '-f', `--prefix=${dir}/`], undefined, env);
|
||||
const blobs = [...readTreeIndex(repoRoot, treeish).entries].filter(([, e]) => e.type === 'blob');
|
||||
const buf = blobs.length ? git(repoRoot, ['cat-file', '--batch'], blobs.map(([, e]) => e.oid).join('\n') + '\n') : Buffer.alloc(0);
|
||||
let at = 0;
|
||||
for (const [rel, e] of blobs) {
|
||||
const nl = buf.indexOf(10, at);
|
||||
const size = Number(buf.subarray(at, nl).toString('utf8').split(' ')[2]);
|
||||
const bytes = buf.subarray(nl + 1, nl + 1 + size);
|
||||
at = nl + 1 + size + 1;
|
||||
const abs = path.join(dir, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
if (e.mode === '120000') fs.symlinkSync(bytes.toString('utf8'), abs);
|
||||
else fs.writeFileSync(abs, bytes, { mode: e.mode === '100755' ? 0o755 : 0o644 });
|
||||
}
|
||||
git(dir, ['init', '-q']);
|
||||
git(dir, ['add', '-A', '-f']);
|
||||
git(repoRoot, ['read-tree', treeish], undefined, { ...process.env, GIT_INDEX_FILE: path.join(dir, '.git', 'index') });
|
||||
return dir;
|
||||
} catch (error) { fs.rmSync(dir, { recursive: true, force: true }); throw error; }
|
||||
finally { fs.rmSync(env.GIT_INDEX_FILE, { force: true }); }
|
||||
}
|
||||
|
||||
/** The runtime manifest a key binds: platform, architecture and Bun version. */
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('test-impact key: what changes it', () => {
|
||||
expect(keyAt(repo, 'HEAD', t).key).not.toBe(before.key);
|
||||
});
|
||||
|
||||
test('a branch .gitattributes export-ignore cannot hide a graph input', () => {
|
||||
test('a branch .gitattributes (export-ignore, working-tree-encoding) cannot hide a graph input', () => {
|
||||
const t = 'test/a.test.ts';
|
||||
const repo = fixture({ 'lib/hidden.ts': 'export const x = 1;\n', [t]: "import { x } from '../lib/hidden';\nimport { test } from 'bun:test';\ntest('a', () => x);\n" });
|
||||
traceFrom(repo, t);
|
||||
@@ -89,6 +89,11 @@ describe('test-impact key: what changes it', () => {
|
||||
const after = keyAt(repo, 'HEAD', t);
|
||||
expect(after.inputs).toContainEqual(expect.objectContaining({ kind: 'graph', path: 'lib/hidden.ts' }));
|
||||
expect(after.key).not.toBe(before.key);
|
||||
fs.writeFileSync(path.join(repo.work, '.gitattributes'), `${t} working-tree-encoding=UTF-16LE\n`);
|
||||
fs.writeFileSync(path.join(repo.work, t), Buffer.from(fs.readFileSync(path.join(repo.work, t), 'utf8'), 'utf16le'));
|
||||
repo.git('add', '-A');
|
||||
repo.git('commit', '-q', '-m', 'utf-16 checkout');
|
||||
expect(keyAt(repo, 'HEAD', t).inputs).toContainEqual(expect.objectContaining({ kind: 'graph', path: 'lib/hidden.ts' }));
|
||||
});
|
||||
|
||||
test('a file added under a directory the test listed changes the key', () => {
|
||||
|
||||
Reference in new issue
Block a user