mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
fix(analyze): allow indexing folders without a .git directory (#384)
Previously gitnexus analyze exited with an error on any directory that
lacked a .git entry, making it impossible to index generated code,
vendored libraries, or monorepo sub-trees that are not git roots.
Changes:
storage/git.ts
- Add hasGitDir(dirPath): boolean — a lightweight synchronous check for
the presence of a .git file or directory. Works for git worktrees
(.git file pointing at the real repo) as well as standard repos.
cli/analyze.ts
- Add noGit?: boolean to AnalyzeOptions.
- When the explicit inputPath resolves to a non-git folder (or the cwd
is not inside any git repo), respect --no-git instead of hard-failing.
- Print an actionable tip pointing at --no-git when git is absent and the
flag was not supplied.
- currentCommit defaults to an empty string for non-git folders so the
up-to-date check still functions (empty string never matches a real
commit hash, so the index is always rebuilt).
- Skip addToGitignore() when no .git is present — there is nothing to
update and the function would create a stale .gitignore at the root.
Git-dependent features that remain disabled for non-git folders:
- Incremental update (always rebuilds from scratch)
- Commit tracking in metadata
- .gitignore update
Closes #384
This commit is contained in:
parent
b75e380bd0
commit
4dffd81b12
3 changed files with 156 additions and 9 deletions
|
|
@ -15,7 +15,7 @@ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReuse
|
|||
// versions whose ABI is not yet supported by the native binary (#89).
|
||||
// disposeEmbedder intentionally not called — ONNX Runtime segfaults on cleanup (see #38)
|
||||
import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, getGlobalRegistryPath, cleanupOldKuzuFiles } from '../storage/repo-manager.js';
|
||||
import { getCurrentCommit, isGitRepo, getGitRoot } from '../storage/git.js';
|
||||
import { getCurrentCommit, isGitRepo, getGitRoot, hasGitDir } from '../storage/git.js';
|
||||
import { generateAIContextFiles } from './ai-context.js';
|
||||
import { generateSkillFiles, type GeneratedSkillInfo } from './skill-gen.js';
|
||||
import fs from 'fs/promises';
|
||||
|
|
@ -48,6 +48,8 @@ export interface AnalyzeOptions {
|
|||
embeddings?: boolean;
|
||||
skills?: boolean;
|
||||
verbose?: boolean;
|
||||
/** Index the folder even when no .git directory is present. */
|
||||
noGit?: boolean;
|
||||
}
|
||||
|
||||
/** Threshold: auto-skip embeddings for repos with more nodes than this */
|
||||
|
|
@ -87,18 +89,27 @@ export const analyzeCommand = async (
|
|||
} else {
|
||||
const gitRoot = getGitRoot(process.cwd());
|
||||
if (!gitRoot) {
|
||||
console.log(' Not inside a git repository\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
if (!options?.noGit) {
|
||||
console.log(' Not inside a git repository.\n Tip: pass --no-git to index any folder without a .git directory.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
// --no-git: fall back to cwd as the root
|
||||
repoPath = path.resolve(process.cwd());
|
||||
} else {
|
||||
repoPath = gitRoot;
|
||||
}
|
||||
repoPath = gitRoot;
|
||||
}
|
||||
|
||||
if (!isGitRepo(repoPath)) {
|
||||
console.log(' Not a git repository\n');
|
||||
const repoHasGit = isGitRepo(repoPath);
|
||||
if (!repoHasGit && !options?.noGit) {
|
||||
console.log(' Not a git repository.\n Tip: pass --no-git to index any folder without a .git directory.\n');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (!repoHasGit) {
|
||||
console.log(' Warning: no .git directory found — commit-tracking and incremental updates disabled.\n');
|
||||
}
|
||||
|
||||
const { storagePath, lbugPath } = getStoragePaths(repoPath);
|
||||
|
||||
|
|
@ -109,7 +120,7 @@ export const analyzeCommand = async (
|
|||
console.log(' Migrating from KuzuDB to LadybugDB — rebuilding index...\n');
|
||||
}
|
||||
|
||||
const currentCommit = getCurrentCommit(repoPath);
|
||||
const currentCommit = repoHasGit ? getCurrentCommit(repoPath) : '';
|
||||
const existingMeta = await loadMeta(storagePath);
|
||||
|
||||
if (existingMeta && !options?.force && !options?.skills && existingMeta.lastCommit === currentCommit) {
|
||||
|
|
@ -317,7 +328,10 @@ export const analyzeCommand = async (
|
|||
};
|
||||
await saveMeta(storagePath, meta);
|
||||
await registerRepo(repoPath, meta);
|
||||
await addToGitignore(repoPath);
|
||||
// Only attempt to update .gitignore when a git repository is present
|
||||
if (repoHasGit) {
|
||||
await addToGitignore(repoPath);
|
||||
}
|
||||
|
||||
const projectName = path.basename(repoPath);
|
||||
let aggregatedClusterCount = 0;
|
||||
|
|
|
|||
|
|
@ -34,3 +34,23 @@ export const getGitRoot = (fromPath: string): string | null => {
|
|||
return null;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Check whether a directory contains a .git entry (file or folder).
|
||||
*
|
||||
* This is intentionally a simple filesystem check rather than running
|
||||
* `git rev-parse`, so it works even when git is not installed or when
|
||||
* the directory is a git-worktree root (which has a .git file, not a
|
||||
* directory). Use `isGitRepo` for a definitive git answer.
|
||||
*
|
||||
* @param dirPath - Absolute path to the directory to inspect.
|
||||
* @returns `true` when `.git` is present, `false` otherwise.
|
||||
*/
|
||||
export const hasGitDir = (dirPath: string): boolean => {
|
||||
try {
|
||||
const { statSync } = require('fs') as typeof import('fs');
|
||||
statSync(path.join(dirPath, '.git'));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
113
gitnexus/test/unit/git-utils.test.ts
Normal file
113
gitnexus/test/unit/git-utils.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Unit Tests: git utility helpers (storage/git.ts)
|
||||
*
|
||||
* Tests isGitRepo, getCurrentCommit, getGitRoot, and the newly added
|
||||
* hasGitDir helper introduced for issue #384 (indexing non-git folders).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
|
||||
// ─── hasGitDir ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// hasGitDir is a synchronous fs.statSync check — we test it by actually
|
||||
// creating temporary directories rather than mocking the fs module,
|
||||
// because the implementation is a simple one-liner and real disk I/O is
|
||||
// fast and deterministic for this purpose.
|
||||
|
||||
describe('hasGitDir', () => {
|
||||
// Import after test setup to ensure module resolution is correct
|
||||
const getHasGitDir = async () => {
|
||||
const mod = await import('../../src/storage/git.js');
|
||||
return mod.hasGitDir;
|
||||
};
|
||||
|
||||
it('returns true when .git directory exists', async () => {
|
||||
const hasGitDir = await getHasGitDir();
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmpDir, '.git'));
|
||||
expect(hasGitDir(tmpDir)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns true when .git is a file (git worktree)', async () => {
|
||||
const hasGitDir = await getHasGitDir();
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(tmpDir, '.git'), 'gitdir: /some/other/.git\n');
|
||||
expect(hasGitDir(tmpDir)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false when .git entry is absent', async () => {
|
||||
const hasGitDir = await getHasGitDir();
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
// No .git here — plain directory
|
||||
expect(hasGitDir(tmpDir)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false for a non-existent path', async () => {
|
||||
const hasGitDir = await getHasGitDir();
|
||||
expect(hasGitDir('/tmp/__gitnexus_nonexistent_path__')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── isGitRepo ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// isGitRepo shells out to `git rev-parse` — we verify it returns false
|
||||
// for a plain temp directory without running git init.
|
||||
|
||||
describe('isGitRepo', () => {
|
||||
it('returns false for a plain (non-git) directory', async () => {
|
||||
const { isGitRepo } = await import('../../src/storage/git.js');
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
expect(isGitRepo(tmpDir)).toBe(false);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false for a non-existent path', async () => {
|
||||
const { isGitRepo } = await import('../../src/storage/git.js');
|
||||
expect(isGitRepo('/tmp/__gitnexus_nonexistent__')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getCurrentCommit ─────────────────────────────────────────────────────
|
||||
|
||||
describe('getCurrentCommit', () => {
|
||||
it('returns empty string for a non-git directory', async () => {
|
||||
const { getCurrentCommit } = await import('../../src/storage/git.js');
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
expect(getCurrentCommit(tmpDir)).toBe('');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getGitRoot ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('getGitRoot', () => {
|
||||
it('returns null for a plain temp directory', async () => {
|
||||
const { getGitRoot } = await import('../../src/storage/git.js');
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-test-'));
|
||||
try {
|
||||
expect(getGitRoot(tmpDir)).toBeNull();
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue