extraction-old-git.test.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /**
  2. * Regression: git older than 2.36 rejects `ls-files -s --recurse-submodules` (#1549).
  3. *
  4. * Kept in its own file rather than appended to extraction.test.ts: that suite
  5. * loads every tree-sitter grammar in `beforeAll`, and running a git-scan case
  6. * after it pushed the worker past its memory ceiling.
  7. */
  8. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  9. import * as fs from 'fs';
  10. import * as path from 'path';
  11. import * as os from 'os';
  12. import { execFileSync } from 'child_process';
  13. import { scanDirectory } from '../src/extraction';
  14. function createTempDir(): string {
  15. return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-'));
  16. }
  17. // git < 2.36 rejects `ls-files -s --recurse-submodules` outright: the guard in
  18. // builtin/ls-files.c listed `show_stage` among the modes that die, and it was
  19. // only dropped in 2.36. The die is unconditional — it does not check whether the
  20. // repo has submodules — so on Ubuntu 22.04 (git 2.34.1), Debian 11 (2.30.2) and
  21. // older, every call threw, `getGitVisibleFiles` swallowed it, and the whole
  22. // git-visible path went with it: `includeIgnored`, gitlink recursion and the
  23. // `codegraph.json` `include` allowlist all silently stopped applying (#1549).
  24. //
  25. // A PATH shim reproduces that on any git version, which is what makes this
  26. // testable in CI at all.
  27. describe('Old git without `ls-files -s --recurse-submodules` support (#1549)', () => {
  28. let tempDir: string;
  29. let originalPath: string | undefined;
  30. const runGit = (cwd: string, ...args: string[]) =>
  31. execFileSync('git', args, { cwd, stdio: 'pipe' });
  32. const makeRepo = (dir: string, base: string) => {
  33. fs.mkdirSync(dir, { recursive: true });
  34. runGit(dir, 'init', '-q');
  35. runGit(dir, 'config', 'user.email', 'test@test.com');
  36. runGit(dir, 'config', 'user.name', 'Test');
  37. fs.writeFileSync(path.join(dir, `${base}.ts`), `export const ${base} = 1;`);
  38. runGit(dir, 'add', '-A');
  39. runGit(dir, 'commit', '-q', '-m', `${base} init`);
  40. };
  41. /** A `git` that dies exactly like < 2.36 when it sees -s with --recurse-submodules. */
  42. const installOldGitShim = () => {
  43. const shimDir = path.join(tempDir, '.shim');
  44. fs.mkdirSync(shimDir, { recursive: true });
  45. const realGit = execFileSync('which', ['git']).toString().trim();
  46. const shim = path.join(shimDir, 'git');
  47. fs.writeFileSync(
  48. shim,
  49. [
  50. '#!/bin/sh',
  51. 'for a in "$@"; do',
  52. ' [ "$a" = "--recurse-submodules" ] && rs=1',
  53. ' [ "$a" = "-s" ] && st=1',
  54. 'done',
  55. 'if [ -n "$rs" ] && [ -n "$st" ]; then',
  56. ' echo "fatal: ls-files --recurse-submodules unsupported mode" >&2',
  57. ' exit 128',
  58. 'fi',
  59. `exec ${JSON.stringify(realGit)} "$@"`,
  60. ].join('\n'),
  61. );
  62. fs.chmodSync(shim, 0o755);
  63. originalPath = process.env.PATH;
  64. process.env.PATH = `${shimDir}:${originalPath ?? ''}`;
  65. };
  66. beforeEach(() => {
  67. tempDir = createTempDir();
  68. });
  69. afterEach(() => {
  70. if (originalPath !== undefined) process.env.PATH = originalPath;
  71. originalPath = undefined;
  72. });
  73. it('still honours includeIgnored when `ls-files --recurse-submodules` is unsupported', () => {
  74. const root = path.join(tempDir, 'root');
  75. makeRepo(root, 'a');
  76. // An embedded repo that .gitignore excludes but codegraph.json opts back in.
  77. makeRepo(path.join(root, 'dir_b'), 'b');
  78. fs.writeFileSync(path.join(root, '.gitignore'), 'dir_b/\n');
  79. fs.writeFileSync(
  80. path.join(root, 'codegraph.json'),
  81. JSON.stringify({ includeIgnored: ['dir_b/'] }),
  82. );
  83. runGit(root, 'add', '-A');
  84. runGit(root, 'commit', '-q', '-m', 'ignore dir_b');
  85. // Baseline: the real git resolves both files.
  86. const withRealGit = scanDirectory(root);
  87. expect(withRealGit).toContain('a.ts');
  88. expect(withRealGit).toContain(path.join('dir_b', 'b.ts'));
  89. installOldGitShim();
  90. // The opted-in file must survive the unsupported-mode failure, not vanish.
  91. const withOldGit = scanDirectory(root);
  92. expect(withOldGit).toContain('a.ts');
  93. expect(withOldGit).toContain(path.join('dir_b', 'b.ts'));
  94. });
  95. });