mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(config): add user-level global ignore file (#2606)
IgnoreService only read per-repo .gitignore/.gitnexusignore, so an exclusion meant to apply across every indexed repo had to be repeated per repo or hand-patched into node_modules (wiped on every upgrade). loadIgnoreRules now also reads a global ignore file at $GITNEXUS_HOME/ignore (default ~/.gitnexus/ignore), reusing the existing global directory that already holds registry.json and config.json. It is added first, so per-repo .gitignore/.gitnexusignore rules can still negate it, mirroring the .gitignore -> .gitnexusignore precedence already in place. GITNEXUS_NO_GLOBAL_IGNORE (or noGlobalIgnore) skips it, mirroring GITNEXUS_NO_GITIGNORE.
This commit is contained in:
parent
3a8b369171
commit
322e05a6be
2 changed files with 142 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ import fs from 'fs/promises';
|
|||
import nodePath from 'path';
|
||||
import type { Path } from 'path-scurry';
|
||||
import { logger } from '../core/logger.js';
|
||||
import { getGlobalDir } from '../storage/repo-manager.js';
|
||||
|
||||
const DEFAULT_IGNORE_LIST = new Set([
|
||||
// Version Control
|
||||
|
|
@ -350,8 +351,18 @@ export const isHardcodedIgnoredDirectory = (name: string): boolean => {
|
|||
export interface IgnoreOptions {
|
||||
/** Skip .gitignore parsing, only read .gitnexusignore. Defaults to GITNEXUS_NO_GITIGNORE env var. */
|
||||
noGitignore?: boolean;
|
||||
/** Skip the user-level global ignore file. Defaults to GITNEXUS_NO_GLOBAL_IGNORE env var. */
|
||||
noGlobalIgnore?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to the user-level global ignore file, applied across every indexed
|
||||
* repo (#2606). Same `.gitnexusignore` syntax; lives alongside
|
||||
* `registry.json`/`config.json` under the existing global GitNexus
|
||||
* directory (`GITNEXUS_HOME` or `~/.gitnexus`) rather than a new location.
|
||||
*/
|
||||
export const getGlobalIgnorePath = (): string => nodePath.join(getGlobalDir(), 'ignore');
|
||||
|
||||
export const loadIgnoreRules = async (
|
||||
repoPath: string,
|
||||
options?: IgnoreOptions,
|
||||
|
|
@ -359,6 +370,23 @@ export const loadIgnoreRules = async (
|
|||
const ig = ignore();
|
||||
let hasRules = false;
|
||||
|
||||
// Global ignore file is added first so per-repo .gitignore/.gitnexusignore
|
||||
// rules layer on top and can negate it, mirroring the existing
|
||||
// .gitignore -> .gitnexusignore precedence below (#2606).
|
||||
const skipGlobalIgnore = options?.noGlobalIgnore ?? !!process.env.GITNEXUS_NO_GLOBAL_IGNORE;
|
||||
if (!skipGlobalIgnore) {
|
||||
try {
|
||||
const content = await fs.readFile(getGlobalIgnorePath(), 'utf-8');
|
||||
ig.add(content);
|
||||
hasRules = true;
|
||||
} catch (err: unknown) {
|
||||
const code = (err as NodeJS.ErrnoException).code;
|
||||
if (code !== 'ENOENT') {
|
||||
logger.warn(` Warning: could not read global ignore file: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow users to bypass .gitignore parsing (e.g. when .gitignore accidentally excludes source files)
|
||||
const skipGitignore = options?.noGitignore ?? !!process.env.GITNEXUS_NO_GITIGNORE;
|
||||
const filenames = skipGitignore ? ['.gitnexusignore'] : ['.gitignore', '.gitnexusignore'];
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
isHardcodedIgnoredDirectory,
|
||||
loadIgnoreRules,
|
||||
createIgnoreFilter,
|
||||
getGlobalIgnorePath,
|
||||
} from '../../src/config/ignore-service.js';
|
||||
import { _captureLogger } from '../../src/core/logger.js';
|
||||
|
||||
|
|
@ -658,3 +659,116 @@ describe('loadIgnoreRules — GITNEXUS_NO_GITIGNORE env var', () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── User-level global ignore file (#2606) ───────────────────────────
|
||||
//
|
||||
// IgnoreService previously read only per-repo .gitignore/.gitnexusignore.
|
||||
// #2606 asked for a global layer so an exclusion meant to apply to every
|
||||
// indexed repo is declared once, under the existing GITNEXUS_HOME/~/.gitnexus
|
||||
// global directory (same one that already holds registry.json/config.json),
|
||||
// rather than being repeated per repo or hand-patched into node_modules.
|
||||
//
|
||||
// Precedence: the global file is added to the `ignore` instance BEFORE
|
||||
// .gitignore/.gitnexusignore, so per-repo rules can negate it — the same
|
||||
// last-add-wins mechanism the #771 tests above already lock in one layer up.
|
||||
describe('loadIgnoreRules — user-level global ignore file (#2606)', () => {
|
||||
let repoDir: string;
|
||||
let globalHomeDir: string;
|
||||
let originalGitnexusHome: string | undefined;
|
||||
let originalNoGlobalIgnore: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
repoDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-global-ignore-repo-'));
|
||||
globalHomeDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-global-ignore-home-'));
|
||||
originalGitnexusHome = process.env.GITNEXUS_HOME;
|
||||
originalNoGlobalIgnore = process.env.GITNEXUS_NO_GLOBAL_IGNORE;
|
||||
process.env.GITNEXUS_HOME = globalHomeDir;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalGitnexusHome === undefined) {
|
||||
delete process.env.GITNEXUS_HOME;
|
||||
} else {
|
||||
process.env.GITNEXUS_HOME = originalGitnexusHome;
|
||||
}
|
||||
if (originalNoGlobalIgnore === undefined) {
|
||||
delete process.env.GITNEXUS_NO_GLOBAL_IGNORE;
|
||||
} else {
|
||||
process.env.GITNEXUS_NO_GLOBAL_IGNORE = originalNoGlobalIgnore;
|
||||
}
|
||||
await fs.rm(repoDir, { recursive: true, force: true });
|
||||
await fs.rm(globalHomeDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('getGlobalIgnorePath resolves under GITNEXUS_HOME', () => {
|
||||
expect(getGlobalIgnorePath()).toBe(path.join(globalHomeDir, 'ignore'));
|
||||
});
|
||||
|
||||
it('honours rules from the global ignore file when no per-repo files exist', async () => {
|
||||
await fs.writeFile(getGlobalIgnorePath(), 'docs/\n');
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).not.toBeNull();
|
||||
expect(ig!.ignores('docs/guide.md')).toBe(true);
|
||||
expect(ig!.ignores('src/index.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('per-repo .gitnexusignore can negate a global-ignore rule', async () => {
|
||||
await fs.writeFile(getGlobalIgnorePath(), 'docs/\n');
|
||||
await fs.writeFile(path.join(repoDir, '.gitnexusignore'), '!docs/\n');
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).not.toBeNull();
|
||||
expect(ig!.ignores('docs/guide.md')).toBe(false);
|
||||
});
|
||||
|
||||
it('GITNEXUS_NO_GLOBAL_IGNORE skips the global file entirely', async () => {
|
||||
await fs.writeFile(getGlobalIgnorePath(), 'docs/\n');
|
||||
process.env.GITNEXUS_NO_GLOBAL_IGNORE = '1';
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).toBeNull();
|
||||
});
|
||||
|
||||
it('noGlobalIgnore option skips the global file entirely', async () => {
|
||||
await fs.writeFile(getGlobalIgnorePath(), 'docs/\n');
|
||||
const ig = await loadIgnoreRules(repoDir, { noGlobalIgnore: true });
|
||||
expect(ig).toBeNull();
|
||||
});
|
||||
|
||||
it('missing global ignore file is a no-op (byte-identical to pre-#2606 behaviour)', async () => {
|
||||
// globalHomeDir exists but has no `ignore` file in it, and no per-repo files either.
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).toBeNull();
|
||||
});
|
||||
|
||||
it('still combines global, .gitignore, and .gitnexusignore rules together', async () => {
|
||||
await fs.writeFile(getGlobalIgnorePath(), 'docs/\n');
|
||||
await fs.writeFile(path.join(repoDir, '.gitignore'), 'data/\n');
|
||||
await fs.writeFile(path.join(repoDir, '.gitnexusignore'), 'vendor/\n');
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).not.toBeNull();
|
||||
expect(ig!.ignores('docs/guide.md')).toBe(true);
|
||||
expect(ig!.ignores('data/file.txt')).toBe(true);
|
||||
expect(ig!.ignores('vendor/lib.js')).toBe(true);
|
||||
expect(ig!.ignores('src/index.ts')).toBe(false);
|
||||
});
|
||||
|
||||
// Root bypasses POSIX read-permission checks (see the analogous EACCES
|
||||
// test above for .gitignore), so this can't reproduce under uid=0.
|
||||
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
|
||||
'warns on an unreadable global ignore file but does not throw',
|
||||
async () => {
|
||||
const globalIgnorePath = getGlobalIgnorePath();
|
||||
await fs.writeFile(globalIgnorePath, 'docs/\n');
|
||||
await fs.chmod(globalIgnorePath, 0o000);
|
||||
|
||||
const cap = _captureLogger();
|
||||
const ig = await loadIgnoreRules(repoDir);
|
||||
expect(ig).toBeNull();
|
||||
expect(
|
||||
cap.records().some((r) => String(r.msg ?? '').includes('global ignore file')),
|
||||
).toBe(true);
|
||||
|
||||
cap.restore();
|
||||
await fs.chmod(globalIgnorePath, 0o644);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue