Files
codegraph/__tests__/extraction-old-git.test.ts
T
a961491cea fix(extraction): keep git-aware scans working before Git 2.36 (#1793)
Land upstream PR #1604 by @maxmilian from commit
27c149524d968485164fb43bdec994fdb9323c75 on current main cece0720.
Fixes #1549.

Use the upstream lsFilesStaged helper at both collection and embedded-repo
discovery call sites. Retry without --recurse-submodules when staged
recursive listing fails, retaining the mode bits needed for gitlinks.

Keep the upstream regression test byte-for-byte and preserve main's
existing extraction/watcher changes, including #1728. Resolve the
changelog conflict with a concise Unreleased entry, and keep the existing
collectGitFiles documentation attached to its function.

Verified on Linux x86_64 with Node 22.19.0 and Git 2.47.3:
- The unchanged upstream test fails against main: expected [ 'a.ts' ]
  to include 'dir_b/b.ts'; it passes with the fix.
- The same persistent fixture under a PATH shim rejecting -s with
  --recurse-submodules (exit 128) changes scanDirectory from [a.ts]
  to [a.ts, dir_b/b.ts], and gitlink watcher roots from [] to [lib/].
- Real-Git controls return both files and the watcher root in both arms.
- 69 related scope/config tests and 23 extraction scanning tests pass.
- npx tsc --noEmit passes.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Co-authored-by: Max Hsu <maxmilian@gmail.com>
Co-authored-by: newshowardz777 <newshowardz777@users.noreply.github.com>
2026-09-08 14:37:46 -05:00

108 lines
4.0 KiB
TypeScript

/**
* Regression: git older than 2.36 rejects `ls-files -s --recurse-submodules` (#1549).
*
* Kept in its own file rather than appended to extraction.test.ts: that suite
* loads every tree-sitter grammar in `beforeAll`, and running a git-scan case
* after it pushed the worker past its memory ceiling.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { execFileSync } from 'child_process';
import { scanDirectory } from '../src/extraction';
function createTempDir(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
}
// git < 2.36 rejects `ls-files -s --recurse-submodules` outright: the guard in
// builtin/ls-files.c listed `show_stage` among the modes that die, and it was
// only dropped in 2.36. The die is unconditional — it does not check whether the
// repo has submodules — so on Ubuntu 22.04 (git 2.34.1), Debian 11 (2.30.2) and
// older, every call threw, `getGitVisibleFiles` swallowed it, and the whole
// git-visible path went with it: `includeIgnored`, gitlink recursion and the
// `codegraph.json` `include` allowlist all silently stopped applying (#1549).
//
// A PATH shim reproduces that on any git version, which is what makes this
// testable in CI at all.
describe('Old git without `ls-files -s --recurse-submodules` support (#1549)', () => {
let tempDir: string;
let originalPath: string | undefined;
const runGit = (cwd: string, ...args: string[]) =>
execFileSync('git', args, { cwd, stdio: 'pipe' });
const makeRepo = (dir: string, base: string) => {
fs.mkdirSync(dir, { recursive: true });
runGit(dir, 'init', '-q');
runGit(dir, 'config', 'user.email', 'test@test.com');
runGit(dir, 'config', 'user.name', 'Test');
fs.writeFileSync(path.join(dir, `${base}.ts`), `export const ${base} = 1;`);
runGit(dir, 'add', '-A');
runGit(dir, 'commit', '-q', '-m', `${base} init`);
};
/** A `git` that dies exactly like < 2.36 when it sees -s with --recurse-submodules. */
const installOldGitShim = () => {
const shimDir = path.join(tempDir, '.shim');
fs.mkdirSync(shimDir, { recursive: true });
const realGit = execFileSync('which', ['git']).toString().trim();
const shim = path.join(shimDir, 'git');
fs.writeFileSync(
shim,
[
'#!/bin/sh',
'for a in "$@"; do',
' [ "$a" = "--recurse-submodules" ] && rs=1',
' [ "$a" = "-s" ] && st=1',
'done',
'if [ -n "$rs" ] && [ -n "$st" ]; then',
' echo "fatal: ls-files --recurse-submodules unsupported mode" >&2',
' exit 128',
'fi',
`exec ${JSON.stringify(realGit)} "$@"`,
].join('\n'),
);
fs.chmodSync(shim, 0o755);
originalPath = process.env.PATH;
process.env.PATH = `${shimDir}:${originalPath ?? ''}`;
};
beforeEach(() => {
tempDir = createTempDir();
});
afterEach(() => {
if (originalPath !== undefined) process.env.PATH = originalPath;
originalPath = undefined;
});
it('still honours includeIgnored when `ls-files --recurse-submodules` is unsupported', () => {
const root = path.join(tempDir, 'root');
makeRepo(root, 'a');
// An embedded repo that .gitignore excludes but codegraph.json opts back in.
makeRepo(path.join(root, 'dir_b'), 'b');
fs.writeFileSync(path.join(root, '.gitignore'), 'dir_b/\n');
fs.writeFileSync(
path.join(root, 'codegraph.json'),
JSON.stringify({ includeIgnored: ['dir_b/'] }),
);
runGit(root, 'add', '-A');
runGit(root, 'commit', '-q', '-m', 'ignore dir_b');
// Baseline: the real git resolves both files.
const withRealGit = scanDirectory(root);
expect(withRealGit).toContain('a.ts');
expect(withRealGit).toContain(path.join('dir_b', 'b.ts'));
installOldGitShim();
// The opted-in file must survive the unsupported-mode failure, not vanish.
const withOldGit = scanDirectory(root);
expect(withOldGit).toContain('a.ts');
expect(withOldGit).toContain(path.join('dir_b', 'b.ts'));
});
});