mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-14 16:52:09 +08:00
feat(init): --local-only keeps .codegraph/ out of git entirely (#3104)
The generated `.codegraph/.gitignore` ignores everything but itself, so the rule can be committed and shared. For someone using CodeGraph alone, that leaves `.codegraph/` showing as untracked in `git status`, with a file to commit that nobody else needs. `codegraph init --local-only` makes the file the wildcard alone, so it ignores itself too and nothing of CodeGraph's shows in git. It works on a new project and on one that's already initialized, where it converts the file without indexing again. Plain `codegraph init` is unchanged. Only a file CodeGraph generated is replaced. A file with the user's own rules is left alone, and so is a default the repository has committed, because rewriting a tracked file would show up as a change, the opposite of what the flag is for. Both cases say why they were left. The local-only file carries no generated header, so every later open leaves it alone. A flag rather than the proposed CODEGRAPH_LOCAL_GITIGNORE env var: the file is only written when it is missing, so an env var does nothing for a project that already has one, which is the reporter's case; an env var in a shell profile would also silently stop every new project sharing the rule, team repositories included. The flag is also what the issue proposed. Validation: 22 new tests, the CLI ones against real git repositories. They cover `git status --porcelain` staying empty, the default unchanged without the flag, conversion leaving the database untouched, and the file surviving sync, status, index and a plain init. Reimplements PR 1013 by @jiangyq9. Fixes #910 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
27c904b011
commit
fb71a9f4b3
@@ -27,6 +27,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
- New `codegraph cycles` command lists the groups of files that import each other in a circle, as text or `--json`, and exits 1 when it finds any, so it works as a pre-commit or CI check (#888). Thanks @jiangyq9 and @goutham80808.
|
||||
- `codegraph init --local-only` keeps `.codegraph/` out of git entirely, so nothing of CodeGraph's shows in `git status`, and it works on a project that's already set up, leaving a `.codegraph/.gitignore` you've edited or committed alone (#910). Thanks @jadchene, @jiangyq9, @Wood-Q and @walterf3.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ The exact text is `src/mcp/server-instructions.ts` — the single source of trut
|
||||
codegraph # Run interactive installer
|
||||
codegraph install # Run installer (explicit)
|
||||
codegraph uninstall # Remove CodeGraph from your agents AND the CLI (--keep-cli for configs only)
|
||||
codegraph init [path] # Initialize a project + build its graph (one step)
|
||||
codegraph init [path] # Initialize a project + build its graph (one step; --local-only keeps .codegraph/ out of git)
|
||||
codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
|
||||
codegraph index [path] # Full index (--force to re-index, --quiet for less output)
|
||||
codegraph sync [path] # Incremental update
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* `codegraph init --local-only` (#910): a `.codegraph/.gitignore` that ignores
|
||||
* itself, so a project's CodeGraph data leaves no trace in `git status`.
|
||||
*
|
||||
* The default stays as it was, because a committed `.gitignore` is how a team
|
||||
* shares the rule. So these also pin what must NOT be rewritten: a file with the
|
||||
* user's own rules, and a file a repository has already committed.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { execFileSync, spawnSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { getCodeGraphDir, makeGitignoreLocalOnly } from '../src/directory';
|
||||
import CodeGraph from '../src/index';
|
||||
|
||||
const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
|
||||
|
||||
// These index a project and spawn the real CLI, up to several times a test.
|
||||
vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 });
|
||||
|
||||
const LOCAL_ONLY = '*\n';
|
||||
const DEFAULT = `# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
`;
|
||||
const STALE_DEFAULT = `# CodeGraph data files
|
||||
*.db
|
||||
*.db-wal
|
||||
*.db-shm
|
||||
cache/
|
||||
.dirty
|
||||
`;
|
||||
|
||||
describe('local-only .codegraph/.gitignore', () => {
|
||||
let dir: string;
|
||||
let gitignore: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-local-only-'));
|
||||
gitignore = path.join(dir, '.codegraph', '.gitignore');
|
||||
fs.writeFileSync(path.join(dir, 'a.ts'), 'export const a = 1;\n');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function seedGitignore(content: string): void {
|
||||
fs.mkdirSync(path.dirname(gitignore), { recursive: true });
|
||||
fs.writeFileSync(gitignore, content);
|
||||
}
|
||||
|
||||
function git(...args: string[]): string {
|
||||
return execFileSync('git', args, {
|
||||
cwd: dir,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: 'CodeGraph Test',
|
||||
GIT_AUTHOR_EMAIL: 'codegraph-test@example.invalid',
|
||||
GIT_COMMITTER_NAME: 'CodeGraph Test',
|
||||
GIT_COMMITTER_EMAIL: 'codegraph-test@example.invalid',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function initRepo(): void {
|
||||
git('init', '-q');
|
||||
git('add', 'a.ts');
|
||||
git('commit', '-qm', 'baseline');
|
||||
}
|
||||
|
||||
function codegraph(...args: string[]): { code: number | null; output: string } {
|
||||
const run = spawnSync(process.execPath, [BIN, ...args], {
|
||||
cwd: dir,
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', NO_COLOR: '1', DO_NOT_TRACK: '1', CODEGRAPH_TELEMETRY: '0' },
|
||||
});
|
||||
return { code: run.status, output: run.stdout + run.stderr };
|
||||
}
|
||||
|
||||
describe('makeGitignoreLocalOnly', () => {
|
||||
it('writes the wildcard alone when there is no file', () => {
|
||||
fs.mkdirSync(path.dirname(gitignore));
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
});
|
||||
|
||||
it('writes into the directory the project really uses', () => {
|
||||
const custom = process.env.CODEGRAPH_DIR;
|
||||
process.env.CODEGRAPH_DIR = '.cg-custom';
|
||||
try {
|
||||
fs.mkdirSync(getCodeGraphDir(dir));
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
expect(fs.readFileSync(path.join(dir, '.cg-custom', '.gitignore'), 'utf-8')).toBe(LOCAL_ONLY);
|
||||
} finally {
|
||||
if (custom === undefined) delete process.env.CODEGRAPH_DIR;
|
||||
else process.env.CODEGRAPH_DIR = custom;
|
||||
}
|
||||
});
|
||||
|
||||
it('replaces the default CodeGraph generates, as the code generates it', () => {
|
||||
CodeGraph.initSync(dir).close();
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(DEFAULT);
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
});
|
||||
|
||||
it('replaces a default checked out with Windows line endings', () => {
|
||||
seedGitignore(DEFAULT.replace(/\n/g, '\r\n'));
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
});
|
||||
|
||||
it('replaces a default from before the wildcard', () => {
|
||||
seedGitignore(STALE_DEFAULT);
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['with a newline', '*\n'],
|
||||
['with a Windows newline', '*\r\n'],
|
||||
])('leaves a file that is already local-only, %s', (_label, content) => {
|
||||
seedGitignore(content);
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('unchanged');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(content);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['the user wrote', 'codegraph.db\n'],
|
||||
['we generated and the user added to', `${DEFAULT}!shared-notes.md\n`],
|
||||
])('leaves a file %s', (_label, content) => {
|
||||
seedGitignore(content);
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('customized');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(content);
|
||||
});
|
||||
|
||||
it('leaves a default the repository has committed', () => {
|
||||
seedGitignore(DEFAULT);
|
||||
initRepo();
|
||||
git('add', '.codegraph/.gitignore');
|
||||
git('commit', '-qm', 'share the rule');
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('tracked');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(DEFAULT);
|
||||
expect(git('status', '--porcelain')).toBe('');
|
||||
});
|
||||
|
||||
it('replaces a default that is only untracked', () => {
|
||||
seedGitignore(DEFAULT);
|
||||
initRepo();
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
});
|
||||
|
||||
it('works outside a git repository', () => {
|
||||
seedGitignore(DEFAULT);
|
||||
|
||||
expect(makeGitignoreLocalOnly(dir)).toBe('written');
|
||||
});
|
||||
});
|
||||
|
||||
describe('codegraph init', () => {
|
||||
it('leaves nothing in git status with --local-only', () => {
|
||||
initRepo();
|
||||
|
||||
const { code, output } = codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).toContain('.codegraph/.gitignore now ignores itself');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
expect(git('status', '--porcelain')).toBe('');
|
||||
});
|
||||
|
||||
it('still builds the index', () => {
|
||||
codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(fs.existsSync(path.join(dir, '.codegraph', 'codegraph.db'))).toBe(true);
|
||||
expect(codegraph('status').output).toContain('typescript');
|
||||
});
|
||||
|
||||
it('keeps its default without the flag', () => {
|
||||
initRepo();
|
||||
|
||||
const { code, output } = codegraph('init', '-y');
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).not.toContain('ignores itself');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(DEFAULT);
|
||||
expect(git('status', '--porcelain')).toContain('.codegraph/');
|
||||
});
|
||||
|
||||
it('converts an initialized project, without indexing it again', () => {
|
||||
initRepo();
|
||||
codegraph('init', '-y');
|
||||
const db = path.join(dir, '.codegraph', 'codegraph.db');
|
||||
const indexed = fs.statSync(db).mtimeMs;
|
||||
|
||||
const { code, output } = codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).toContain('Already initialized');
|
||||
expect(output).toContain('.codegraph/.gitignore now ignores itself');
|
||||
expect(fs.statSync(db).mtimeMs).toBe(indexed);
|
||||
expect(git('status', '--porcelain')).toBe('');
|
||||
});
|
||||
|
||||
it('says so when there is nothing to convert', () => {
|
||||
codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(codegraph('init', '-y', '--local-only').output).toContain('.codegraph/.gitignore already ignores itself');
|
||||
});
|
||||
|
||||
it('survives everything that opens the index afterwards', () => {
|
||||
codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(codegraph('sync').code).toBe(0);
|
||||
expect(codegraph('status').code).toBe(0);
|
||||
expect(codegraph('index', '--quiet').code).toBe(0);
|
||||
expect(codegraph('init', '-y').code).toBe(0);
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe(LOCAL_ONLY);
|
||||
});
|
||||
|
||||
it('says why it left a file with the user\'s own rules', () => {
|
||||
seedGitignore('codegraph.db\n');
|
||||
|
||||
const { code, output } = codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).toContain('Left .codegraph/.gitignore as it is because it has your own rules');
|
||||
expect(fs.readFileSync(gitignore, 'utf-8')).toBe('codegraph.db\n');
|
||||
});
|
||||
|
||||
it('says why it left a committed file, and shows no change in git', () => {
|
||||
seedGitignore(DEFAULT);
|
||||
initRepo();
|
||||
git('add', '.codegraph/.gitignore');
|
||||
git('commit', '-qm', 'share the rule');
|
||||
|
||||
const { code, output } = codegraph('init', '-y', '--local-only');
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(output).toContain('Left .codegraph/.gitignore as it is because it is committed');
|
||||
expect(git('status', '--porcelain')).toBe('');
|
||||
});
|
||||
|
||||
it('refuses an unsafe root before touching anything', () => {
|
||||
const home = spawnSync(process.execPath, [BIN, 'init', '-y', '--local-only'], {
|
||||
cwd: os.homedir(),
|
||||
encoding: 'utf-8',
|
||||
env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', NO_COLOR: '1' },
|
||||
});
|
||||
|
||||
expect(home.status).toBe(1);
|
||||
expect(fs.existsSync(path.join(os.homedir(), '.codegraph', '.gitignore'))).toBe(false);
|
||||
});
|
||||
|
||||
it('offers the flag in --help', () => {
|
||||
expect(codegraph('init', '--help').output).toContain('--local-only');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ description: Every CodeGraph command and the flags it accepts.
|
||||
codegraph # Run interactive installer
|
||||
codegraph install # Run installer (explicit)
|
||||
codegraph uninstall # Remove CodeGraph from your agents (inverse of install)
|
||||
codegraph init [path] # Initialize a project + build its graph (one step)
|
||||
codegraph init [path] # Initialize a project + build its graph (--local-only)
|
||||
codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
|
||||
codegraph index [path] # Full re-index from scratch (--force, --quiet, --verbose)
|
||||
codegraph sync [path] # Incremental update (--quiet)
|
||||
@@ -36,6 +36,8 @@ The MCP server (`codegraph serve --mcp`) is launched automatically by your agent
|
||||
|
||||
`codegraph init` creates the local `.codegraph/` directory **and** builds the full graph in one step. (The old `-i`/`--index` flag is now a no-op, accepted only so existing scripts don't break.) After that the file watcher keeps the graph current automatically — `index` (a full rebuild from scratch) and `sync` (an incremental update) are only needed when the watcher is disabled or you're scripting against the index outside an agent session.
|
||||
|
||||
`.codegraph/` holds a `.gitignore` that keeps the index out of git and is itself meant to be committed, so the rule is shared with your team. If you use CodeGraph on your own, `codegraph init --local-only` makes that file ignore itself too, so nothing shows in `git status`. It also works on a project that's already initialized, without re-indexing. A `.gitignore` you've edited or committed is left as it is.
|
||||
|
||||
## status
|
||||
|
||||
`codegraph status` shows what is in the index, including how many files each language has. When only some of a language's files yielded symbols, its row says how many did:
|
||||
|
||||
+28
-3
@@ -55,7 +55,7 @@ try {
|
||||
import { Command } from 'commander';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, isCrossSessionMessage, stripSystemReminders, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
|
||||
import { getCodeGraphDir, makeGitignoreLocalOnly, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, isCrossSessionMessage, stripSystemReminders, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
|
||||
import { extractProseCandidates } from '../search/identifier-segments';
|
||||
import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree';
|
||||
import { createShimmerProgress } from '../ui/shimmer-progress';
|
||||
@@ -707,6 +707,28 @@ async function recordIndexTelemetry(
|
||||
// Commands
|
||||
// =============================================================================
|
||||
|
||||
/** `init --local-only`: make the project's `.gitignore` ignore itself, and say what happened. */
|
||||
function applyLocalOnlyGitignore(
|
||||
clack: { log: { success(message: string): void; info(message: string): void; warn(message: string): void } },
|
||||
projectPath: string,
|
||||
): void {
|
||||
const file = `${path.basename(getCodeGraphDir(projectPath))}/.gitignore`;
|
||||
switch (makeGitignoreLocalOnly(projectPath)) {
|
||||
case 'written':
|
||||
clack.log.success(`${file} now ignores itself, so nothing of CodeGraph's shows in git.`);
|
||||
break;
|
||||
case 'unchanged':
|
||||
clack.log.info(`${file} already ignores itself.`);
|
||||
break;
|
||||
case 'customized':
|
||||
clack.log.warn(`Left ${file} as it is because it has your own rules. A lone * in it would keep the whole folder out of git.`);
|
||||
break;
|
||||
case 'tracked':
|
||||
clack.log.warn(`Left ${file} as it is because it is committed to this repository, so changing it would show up in git.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `init` flow — shared by `codegraph init` and `codegraph install --init`
|
||||
* (#1578): refuse an unsafe root, create `.codegraph/`, build the initial
|
||||
@@ -717,7 +739,7 @@ async function recordIndexTelemetry(
|
||||
*/
|
||||
async function runInit(
|
||||
projectPath: string,
|
||||
options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean },
|
||||
options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean; localOnly?: boolean },
|
||||
): Promise<void> {
|
||||
const clack = await importESM('@clack/prompts');
|
||||
|
||||
@@ -739,6 +761,7 @@ async function runInit(
|
||||
if (isInitialized(projectPath)) {
|
||||
clack.log.warn(`Already initialized in ${projectPath}`);
|
||||
clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
|
||||
if (options.localOnly) applyLocalOnlyGitignore(clack, projectPath);
|
||||
try {
|
||||
const { offerWatchFallback } = await import('../installer');
|
||||
await offerWatchFallback(clack, projectPath, { yes: options.yes });
|
||||
@@ -761,6 +784,7 @@ async function runInit(
|
||||
const { default: CodeGraph, getDatabasePath } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.init(projectPath, { index: false });
|
||||
clack.log.success(`Initialized in ${projectPath}`);
|
||||
if (options.localOnly) applyLocalOnlyGitignore(clack, projectPath);
|
||||
// A fresh index on a Windows drive under WSL gets its own directory (#995).
|
||||
// It isn't the documented name, so say where it went and why.
|
||||
const dataDir = path.basename(getCodeGraphDir(projectPath));
|
||||
@@ -831,7 +855,8 @@ program
|
||||
.option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
|
||||
.option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
|
||||
.option('-y, --yes', 'Non-interactive: skip every prompt and take the defaults (for scripts / CI / container bootstraps)')
|
||||
.action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean }) => {
|
||||
.option('--local-only', 'Keep .codegraph/ out of git entirely: its .gitignore ignores itself too. Also converts an initialized project')
|
||||
.action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean; localOnly?: boolean }) => {
|
||||
await runInit(path.resolve(pathArg || process.cwd()), options);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Manages the .codegraph/ directory structure for CodeGraph data.
|
||||
*/
|
||||
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -955,6 +956,62 @@ function ensureGitignore(gitignorePath: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The local-only `.codegraph/.gitignore` (#910): the wildcard alone, which
|
||||
* ignores this file too, so nothing of CodeGraph's shows in `git status` and
|
||||
* nothing asks to be committed. It carries no header, so `ensureGitignore`
|
||||
* treats it as the user's own and leaves it alone from then on.
|
||||
*/
|
||||
const LOCAL_ONLY_GITIGNORE_CONTENT = '*\n';
|
||||
|
||||
/** What `makeGitignoreLocalOnly` did. */
|
||||
export type LocalOnlyGitignoreResult =
|
||||
/** The file is local-only now. */
|
||||
| 'written'
|
||||
/** It already was. */
|
||||
| 'unchanged'
|
||||
/** It holds rules CodeGraph didn't write, so it was left alone. */
|
||||
| 'customized'
|
||||
/** It is committed, so rewriting it would show up as a change. Left alone. */
|
||||
| 'tracked';
|
||||
|
||||
/** Whether git tracks `file`. False outside a repository, or without git. */
|
||||
function isTrackedByGit(file: string): boolean {
|
||||
try {
|
||||
execFileSync('git', ['ls-files', '--error-unmatch', '--', path.basename(file)], {
|
||||
cwd: path.dirname(file),
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
timeout: 10_000,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make `.codegraph/.gitignore` local-only, for `codegraph init --local-only`.
|
||||
* Only a file CodeGraph generated is replaced: anything else, or one a project
|
||||
* has committed to share the rule, stays as it is and the caller says why.
|
||||
*/
|
||||
export function makeGitignoreLocalOnly(projectRoot: string): LocalOnlyGitignoreResult {
|
||||
const gitignorePath = path.join(getCodeGraphDir(projectRoot), '.gitignore');
|
||||
let existing: string | null;
|
||||
try {
|
||||
existing = fs.readFileSync(gitignorePath, 'utf-8').replace(/\r\n/g, '\n');
|
||||
} catch {
|
||||
existing = null;
|
||||
}
|
||||
if (existing === LOCAL_ONLY_GITIGNORE_CONTENT) return 'unchanged';
|
||||
if (existing !== null) {
|
||||
if (existing !== GITIGNORE_CONTENT && !isStaleDefaultGitignore(existing)) return 'customized';
|
||||
if (isTrackedByGit(gitignorePath)) return 'tracked';
|
||||
}
|
||||
fs.writeFileSync(gitignorePath, LOCAL_ONLY_GITIGNORE_CONTENT, 'utf-8');
|
||||
return 'written';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the .codegraph directory structure
|
||||
* Note: Only throws if codegraph.db already exists, not just if .codegraph/ exists.
|
||||
|
||||
Reference in New Issue
Block a user