Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions __tests__/git-changed-untracked-dir.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Regression test for #1213: `codegraph sync` silently skips untracked files
* that live inside an untracked directory.
*
* `git status --porcelain` collapses an entirely-untracked directory into a
* single `?? frontend/` entry. getGitChangedFiles must still surface the source
* files inside it (via `-uall`) rather than dropping the whole directory.
*/

import { describe, it, expect, afterEach } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { getGitChangedFiles } from '../src/extraction/index';

function git(cwd: string, args: string[]): void {
execFileSync('git', args, { cwd, stdio: 'pipe' });
}

describe('getGitChangedFiles — untracked directories (#1213)', () => {
const dirs: string[] = [];

function makeRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1213-'));
dirs.push(dir);
git(dir, ['init']);
git(dir, ['config', 'user.email', 'test@example.com']);
git(dir, ['config', 'user.name', 'test']);
fs.writeFileSync(path.join(dir, 'root.js'), 'function foo() {}\n');
git(dir, ['add', 'root.js']);
git(dir, ['commit', '-m', 'init']);
return dir;
}

afterEach(() => {
while (dirs.length) {
fs.rmSync(dirs.pop()!, { recursive: true, force: true });
}
});

it('detects source files inside a fully-untracked directory', () => {
const dir = makeRepo();
fs.mkdirSync(path.join(dir, 'frontend'));
fs.writeFileSync(path.join(dir, 'frontend', 'app.js'), 'function bar() {}\n');

const changes = getGitChangedFiles(dir);

expect(changes).not.toBeNull();
expect(changes!.added).toContain('frontend/app.js');
});

it('still recurses into an untracked embedded git repo (no -uall regression)', () => {
// `-uall` must not break the embedded-repo path: git collapses a nested
// repo to `?? embedded/` regardless of `-uall`, so its files are only
// reachable through collectGitStatus's recursion.
const dir = makeRepo();
const embedded = path.join(dir, 'embedded');
fs.mkdirSync(embedded);
git(embedded, ['init']);
fs.writeFileSync(path.join(embedded, 'inner.js'), 'function baz() {}\n');

const changes = getGitChangedFiles(dir);

expect(changes).not.toBeNull();
expect(changes!.added).toContain('embedded/inner.js');
});
});
10 changes: 8 additions & 2 deletions src/extraction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ interface GitChanges {
* case this cannot see (the child status that would report the deletions is gone
* with it); a full `codegraph index` reconciles that.
*/
function getGitChangedFiles(rootDir: string): GitChanges | null {
export function getGitChangedFiles(rootDir: string): GitChanges | null {
try {
const changes: GitChanges = { modified: [], added: [], deleted: [] };
// Custom extension → language overrides from the project's codegraph.json,
Expand All @@ -1081,7 +1081,13 @@ function getGitChangedFiles(rootDir: string): GitChanges | null {
function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record<string, Language>, includeIgnored: Ignore | null = null, exclude: Ignore | null = null): void {
const output = execFileSync(
'git',
['status', '--porcelain', '--no-renames'],
// `-uall` lists individual untracked files instead of collapsing an
// entirely-untracked directory into one `?? dir/` entry, which would
// otherwise be dropped here (only embedded git repos are recursed into
// below). Nested untracked git repos still collapse to `?? repo/` even
// with `-uall` — git never crosses a repo boundary — so the recursion
// still handles them. (#1213)
['status', '--porcelain', '--no-renames', '-uall'],
{ cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }
);

Expand Down