gitnexus 1.6.10-rc.79 → 1.6.10-rc.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/config/ignore-service.d.ts +2 -0
- package/dist/config/ignore-service.js +27 -0
- package/dist/storage/git.d.ts +22 -0
- package/dist/storage/git.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -511,9 +511,19 @@ For very large repositories:
|
|
|
511
511
|
# Increase Node.js heap size
|
|
512
512
|
NODE_OPTIONS="--max-old-space-size=16384" npx gitnexus analyze
|
|
513
513
|
|
|
514
|
-
# Exclude large directories
|
|
514
|
+
# Exclude large directories (this repo only)
|
|
515
515
|
echo "vendor/" >> .gitnexusignore
|
|
516
516
|
echo "dist/" >> .gitnexusignore
|
|
517
|
+
|
|
518
|
+
# Exclude a directory across every repo you index, without touching each
|
|
519
|
+
# repo's own .gitnexusignore or needing push/commit access to it. GitNexus
|
|
520
|
+
# reads the same sources `git` itself does: core.excludesFile (all repos)
|
|
521
|
+
# and $GIT_DIR/info/exclude (this repo only, untracked). A repo's own
|
|
522
|
+
# .gitignore/.gitnexusignore can still override either with a `!pattern`
|
|
523
|
+
# negation. Skip both entirely with GITNEXUS_NO_GLOBAL_IGNORE=1.
|
|
524
|
+
git config --global core.excludesFile ~/.gitignore_global # applies to every repo
|
|
525
|
+
echo "docs/" >> ~/.gitignore_global
|
|
526
|
+
echo "build/" >> .git/info/exclude # this repo only, untracked
|
|
517
527
|
```
|
|
518
528
|
|
|
519
529
|
### Large files are being skipped
|
|
@@ -10,6 +10,8 @@ export declare const isHardcodedIgnoredDirectory: (name: string) => boolean;
|
|
|
10
10
|
export interface IgnoreOptions {
|
|
11
11
|
/** Skip .gitignore parsing, only read .gitnexusignore. Defaults to GITNEXUS_NO_GITIGNORE env var. */
|
|
12
12
|
noGitignore?: boolean;
|
|
13
|
+
/** Skip core.excludesFile and $GIT_COMMON_DIR/info/exclude. Defaults to GITNEXUS_NO_GLOBAL_IGNORE env var. */
|
|
14
|
+
noGlobalIgnore?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions) => Promise<Ignore | null>;
|
|
15
17
|
/**
|
|
@@ -2,6 +2,7 @@ import ignore from 'ignore';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import nodePath from 'path';
|
|
4
4
|
import { logger } from '../core/logger.js';
|
|
5
|
+
import { getCoreExcludesFilePath, getGitInfoExcludePath } from '../storage/git.js';
|
|
5
6
|
const DEFAULT_IGNORE_LIST = new Set([
|
|
6
7
|
// Version Control
|
|
7
8
|
'.git',
|
|
@@ -312,6 +313,32 @@ export const isHardcodedIgnoredDirectory = (name) => {
|
|
|
312
313
|
export const loadIgnoreRules = async (repoPath, options) => {
|
|
313
314
|
const ig = ignore();
|
|
314
315
|
let hasRules = false;
|
|
316
|
+
// Mirror git's own precedence for ignore sources (gitignore(5)): patterns
|
|
317
|
+
// from core.excludesFile are consulted first (lowest precedence — git's
|
|
318
|
+
// real global, all-repos file), then $GIT_COMMON_DIR/info/exclude
|
|
319
|
+
// (per-repo, untracked — no write access to the repo needed), then
|
|
320
|
+
// .gitignore/.gitnexusignore below. Later ig.add() calls win on
|
|
321
|
+
// conflicting patterns, matching git's own last-match-wins semantics (#2606).
|
|
322
|
+
const skipGlobalIgnore = options?.noGlobalIgnore ?? !!process.env.GITNEXUS_NO_GLOBAL_IGNORE;
|
|
323
|
+
if (!skipGlobalIgnore) {
|
|
324
|
+
const globalSources = [
|
|
325
|
+
getCoreExcludesFilePath(repoPath),
|
|
326
|
+
getGitInfoExcludePath(repoPath),
|
|
327
|
+
].filter((candidate) => candidate !== null);
|
|
328
|
+
for (const sourcePath of globalSources) {
|
|
329
|
+
try {
|
|
330
|
+
const content = await fs.readFile(sourcePath, 'utf-8');
|
|
331
|
+
ig.add(content);
|
|
332
|
+
hasRules = true;
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
const code = err.code;
|
|
336
|
+
if (code !== 'ENOENT') {
|
|
337
|
+
logger.warn(` Warning: could not read ${sourcePath}: ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
315
342
|
// Allow users to bypass .gitignore parsing (e.g. when .gitignore accidentally excludes source files)
|
|
316
343
|
const skipGitignore = options?.noGitignore ?? !!process.env.GITNEXUS_NO_GITIGNORE;
|
|
317
344
|
const filenames = skipGitignore ? ['.gitnexusignore'] : ['.gitignore', '.gitnexusignore'];
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -65,6 +65,28 @@ export declare const getGitRoot: (fromPath: string) => string | null;
|
|
|
65
65
|
* path if the caller later resolved it from a different directory.
|
|
66
66
|
*/
|
|
67
67
|
export declare const getCanonicalRepoRoot: (fromPath: string) => string | null;
|
|
68
|
+
/**
|
|
69
|
+
* Path to the repo's `$GIT_COMMON_DIR/info/exclude` file — git's own
|
|
70
|
+
* per-repo, untracked exclude list (same tier as `.gitignore` in
|
|
71
|
+
* precedence, but never committed, so it works even when the caller has
|
|
72
|
+
* no write access to the repo's tracked content). Shared across every
|
|
73
|
+
* linked worktree of a repo, matching git's own resolution (#2606).
|
|
74
|
+
*
|
|
75
|
+
* Returns `null` when `fromPath` is not inside a git repository or `git`
|
|
76
|
+
* is unavailable; callers should treat that the same as "no file".
|
|
77
|
+
*/
|
|
78
|
+
export declare const getGitInfoExcludePath: (fromPath: string) => string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Path to git's own global, all-repos ignore file: the value of
|
|
81
|
+
* `core.excludesFile` (any config scope — system/global/local, resolved
|
|
82
|
+
* the same way `git` itself would from `fromPath`), or git's documented
|
|
83
|
+
* default of `$XDG_CONFIG_HOME/git/ignore` when unset (gitignore(5)).
|
|
84
|
+
* Lowest-precedence source, mirroring git's own behavior (#2606).
|
|
85
|
+
*
|
|
86
|
+
* Never throws: an unset key or unavailable `git` falls through to the
|
|
87
|
+
* default path, which is always computable without `git`.
|
|
88
|
+
*/
|
|
89
|
+
export declare const getCoreExcludesFilePath: (fromPath: string) => string;
|
|
68
90
|
/**
|
|
69
91
|
* Resolve `fromPath` to the directory whose basename should drive the
|
|
70
92
|
* registry name (#1259) — the *identity root*. Three outcomes:
|
package/dist/storage/git.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, execSync } from 'child_process';
|
|
2
2
|
import { statSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
4
5
|
// Git utilities for repository detection, commit tracking, and diff analysis
|
|
5
6
|
const chompGitOutput = (value) => value.toString().replace(/\r?\n$/, '');
|
|
6
7
|
/**
|
|
@@ -198,6 +199,80 @@ export const getCanonicalRepoRoot = (fromPath) => {
|
|
|
198
199
|
return null;
|
|
199
200
|
}
|
|
200
201
|
};
|
|
202
|
+
// getGitInfoExcludePath/getCoreExcludesFilePath are called once per repo
|
|
203
|
+
// PER language/contract extractor during group sync (#2606) — an N-repo
|
|
204
|
+
// group fans out to 6+ extractors each calling these, so an uncached
|
|
205
|
+
// execSync per call turns into O(extractors × repos) blocking subprocess
|
|
206
|
+
// spawns. Both resolve to the same value for the same fromPath for the
|
|
207
|
+
// life of the process (git config/exclude files don't change mid-run), so
|
|
208
|
+
// memoize by fromPath. ponytail: process-lifetime cache, never invalidated
|
|
209
|
+
// — fine for one-shot CLI runs; the long-lived MCP server would need a
|
|
210
|
+
// TTL or explicit invalidation if a user edits core.excludesFile mid-session.
|
|
211
|
+
const gitInfoExcludePathCache = new Map();
|
|
212
|
+
const coreExcludesFilePathCache = new Map();
|
|
213
|
+
/**
|
|
214
|
+
* Path to the repo's `$GIT_COMMON_DIR/info/exclude` file — git's own
|
|
215
|
+
* per-repo, untracked exclude list (same tier as `.gitignore` in
|
|
216
|
+
* precedence, but never committed, so it works even when the caller has
|
|
217
|
+
* no write access to the repo's tracked content). Shared across every
|
|
218
|
+
* linked worktree of a repo, matching git's own resolution (#2606).
|
|
219
|
+
*
|
|
220
|
+
* Returns `null` when `fromPath` is not inside a git repository or `git`
|
|
221
|
+
* is unavailable; callers should treat that the same as "no file".
|
|
222
|
+
*/
|
|
223
|
+
export const getGitInfoExcludePath = (fromPath) => {
|
|
224
|
+
const cached = gitInfoExcludePathCache.get(fromPath);
|
|
225
|
+
if (cached !== undefined)
|
|
226
|
+
return cached;
|
|
227
|
+
let result;
|
|
228
|
+
try {
|
|
229
|
+
const commonDir = chompGitOutput(execSync('git rev-parse --path-format=absolute --git-common-dir', {
|
|
230
|
+
cwd: fromPath,
|
|
231
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
232
|
+
windowsHide: true,
|
|
233
|
+
}));
|
|
234
|
+
result = commonDir ? path.join(path.resolve(commonDir), 'info', 'exclude') : null;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
result = null;
|
|
238
|
+
}
|
|
239
|
+
gitInfoExcludePathCache.set(fromPath, result);
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Path to git's own global, all-repos ignore file: the value of
|
|
244
|
+
* `core.excludesFile` (any config scope — system/global/local, resolved
|
|
245
|
+
* the same way `git` itself would from `fromPath`), or git's documented
|
|
246
|
+
* default of `$XDG_CONFIG_HOME/git/ignore` when unset (gitignore(5)).
|
|
247
|
+
* Lowest-precedence source, mirroring git's own behavior (#2606).
|
|
248
|
+
*
|
|
249
|
+
* Never throws: an unset key or unavailable `git` falls through to the
|
|
250
|
+
* default path, which is always computable without `git`.
|
|
251
|
+
*/
|
|
252
|
+
export const getCoreExcludesFilePath = (fromPath) => {
|
|
253
|
+
const cached = coreExcludesFilePathCache.get(fromPath);
|
|
254
|
+
if (cached !== undefined)
|
|
255
|
+
return cached;
|
|
256
|
+
let result;
|
|
257
|
+
try {
|
|
258
|
+
const configured = chompGitOutput(execSync('git config --get --type=path core.excludesFile', {
|
|
259
|
+
cwd: fromPath,
|
|
260
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
261
|
+
windowsHide: true,
|
|
262
|
+
}));
|
|
263
|
+
if (configured)
|
|
264
|
+
result = configured;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Unset, or git unavailable — fall through to git's documented default.
|
|
268
|
+
}
|
|
269
|
+
if (!result) {
|
|
270
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
271
|
+
result = path.join(xdgConfigHome, 'git', 'ignore');
|
|
272
|
+
}
|
|
273
|
+
coreExcludesFilePathCache.set(fromPath, result);
|
|
274
|
+
return result;
|
|
275
|
+
};
|
|
201
276
|
/**
|
|
202
277
|
* Resolve `fromPath` to the directory whose basename should drive the
|
|
203
278
|
* registry name (#1259) — the *identity root*. Three outcomes:
|
package/package.json
CHANGED