dsh-gitnexus-plus 0.1.3 → 0.1.5
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-CN.md +34 -10
- package/README.md +79 -14
- package/lib/bin/graph-helper.d.ts +21 -0
- package/lib/bin/graph-helper.d.ts.map +1 -0
- package/lib/bin/graph-helper.js +71 -0
- package/lib/bin/graph-helper.js.map +1 -0
- package/lib/client.js +4402 -2371
- package/lib/gitnexus/analyze.d.ts +54 -8
- package/lib/gitnexus/analyze.d.ts.map +1 -1
- package/lib/gitnexus/analyze.js +355 -84
- package/lib/gitnexus/analyze.js.map +1 -1
- package/lib/gitnexus/commands.d.ts +19 -2
- package/lib/gitnexus/commands.d.ts.map +1 -1
- package/lib/gitnexus/commands.js +42 -4
- package/lib/gitnexus/commands.js.map +1 -1
- package/lib/gitnexus/coverage.d.ts +8 -0
- package/lib/gitnexus/coverage.d.ts.map +1 -1
- package/lib/gitnexus/coverage.js +14 -2
- package/lib/gitnexus/coverage.js.map +1 -1
- package/lib/gitnexus/delta.d.ts +144 -0
- package/lib/gitnexus/delta.d.ts.map +1 -0
- package/lib/gitnexus/delta.js +347 -0
- package/lib/gitnexus/delta.js.map +1 -0
- package/lib/gitnexus/discover.d.ts +91 -0
- package/lib/gitnexus/discover.d.ts.map +1 -0
- package/lib/gitnexus/discover.js +214 -0
- package/lib/gitnexus/discover.js.map +1 -0
- package/lib/gitnexus/git-activity.d.ts +13 -21
- package/lib/gitnexus/git-activity.d.ts.map +1 -1
- package/lib/gitnexus/git-activity.js +60 -26
- package/lib/gitnexus/git-activity.js.map +1 -1
- package/lib/gitnexus/graph.d.ts +135 -0
- package/lib/gitnexus/graph.d.ts.map +1 -0
- package/lib/gitnexus/graph.js +474 -0
- package/lib/gitnexus/graph.js.map +1 -0
- package/lib/gitnexus/index-meta.d.ts +34 -6
- package/lib/gitnexus/index-meta.d.ts.map +1 -1
- package/lib/gitnexus/index-meta.js +47 -7
- package/lib/gitnexus/index-meta.js.map +1 -1
- package/lib/gitnexus/install.d.ts +11 -0
- package/lib/gitnexus/install.d.ts.map +1 -1
- package/lib/gitnexus/install.js +65 -9
- package/lib/gitnexus/install.js.map +1 -1
- package/lib/gitnexus/root.d.ts +0 -3
- package/lib/gitnexus/root.d.ts.map +1 -1
- package/lib/gitnexus/root.js +64 -5
- package/lib/gitnexus/root.js.map +1 -1
- package/lib/gitnexus/search.d.ts +34 -8
- package/lib/gitnexus/search.d.ts.map +1 -1
- package/lib/gitnexus/search.js +89 -13
- package/lib/gitnexus/search.js.map +1 -1
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +189 -7
- package/lib/index.js.map +1 -1
- package/lib/routes/analyze.d.ts +8 -6
- package/lib/routes/analyze.d.ts.map +1 -1
- package/lib/routes/analyze.js +25 -6
- package/lib/routes/analyze.js.map +1 -1
- package/lib/routes/coverage.d.ts +16 -10
- package/lib/routes/coverage.d.ts.map +1 -1
- package/lib/routes/coverage.js +31 -22
- package/lib/routes/coverage.js.map +1 -1
- package/lib/routes/delta.d.ts +22 -0
- package/lib/routes/delta.d.ts.map +1 -0
- package/lib/routes/delta.js +50 -0
- package/lib/routes/delta.js.map +1 -0
- package/lib/routes/file-diff.d.ts +19 -0
- package/lib/routes/file-diff.d.ts.map +1 -0
- package/lib/routes/file-diff.js +69 -0
- package/lib/routes/file-diff.js.map +1 -0
- package/lib/routes/graph.d.ts +22 -0
- package/lib/routes/graph.d.ts.map +1 -0
- package/lib/routes/graph.js +71 -0
- package/lib/routes/graph.js.map +1 -0
- package/lib/routes/guard.d.ts +3 -6
- package/lib/routes/guard.d.ts.map +1 -1
- package/lib/routes/guard.js +3 -6
- package/lib/routes/guard.js.map +1 -1
- package/lib/routes/projects.d.ts +21 -0
- package/lib/routes/projects.d.ts.map +1 -0
- package/lib/routes/projects.js +61 -0
- package/lib/routes/projects.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 0 staleness: is a refresh worth running at all?
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* The panel's only maintenance action used to be "run `gitnexus analyze`", and it ran
|
|
7
|
+
* the analyzer every time — on a repository where nothing had changed since the index
|
|
8
|
+
* was written. That is not a correctness bug, it is a cost bug with a bad shape: the
|
|
9
|
+
* work is measured in seconds on a small repository and in minutes on a large one, and
|
|
10
|
+
* the user has no way to know beforehand whether they are about to spend it.
|
|
11
|
+
*
|
|
12
|
+
* The index already records what it saw: `.gitnexus/gitnexus.json` carries
|
|
13
|
+
* `fileHashes`, a map of repo-relative path to sha256. Comparing the working tree
|
|
14
|
+
* against those hashes answers "would a re-analysis change anything?" exactly, and it
|
|
15
|
+
* needs nothing but git and a hash function.
|
|
16
|
+
*
|
|
17
|
+
* WHAT IS COMPARED, AND AGAINST WHAT
|
|
18
|
+
* ----------------------------------
|
|
19
|
+
* The baseline is the **index snapshot**, not `HEAD`. Those differ whenever the index
|
|
20
|
+
* was built from a dirty tree, and the manifest records that case in
|
|
21
|
+
* `indexCoverage.dirtyPaths`. Comparing against `HEAD` would call a file unchanged
|
|
22
|
+
* merely because it was committed, even though the index remembered different content
|
|
23
|
+
* — which is the direction that produces a stale graph the panel says is current.
|
|
24
|
+
*
|
|
25
|
+
* The candidate set is therefore everything that differs from the *indexed commit*:
|
|
26
|
+
*
|
|
27
|
+
* `git diff --no-renames --name-only <lastCommit>` tracked files, working tree vs
|
|
28
|
+
* that commit (staged and unstaged)
|
|
29
|
+
* `git ls-files --others --exclude-standard` untracked files git would include
|
|
30
|
+
*
|
|
31
|
+
* `--no-renames` on purpose: with rename detection on, git reports only the new path,
|
|
32
|
+
* so a rename looks like a pure addition and the index's old path silently disappears
|
|
33
|
+
* from the comparison. Forcing add+delete pairs lets each side be classified on its
|
|
34
|
+
* own evidence.
|
|
35
|
+
*
|
|
36
|
+
* Hash comparison is per candidate, not per indexed file, which is what keeps this
|
|
37
|
+
* cheap: a clean working tree produces an empty candidate set and no file is read at
|
|
38
|
+
* all. `mtime` is *not* used as a prefilter even though the plan called for one — the
|
|
39
|
+
* manifest stores a hash and no mtime, so "the mtime did not move" cannot prove the
|
|
40
|
+
* content matches, and using it would trade an exact answer for a fast wrong one.
|
|
41
|
+
*
|
|
42
|
+
* WHAT IT DOES NOT DO
|
|
43
|
+
* -------------------
|
|
44
|
+
* It never runs the analyzer, never writes, and never reads the graph. It also does not
|
|
45
|
+
* decide *which* tier of refresh to run — that needs the analyzer's own judgement about
|
|
46
|
+
* schema and FTS state. It answers one question: has indexed content changed?
|
|
47
|
+
*/
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
import { createReadStream } from 'node:fs';
|
|
50
|
+
import fs from 'node:fs/promises';
|
|
51
|
+
import path from 'node:path';
|
|
52
|
+
import { execFile } from 'node:child_process';
|
|
53
|
+
import { promisify } from 'node:util';
|
|
54
|
+
import { formatBytes } from './coverage.js';
|
|
55
|
+
import { readIndexMeta } from './index-meta.js';
|
|
56
|
+
/**
|
|
57
|
+
* `execFile`, promisified, resolved as decoded text.
|
|
58
|
+
*
|
|
59
|
+
* No shell: the repository path is caller-supplied and only ever travels as `cwd`.
|
|
60
|
+
* `-z` output is NUL-separated rather than newline-separated precisely because a path
|
|
61
|
+
* may contain a newline, and `git status` would otherwise quote it into an unparseable
|
|
62
|
+
* form.
|
|
63
|
+
*/
|
|
64
|
+
const run = promisify(execFile);
|
|
65
|
+
const DEFAULT_MAX_FILES = 4000;
|
|
66
|
+
const DEFAULT_HASH_CONCURRENCY = 8;
|
|
67
|
+
const DEFAULT_GIT_TIMEOUT_MS = 15_000;
|
|
68
|
+
/** Run one git command, or report that git could not answer. */
|
|
69
|
+
async function git(repoPath, args, timeoutMs) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await run('git', args, {
|
|
72
|
+
cwd: repoPath,
|
|
73
|
+
timeout: timeoutMs,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
// Name lists on a very large repository can exceed the default 1 MB cap. The
|
|
76
|
+
// cap exists so a runaway command cannot exhaust memory; 32 MB of paths is
|
|
77
|
+
// already an unreasonable candidate set, and `maxFiles` truncates it anyway.
|
|
78
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
79
|
+
});
|
|
80
|
+
return stdout;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Split `-z` output on NUL and drop the trailing empty record. */
|
|
87
|
+
function splitNul(text) {
|
|
88
|
+
return text.split('\0').filter((part) => part !== '');
|
|
89
|
+
}
|
|
90
|
+
/** The key form `fileHashes` uses: forward slashes, no leading `./`. */
|
|
91
|
+
function toKey(relativePath) {
|
|
92
|
+
return relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
93
|
+
}
|
|
94
|
+
/** Stream one file through sha256. Returns null when it cannot be read. */
|
|
95
|
+
async function hashFile(absolutePath) {
|
|
96
|
+
try {
|
|
97
|
+
const hash = createHash('sha256');
|
|
98
|
+
await new Promise((resolvePromise, reject) => {
|
|
99
|
+
const stream = createReadStream(absolutePath);
|
|
100
|
+
stream.on('error', reject);
|
|
101
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
102
|
+
stream.on('end', () => resolvePromise());
|
|
103
|
+
});
|
|
104
|
+
return hash.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** The counts of an empty report, so every early return carries the same shape. */
|
|
111
|
+
const ZERO_COUNTS = {
|
|
112
|
+
checked: 0,
|
|
113
|
+
hashed: 0,
|
|
114
|
+
unchanged: 0,
|
|
115
|
+
modified: 0,
|
|
116
|
+
added: 0,
|
|
117
|
+
deleted: 0,
|
|
118
|
+
excluded: 0,
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Compare the working tree against the content the index recorded.
|
|
122
|
+
*
|
|
123
|
+
* @param repoPath absolute repository root
|
|
124
|
+
*/
|
|
125
|
+
export async function readDelta(repoPath, options = {}) {
|
|
126
|
+
const now = options.now ?? (() => Date.now());
|
|
127
|
+
const startedAt = now();
|
|
128
|
+
const absRepo = path.resolve(repoPath);
|
|
129
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
130
|
+
const concurrency = Math.max(1, options.hashConcurrency ?? DEFAULT_HASH_CONCURRENCY);
|
|
131
|
+
const gitTimeoutMs = options.gitTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
|
|
132
|
+
const empty = (patch) => ({
|
|
133
|
+
repoPath: absRepo,
|
|
134
|
+
indexed: false,
|
|
135
|
+
comparable: false,
|
|
136
|
+
basis: null,
|
|
137
|
+
indexedCommit: null,
|
|
138
|
+
headCommit: null,
|
|
139
|
+
entries: [],
|
|
140
|
+
counts: { ...ZERO_COUNTS },
|
|
141
|
+
needsRefresh: false,
|
|
142
|
+
certain: false,
|
|
143
|
+
truncated: false,
|
|
144
|
+
notes: [],
|
|
145
|
+
durationMs: now() - startedAt,
|
|
146
|
+
readAt: new Date().toISOString(),
|
|
147
|
+
...patch,
|
|
148
|
+
});
|
|
149
|
+
const meta = await readIndexMeta(absRepo);
|
|
150
|
+
if (meta === null) {
|
|
151
|
+
return empty({ notes: ['该项目还没有 GitNexus 索引,先初始化再说'] });
|
|
152
|
+
}
|
|
153
|
+
const indexedCommit = meta.lastCommit === '' ? null : meta.lastCommit;
|
|
154
|
+
const base = { indexed: true, indexedCommit };
|
|
155
|
+
// The map is normalized-key -> original key, and the hash is read through the
|
|
156
|
+
// original: `fileHashes` is written by GitNexus, and a manifest whose keys use
|
|
157
|
+
// backslashes (Windows builds did) must still match the forward-slash key git
|
|
158
|
+
// reports. Rewriting the manifest's keys instead would mutate what we only read.
|
|
159
|
+
const hashByKey = new Map();
|
|
160
|
+
for (const [rawPath, hash] of Object.entries(meta.fileHashes)) {
|
|
161
|
+
hashByKey.set(toKey(rawPath), hash.toLowerCase());
|
|
162
|
+
}
|
|
163
|
+
if (hashByKey.size === 0) {
|
|
164
|
+
return empty({
|
|
165
|
+
...base,
|
|
166
|
+
notes: ['该索引没有记录文件哈希(旧格式 manifest),无法判断内容是否变化 —— 直接刷新'],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const headCommit = (await git(absRepo, ['rev-parse', 'HEAD'], gitTimeoutMs))?.trim() ?? null;
|
|
170
|
+
let basis = null;
|
|
171
|
+
let candidates = [];
|
|
172
|
+
if (indexedCommit !== null && headCommit !== null) {
|
|
173
|
+
const diff = await git(absRepo, ['diff', '--no-renames', '--name-only', '-z', indexedCommit, '--'], gitTimeoutMs);
|
|
174
|
+
if (diff !== null) {
|
|
175
|
+
basis = 'index';
|
|
176
|
+
candidates = splitNul(diff);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (basis === null && headCommit !== null) {
|
|
180
|
+
// The recorded commit is gone (rebase, branch rewrite, shallow clone) or the
|
|
181
|
+
// manifest never had one. Working tree vs HEAD is the honest fallback: it finds
|
|
182
|
+
// uncommitted and newly committed files, and it cannot see a difference that was
|
|
183
|
+
// committed before the index was written. Say so rather than implying otherwise.
|
|
184
|
+
const againstHead = await git(absRepo, ['diff', '--no-renames', '--name-only', '-z', 'HEAD', '--'], gitTimeoutMs);
|
|
185
|
+
if (againstHead !== null) {
|
|
186
|
+
basis = 'worktree';
|
|
187
|
+
candidates = splitNul(againstHead);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (basis === null) {
|
|
191
|
+
return empty({
|
|
192
|
+
...base,
|
|
193
|
+
headCommit,
|
|
194
|
+
notes: [
|
|
195
|
+
indexedCommit === null
|
|
196
|
+
? '索引没有记录提交基线,且 git 也无法读取 HEAD —— 无法比较,直接刷新'
|
|
197
|
+
: '索引记录的提交在本地 git 历史中已不可用,且无法读取 HEAD —— 无法比较,直接刷新',
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const untracked = await git(absRepo, ['ls-files', '--others', '--exclude-standard', '-z'], gitTimeoutMs);
|
|
202
|
+
if (untracked !== null) {
|
|
203
|
+
candidates.push(...splitNul(untracked));
|
|
204
|
+
}
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
const unique = [];
|
|
207
|
+
for (const candidate of candidates) {
|
|
208
|
+
const key = toKey(candidate);
|
|
209
|
+
if (key === '' || seen.has(key))
|
|
210
|
+
continue;
|
|
211
|
+
// The index's own storage is never indexed content. `.gitnexus/` self-ignores in
|
|
212
|
+
// most repositories (GitNexus writes a `*` .gitignore there), so this only matters
|
|
213
|
+
// where it does not — but in exactly that case every read would report the index
|
|
214
|
+
// manifest itself as a new file and ask for a refresh forever.
|
|
215
|
+
if (key.startsWith('.gitnexus/') || key.startsWith('.git/'))
|
|
216
|
+
continue;
|
|
217
|
+
seen.add(key);
|
|
218
|
+
unique.push(key);
|
|
219
|
+
}
|
|
220
|
+
unique.sort();
|
|
221
|
+
const truncated = unique.length > maxFiles;
|
|
222
|
+
const examined = truncated ? unique.slice(0, maxFiles) : unique;
|
|
223
|
+
const entries = [];
|
|
224
|
+
const counts = { ...ZERO_COUNTS };
|
|
225
|
+
counts.checked = examined.length;
|
|
226
|
+
// Bounded parallelism rather than one read at a time: a 4000-file candidate set read
|
|
227
|
+
// sequentially is dominated by syscall latency, and these are local files.
|
|
228
|
+
for (let offset = 0; offset < examined.length; offset += concurrency) {
|
|
229
|
+
const batch = examined.slice(offset, offset + concurrency);
|
|
230
|
+
const settled = await Promise.all(batch.map(async (key) => {
|
|
231
|
+
const absolute = path.join(absRepo, key);
|
|
232
|
+
let size;
|
|
233
|
+
try {
|
|
234
|
+
const stat = await fs.stat(absolute);
|
|
235
|
+
if (!stat.isFile())
|
|
236
|
+
return null;
|
|
237
|
+
size = stat.size;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Gone from the working tree. Only worth reporting when the index had it;
|
|
241
|
+
// an untracked file deleted between `ls-files` and this stat is not news.
|
|
242
|
+
return hashByKey.has(key) ? { path: key, kind: 'deleted', reason: null } : null;
|
|
243
|
+
}
|
|
244
|
+
const recorded = hashByKey.get(key);
|
|
245
|
+
if (recorded === undefined) {
|
|
246
|
+
const cap = meta.coverageMaxFileSizeBytes;
|
|
247
|
+
if (cap !== null && size > cap) {
|
|
248
|
+
return {
|
|
249
|
+
path: key,
|
|
250
|
+
kind: 'excluded',
|
|
251
|
+
reason: `超过索引记录的大小上限(${formatBytes(cap)}),刷新也会被跳过`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { path: key, kind: 'added', reason: null };
|
|
255
|
+
}
|
|
256
|
+
counts.hashed += 1;
|
|
257
|
+
const current = await hashFile(absolute);
|
|
258
|
+
if (current === null) {
|
|
259
|
+
// Unreadable is not unchanged. Treating it as modified makes the panel ask
|
|
260
|
+
// for a refresh; treating it as unchanged would report a stale index as
|
|
261
|
+
// current, which is the one error this module exists to prevent.
|
|
262
|
+
return { path: key, kind: 'modified', reason: '无法读取文件内容,按已变更处理' };
|
|
263
|
+
}
|
|
264
|
+
if (current === recorded) {
|
|
265
|
+
counts.unchanged += 1;
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
return { path: key, kind: 'modified', reason: null };
|
|
269
|
+
}));
|
|
270
|
+
for (const entry of settled) {
|
|
271
|
+
if (entry !== null)
|
|
272
|
+
entries.push(entry);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
276
|
+
for (const entry of entries)
|
|
277
|
+
counts[entry.kind] += 1;
|
|
278
|
+
const actionable = counts.modified + counts.added + counts.deleted;
|
|
279
|
+
const certain = basis === 'index';
|
|
280
|
+
const notes = [];
|
|
281
|
+
if (basis === 'worktree') {
|
|
282
|
+
notes.push('索引没有记录可用的提交基线:只比较了工作区相对 HEAD 的改动,索引建立之后已提交的改动看不到');
|
|
283
|
+
}
|
|
284
|
+
if (truncated) {
|
|
285
|
+
notes.push(`候选文件超过 ${maxFiles} 个,只检查了前 ${maxFiles} 个 —— 建议直接刷新`);
|
|
286
|
+
}
|
|
287
|
+
if (actionable === 0) {
|
|
288
|
+
// A clean worktree is not the same claim as a clean index. Saying "一致" on the
|
|
289
|
+
// worktree basis is how a stale index gets reported as current: the next commit
|
|
290
|
+
// makes it stale and this comparison will still be empty.
|
|
291
|
+
notes.push(certain
|
|
292
|
+
? counts.checked === 0
|
|
293
|
+
? '工作区与索引记录一致:没有任何文件相对索引提交发生改动'
|
|
294
|
+
: `工作区与索引记录一致:${counts.checked} 个候选文件中 ${counts.unchanged} 个内容哈希匹配` +
|
|
295
|
+
(counts.excluded > 0 ? `,${counts.excluded} 个属索引策略排除` : '')
|
|
296
|
+
: '工作区相对 HEAD 没有改动,但无法确认索引已包含当前提交 —— 需要刷新才能确定');
|
|
297
|
+
if (counts.excluded > 0) {
|
|
298
|
+
notes.push('被排除的文件刷新也会再次跳过,因此不需要为它们刷新索引');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (counts.modified > 0)
|
|
304
|
+
parts.push(`${counts.modified} 个修改`);
|
|
305
|
+
if (counts.added > 0)
|
|
306
|
+
parts.push(`${counts.added} 个新增`);
|
|
307
|
+
if (counts.deleted > 0)
|
|
308
|
+
parts.push(`${counts.deleted} 个删除`);
|
|
309
|
+
notes.unshift(`索引内容已落后:${parts.join('、')}`);
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
repoPath: absRepo,
|
|
313
|
+
indexed: true,
|
|
314
|
+
comparable: true,
|
|
315
|
+
basis,
|
|
316
|
+
indexedCommit,
|
|
317
|
+
headCommit,
|
|
318
|
+
entries,
|
|
319
|
+
counts,
|
|
320
|
+
needsRefresh: actionable > 0,
|
|
321
|
+
certain,
|
|
322
|
+
truncated,
|
|
323
|
+
notes,
|
|
324
|
+
durationMs: now() - startedAt,
|
|
325
|
+
readAt: new Date().toISOString(),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/** One-line verdict for a tool result or a log line. */
|
|
329
|
+
export function describeDelta(report) {
|
|
330
|
+
if (!report.comparable) {
|
|
331
|
+
return `无法差量判断:${report.notes[0] ?? '原因未知'}`;
|
|
332
|
+
}
|
|
333
|
+
if (!report.needsRefresh) {
|
|
334
|
+
return report.certain
|
|
335
|
+
? `无需刷新(${report.counts.checked} 个候选,${report.counts.hashed} 次哈希,${report.durationMs}ms)`
|
|
336
|
+
: `无法确认(${report.counts.checked} 个候选,${report.durationMs}ms):${report.notes[0] ?? '索引没有记录提交基线'}`;
|
|
337
|
+
}
|
|
338
|
+
const changed = report.entries.filter((entry) => entry.kind !== 'excluded');
|
|
339
|
+
const preview = changed
|
|
340
|
+
.slice(0, 10)
|
|
341
|
+
.map((entry) => `${entry.kind === 'modified' ? 'M' : entry.kind === 'added' ? 'A' : 'D'} ${entry.path}`)
|
|
342
|
+
.join('\n');
|
|
343
|
+
const more = changed.length > 10 ? `\n…还有 ${changed.length - 10} 个` : '';
|
|
344
|
+
return (`需要刷新:${report.counts.modified} 改 / ${report.counts.added} 增 / ${report.counts.deleted} 删` +
|
|
345
|
+
`(${report.durationMs}ms)\n${preview}${more}`);
|
|
346
|
+
}
|
|
347
|
+
//# sourceMappingURL=delta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/gitnexus/delta.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,QAAQ,EAAwB,MAAM,oBAAoB,CAAC;AACpE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAkB,MAAM,iBAAiB,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAIkB,CAAC;AAgGjD,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,wBAAwB,GAAG,CAAC,CAAC;AACnC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEtC,gEAAgE;AAChE,KAAK,UAAU,GAAG,CAAC,QAAgB,EAAE,IAAc,EAAE,SAAiB;IACpE,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE;YACxC,GAAG,EAAE,QAAQ;YACb,OAAO,EAAE,SAAS;YAClB,WAAW,EAAE,IAAI;YACjB,6EAA6E;YAC7E,2EAA2E;YAC3E,6EAA6E;YAC7E,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;SAC5B,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,SAAS,QAAQ,CAAC,IAAY;IAC5B,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,wEAAwE;AACxE,SAAS,KAAK,CAAC,YAAoB;IACjC,OAAO,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAC/D,CAAC;AAED,2EAA2E;AAC3E,KAAK,UAAU,QAAQ,CAAC,YAAoB;IAC1C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,IAAI,OAAO,CAAO,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE;YACjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;YAC9C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,cAAc,EAAE,CAAC,CAAC;QAC3C,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,WAAW,GAAgB;IAC/B,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IACT,SAAS,EAAE,CAAC;IACZ,QAAQ,EAAE,CAAC;IACX,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,QAAQ,EAAE,CAAC;CACZ,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB,EAAE,UAAwB,EAAE;IAC1E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC;IACxB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAC;IACvD,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,IAAI,wBAAwB,CAAC,CAAC;IACrF,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC;IAEpE,MAAM,KAAK,GAAG,CAAC,KAA2B,EAAe,EAAE,CAAC,CAAC;QAC3D,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,KAAK;QACd,UAAU,EAAE,KAAK;QACjB,KAAK,EAAE,IAAI;QACX,aAAa,EAAE,IAAI;QACnB,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,EAAE;QACX,MAAM,EAAE,EAAE,GAAG,WAAW,EAAE;QAC1B,YAAY,EAAE,KAAK;QACnB,OAAO,EAAE,KAAK;QACd,SAAS,EAAE,KAAK;QAChB,KAAK,EAAE,EAAE;QACT,UAAU,EAAE,GAAG,EAAE,GAAG,SAAS;QAC7B,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QAChC,GAAG,KAAK;KACT,CAAC,CAAC;IAEH,MAAM,IAAI,GAAqB,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;QAClB,OAAO,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;IACtE,MAAM,IAAI,GAAyB,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;IAEpE,8EAA8E;IAC9E,+EAA+E;IAC/E,8EAA8E;IAC9E,iFAAiF;IACjF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC5C,KAAK,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9D,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC;YACX,GAAG,IAAI;YACP,KAAK,EAAE,CAAC,8CAA8C,CAAC;SACxD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,UAAU,GAAG,CAAC,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;IAE7F,IAAI,KAAK,GAAsB,IAAI,CAAC;IACpC,IAAI,UAAU,GAAa,EAAE,CAAC;IAE9B,IAAI,aAAa,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,MAAM,GAAG,CACpB,OAAO,EACP,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAClE,YAAY,CACb,CAAC;QACF,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,KAAK,GAAG,OAAO,CAAC;YAChB,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QAC1C,6EAA6E;QAC7E,gFAAgF;QAChF,iFAAiF;QACjF,iFAAiF;QACjF,MAAM,WAAW,GAAG,MAAM,GAAG,CAC3B,OAAO,EACP,CAAC,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAC3D,YAAY,CACb,CAAC;QACF,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,KAAK,GAAG,UAAU,CAAC;YACnB,UAAU,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;YACX,GAAG,IAAI;YACP,UAAU;YACV,KAAK,EAAE;gBACL,aAAa,KAAK,IAAI;oBACpB,CAAC,CAAC,0CAA0C;oBAC5C,CAAC,CAAC,gDAAgD;aACrD;SACF,CAAC,CAAC;IACL,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,GAAG,CACzB,OAAO,EACP,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,EAAE,IAAI,CAAC,EACpD,YAAY,CACb,CAAC;IACF,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACvB,UAAU,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IAC1C,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;QAC7B,IAAI,GAAG,KAAK,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC1C,iFAAiF;QACjF,mFAAmF;QACnF,iFAAiF;QACjF,+DAA+D;QAC/D,IAAI,GAAG,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAS;QACtE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,IAAI,EAAE,CAAC;IAEd,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC;IAC3C,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAEhE,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,MAAM,MAAM,GAAgB,EAAE,GAAG,WAAW,EAAE,CAAC;IAC/C,MAAM,CAAC,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC;IAEjC,qFAAqF;IACrF,2EAA2E;IAC3E,KAAK,IAAI,MAAM,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,WAAW,EAAE,CAAC;QACrE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC;QAC3D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAA8B,EAAE;YAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACzC,IAAI,IAAY,CAAC;YACjB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gBACrC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;oBAAE,OAAO,IAAI,CAAC;gBAChC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;YACnB,CAAC;YAAC,MAAM,CAAC;gBACP,0EAA0E;gBAC1E,0EAA0E;gBAC1E,OAAO,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAClF,CAAC;YAED,MAAM,QAAQ,GAAG,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,wBAAwB,CAAC;gBAC1C,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,GAAG,GAAG,EAAE,CAAC;oBAC/B,OAAO;wBACL,IAAI,EAAE,GAAG;wBACT,IAAI,EAAE,UAAU;wBAChB,MAAM,EAAE,eAAe,WAAW,CAAC,GAAG,CAAC,WAAW;qBACnD,CAAC;gBACJ,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACpD,CAAC;YAED,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;YACnB,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACzC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,2EAA2E;gBAC3E,wEAAwE;gBACxE,iEAAiE;gBACjE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;YACpE,CAAC;YACD,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACzB,MAAM,CAAC,SAAS,IAAI,CAAC,CAAC;gBACtB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACvD,CAAC,CAAC,CACH,CAAC;QACF,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,KAAK,IAAI;gBAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAErD,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC;IACnE,MAAM,OAAO,GAAG,KAAK,KAAK,OAAO,CAAC;IAClC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CACR,kDAAkD,CACnD,CAAC;IACJ,CAAC;IACD,IAAI,SAAS,EAAE,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,UAAU,QAAQ,YAAY,QAAQ,cAAc,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,UAAU,KAAK,CAAC,EAAE,CAAC;QACrB,8EAA8E;QAC9E,gFAAgF;QAChF,0DAA0D;QAC1D,KAAK,CAAC,IAAI,CACR,OAAO;YACL,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,CAAC;gBACpB,CAAC,CAAC,6BAA6B;gBAC/B,CAAC,CAAC,cAAc,MAAM,CAAC,OAAO,WAAW,MAAM,CAAC,SAAS,UAAU;oBAC/D,CAAC,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,CAAC,CAAC,4CAA4C,CACjD,CAAC;QACF,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,MAAM,CAAC,QAAQ,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,MAAM,CAAC,CAAC;QAC9D,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,MAAM,CAAC,CAAC;QACxD,IAAI,MAAM,CAAC,OAAO,GAAG,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,MAAM,CAAC,CAAC;QAC5D,KAAK,CAAC,OAAO,CAAC,WAAW,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,IAAI;QAChB,KAAK;QACL,aAAa;QACb,UAAU;QACV,OAAO;QACP,MAAM;QACN,YAAY,EAAE,UAAU,GAAG,CAAC;QAC5B,OAAO;QACP,SAAS;QACT,KAAK;QACL,UAAU,EAAE,GAAG,EAAE,GAAG,SAAS;QAC7B,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACjC,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,aAAa,CAAC,MAAmB;IAC/C,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;QACvB,OAAO,UAAU,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC;IAC/C,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,OAAO;YACnB,CAAC,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,OAAO,QAAQ,MAAM,CAAC,MAAM,CAAC,MAAM,QAAQ,MAAM,CAAC,UAAU,KAAK;YACzF,CAAC,CAAC,QAAQ,MAAM,CAAC,MAAM,CAAC,OAAO,QAAQ,MAAM,CAAC,UAAU,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,YAAY,EAAE,CAAC;IACrG,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAC5E,MAAM,OAAO,GAAG,OAAO;SACpB,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC;SACZ,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;SACvG,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACzE,OAAO,CACL,QAAQ,MAAM,CAAC,MAAM,CAAC,QAAQ,QAAQ,MAAM,CAAC,MAAM,CAAC,KAAK,QAAQ,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI;QAC1F,IAAI,MAAM,CAAC,UAAU,QAAQ,OAAO,GAAG,IAAI,EAAE,CAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the GitNexus indexes under a workspace.
|
|
3
|
+
*
|
|
4
|
+
* WHY SEARCH BELOW THE WORKSPACE
|
|
5
|
+
* ------------------------------
|
|
6
|
+
* The panel reports the project the operator is *in*, and on this machine that is
|
|
7
|
+
* often a container rather than a repository: `D:\Project\geo` holds `admin/`,
|
|
8
|
+
* `backend/`, `frontend/` and others, any one of which may be the tree GitNexus
|
|
9
|
+
* indexed. Requiring the workspace itself to carry `.gitnexus` made the panel answer
|
|
10
|
+
* a workspace-shaped path with a 404 and no numbers at all.
|
|
11
|
+
*
|
|
12
|
+
* So the search starts at the workspace and walks down breadth-first. Every directory
|
|
13
|
+
* carrying a readable manifest becomes a candidate; an indexed directory is a project
|
|
14
|
+
* boundary, so the walk does not descend into its source tree (the project's own
|
|
15
|
+
* subdirectories are not separate projects). The caller decides which candidate is
|
|
16
|
+
* active; this module only reports them, ordered so the nearest complete index is
|
|
17
|
+
* first.
|
|
18
|
+
*
|
|
19
|
+
* The walk is bounded (depth, directory count, wall clock) because a workspace can be
|
|
20
|
+
* a home directory or a drive. Dependency and build directories are skipped by name,
|
|
21
|
+
* hidden directories are skipped wholesale, and symlinks are not followed, so the
|
|
22
|
+
* walk cannot leave the workspace or loop.
|
|
23
|
+
*/
|
|
24
|
+
/** One project found under the workspace. */
|
|
25
|
+
export interface ProjectCandidate {
|
|
26
|
+
/** Absolute directory carrying `.gitnexus` or `.git`. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Basename, for a compact switcher label. */
|
|
29
|
+
name: string;
|
|
30
|
+
/** Path relative to the workspace; empty for the workspace itself. */
|
|
31
|
+
relPath: string;
|
|
32
|
+
/**
|
|
33
|
+
* `index` — a readable GitNexus manifest;
|
|
34
|
+
* `git` — a git repository without an index;
|
|
35
|
+
* `workspace` — the workspace itself, with neither.
|
|
36
|
+
*/
|
|
37
|
+
kind: 'index' | 'git' | 'workspace';
|
|
38
|
+
/** Whether the directory carries a `.git` marker. */
|
|
39
|
+
hasGit: boolean;
|
|
40
|
+
/** Whether this is the workspace itself or a descendant. */
|
|
41
|
+
direction: 'self' | 'descendant';
|
|
42
|
+
/** Distance below the workspace; 0 for the workspace itself. */
|
|
43
|
+
depth: number;
|
|
44
|
+
indexedAt: string | null;
|
|
45
|
+
lastCommit: string | null;
|
|
46
|
+
branch: string | null;
|
|
47
|
+
/** The manifest's counters, when it recorded them. */
|
|
48
|
+
stats: {
|
|
49
|
+
files: number;
|
|
50
|
+
nodes: number;
|
|
51
|
+
edges: number;
|
|
52
|
+
communities: number;
|
|
53
|
+
processes: number;
|
|
54
|
+
embeddings: number;
|
|
55
|
+
} | null;
|
|
56
|
+
/** Whether the graph store is present, not just the manifest. */
|
|
57
|
+
complete: boolean;
|
|
58
|
+
/** Which manifest the values came from. */
|
|
59
|
+
metaFile: string | null;
|
|
60
|
+
}
|
|
61
|
+
/** The result of one workspace search. */
|
|
62
|
+
export interface ProjectSearchResult {
|
|
63
|
+
root: string;
|
|
64
|
+
projects: ProjectCandidate[];
|
|
65
|
+
/** Directories examined, including the root. */
|
|
66
|
+
visited: number;
|
|
67
|
+
/** Whether a depth/count/time bound stopped the walk early. */
|
|
68
|
+
truncated: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface ProjectSearchOptions {
|
|
71
|
+
/** How many directory levels below the root to inspect. */
|
|
72
|
+
maxDepth?: number;
|
|
73
|
+
/** Hard cap on directories examined. */
|
|
74
|
+
maxDirectories?: number;
|
|
75
|
+
/** Wall-clock budget for the whole search. */
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
/** Injectable clock, so a test can prove the budget is enforced. */
|
|
78
|
+
now?: () => number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find every project at or below `root`: indexed directories, git repositories without
|
|
82
|
+
* an index, and the workspace itself when it is only a container.
|
|
83
|
+
*
|
|
84
|
+
* Never throws: an unreadable directory is skipped. The caller decides what each kind
|
|
85
|
+
* means; the panel shows unindexed git projects so they can be initialized instead of
|
|
86
|
+
* reporting a bare "no index" for a workspace that clearly has subprojects.
|
|
87
|
+
*
|
|
88
|
+
* @param root absolute working directory to search from
|
|
89
|
+
*/
|
|
90
|
+
export declare function findIndexProjects(root: string, options?: ProjectSearchOptions): Promise<ProjectSearchResult>;
|
|
91
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/gitnexus/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,WAAW,CAAC;IACpC,qDAAqD;IACrD,MAAM,EAAE,OAAO,CAAC;IAChB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,sDAAsD;IACtD,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI,CAAC;IACT,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAoED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAiH9B"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find the GitNexus indexes under a workspace.
|
|
3
|
+
*
|
|
4
|
+
* WHY SEARCH BELOW THE WORKSPACE
|
|
5
|
+
* ------------------------------
|
|
6
|
+
* The panel reports the project the operator is *in*, and on this machine that is
|
|
7
|
+
* often a container rather than a repository: `D:\Project\geo` holds `admin/`,
|
|
8
|
+
* `backend/`, `frontend/` and others, any one of which may be the tree GitNexus
|
|
9
|
+
* indexed. Requiring the workspace itself to carry `.gitnexus` made the panel answer
|
|
10
|
+
* a workspace-shaped path with a 404 and no numbers at all.
|
|
11
|
+
*
|
|
12
|
+
* So the search starts at the workspace and walks down breadth-first. Every directory
|
|
13
|
+
* carrying a readable manifest becomes a candidate; an indexed directory is a project
|
|
14
|
+
* boundary, so the walk does not descend into its source tree (the project's own
|
|
15
|
+
* subdirectories are not separate projects). The caller decides which candidate is
|
|
16
|
+
* active; this module only reports them, ordered so the nearest complete index is
|
|
17
|
+
* first.
|
|
18
|
+
*
|
|
19
|
+
* The walk is bounded (depth, directory count, wall clock) because a workspace can be
|
|
20
|
+
* a home directory or a drive. Dependency and build directories are skipped by name,
|
|
21
|
+
* hidden directories are skipped wholesale, and symlinks are not followed, so the
|
|
22
|
+
* walk cannot leave the workspace or loop.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'node:fs/promises';
|
|
25
|
+
import { existsSync } from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { readIndexMeta } from './index-meta.js';
|
|
28
|
+
const DEFAULT_MAX_DEPTH = 12;
|
|
29
|
+
const DEFAULT_MAX_DIRECTORIES = 4000;
|
|
30
|
+
const DEFAULT_TIMEOUT_MS = 2000;
|
|
31
|
+
/**
|
|
32
|
+
* Directories that never hold the project the operator opened.
|
|
33
|
+
*
|
|
34
|
+
* Lower-cased because Windows paths are case-insensitive and a dependency tree can be
|
|
35
|
+
* named `Node_Modules` on a case-sensitive filesystem mounted there.
|
|
36
|
+
*/
|
|
37
|
+
const SKIP_DIRECTORIES = new Set([
|
|
38
|
+
'node_modules',
|
|
39
|
+
'bower_components',
|
|
40
|
+
'vendor',
|
|
41
|
+
'dist',
|
|
42
|
+
'build',
|
|
43
|
+
'out',
|
|
44
|
+
'output',
|
|
45
|
+
'target',
|
|
46
|
+
'bin',
|
|
47
|
+
'obj',
|
|
48
|
+
'coverage',
|
|
49
|
+
'tmp',
|
|
50
|
+
'temp',
|
|
51
|
+
'logs',
|
|
52
|
+
'venv',
|
|
53
|
+
'env',
|
|
54
|
+
'__pycache__',
|
|
55
|
+
]);
|
|
56
|
+
/** A directory the walk may enter: real directory, not hidden, not a dependency tree. */
|
|
57
|
+
function isSearchableDirectory(entry) {
|
|
58
|
+
if (!entry.isDirectory())
|
|
59
|
+
return false;
|
|
60
|
+
if (entry.name.startsWith('.'))
|
|
61
|
+
return false;
|
|
62
|
+
return !SKIP_DIRECTORIES.has(entry.name.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
/** Whether the graph store exists beside the manifest. */
|
|
65
|
+
function hasGraphStore(dir) {
|
|
66
|
+
try {
|
|
67
|
+
return existsSync(path.join(dir, '.gitnexus', 'lbug'));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rank candidates for display and for a default selection.
|
|
75
|
+
*
|
|
76
|
+
* The workspace itself comes first; indexed projects before unindexed git repositories;
|
|
77
|
+
* a complete index before one whose graph store is missing; the more recently indexed
|
|
78
|
+
* project before an older one; the path is the final deterministic tie-break.
|
|
79
|
+
*/
|
|
80
|
+
function rankProjects(a, b) {
|
|
81
|
+
if (a.direction !== b.direction)
|
|
82
|
+
return a.direction === 'self' ? -1 : 1;
|
|
83
|
+
const kindRank = (project) => project.kind === 'index' ? 0 : project.kind === 'git' ? 1 : 2;
|
|
84
|
+
if (kindRank(a) !== kindRank(b))
|
|
85
|
+
return kindRank(a) - kindRank(b);
|
|
86
|
+
if (a.depth !== b.depth)
|
|
87
|
+
return a.depth - b.depth;
|
|
88
|
+
if (a.complete !== b.complete)
|
|
89
|
+
return a.complete ? -1 : 1;
|
|
90
|
+
const aAt = a.indexedAt ?? '';
|
|
91
|
+
const bAt = b.indexedAt ?? '';
|
|
92
|
+
if (aAt !== bAt)
|
|
93
|
+
return aAt < bAt ? 1 : -1;
|
|
94
|
+
return a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Find every project at or below `root`: indexed directories, git repositories without
|
|
98
|
+
* an index, and the workspace itself when it is only a container.
|
|
99
|
+
*
|
|
100
|
+
* Never throws: an unreadable directory is skipped. The caller decides what each kind
|
|
101
|
+
* means; the panel shows unindexed git projects so they can be initialized instead of
|
|
102
|
+
* reporting a bare "no index" for a workspace that clearly has subprojects.
|
|
103
|
+
*
|
|
104
|
+
* @param root absolute working directory to search from
|
|
105
|
+
*/
|
|
106
|
+
export async function findIndexProjects(root, options = {}) {
|
|
107
|
+
const absolute = path.resolve(root);
|
|
108
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
109
|
+
const maxDirectories = options.maxDirectories ?? DEFAULT_MAX_DIRECTORIES;
|
|
110
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
111
|
+
const now = options.now ?? Date.now;
|
|
112
|
+
const startedAt = now();
|
|
113
|
+
const queue = [{ dir: absolute, depth: 0 }];
|
|
114
|
+
let head = 0;
|
|
115
|
+
let visited = 0;
|
|
116
|
+
let truncated = false;
|
|
117
|
+
const projects = [];
|
|
118
|
+
while (head < queue.length) {
|
|
119
|
+
if (now() - startedAt > timeoutMs) {
|
|
120
|
+
truncated = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
const { dir, depth } = queue[head];
|
|
124
|
+
head += 1;
|
|
125
|
+
visited += 1;
|
|
126
|
+
if (visited > maxDirectories) {
|
|
127
|
+
truncated = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const meta = await readIndexMeta(dir);
|
|
131
|
+
const hasGit = existsSync(path.join(dir, '.git'));
|
|
132
|
+
if (meta !== null) {
|
|
133
|
+
projects.push({
|
|
134
|
+
path: dir,
|
|
135
|
+
name: path.basename(dir),
|
|
136
|
+
relPath: path.relative(absolute, dir).replace(/\\/g, '/'),
|
|
137
|
+
kind: 'index',
|
|
138
|
+
hasGit,
|
|
139
|
+
direction: dir === absolute ? 'self' : 'descendant',
|
|
140
|
+
depth,
|
|
141
|
+
indexedAt: meta.indexedAt === '' ? null : meta.indexedAt,
|
|
142
|
+
lastCommit: meta.lastCommit === '' ? null : meta.lastCommit,
|
|
143
|
+
branch: meta.branch ?? null,
|
|
144
|
+
stats: meta.stats ?? null,
|
|
145
|
+
complete: hasGraphStore(dir),
|
|
146
|
+
metaFile: meta.metaFile,
|
|
147
|
+
});
|
|
148
|
+
// A project boundary: its own subdirectories are source, not sibling projects.
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (hasGit) {
|
|
152
|
+
projects.push({
|
|
153
|
+
path: dir,
|
|
154
|
+
name: path.basename(dir),
|
|
155
|
+
relPath: path.relative(absolute, dir).replace(/\\/g, '/'),
|
|
156
|
+
kind: 'git',
|
|
157
|
+
hasGit: true,
|
|
158
|
+
direction: dir === absolute ? 'self' : 'descendant',
|
|
159
|
+
depth,
|
|
160
|
+
indexedAt: null,
|
|
161
|
+
lastCommit: null,
|
|
162
|
+
branch: null,
|
|
163
|
+
stats: null,
|
|
164
|
+
complete: false,
|
|
165
|
+
metaFile: null,
|
|
166
|
+
});
|
|
167
|
+
// A git project without an index is still a project: its source is not a set of
|
|
168
|
+
// sibling projects. It appears in the switcher so the operator can initialize it.
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (dir === absolute) {
|
|
172
|
+
projects.push({
|
|
173
|
+
path: dir,
|
|
174
|
+
name: path.basename(dir),
|
|
175
|
+
relPath: '',
|
|
176
|
+
kind: 'workspace',
|
|
177
|
+
hasGit: false,
|
|
178
|
+
direction: 'self',
|
|
179
|
+
depth: 0,
|
|
180
|
+
indexedAt: null,
|
|
181
|
+
lastCommit: null,
|
|
182
|
+
branch: null,
|
|
183
|
+
stats: null,
|
|
184
|
+
complete: false,
|
|
185
|
+
metaFile: null,
|
|
186
|
+
});
|
|
187
|
+
// The workspace is a container; keep walking for its indexed/git children.
|
|
188
|
+
}
|
|
189
|
+
let entries;
|
|
190
|
+
try {
|
|
191
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (depth >= maxDepth) {
|
|
197
|
+
// Nothing is enqueued at the cap; say so only when there was something to skip.
|
|
198
|
+
if (entries.some(isSearchableDirectory))
|
|
199
|
+
truncated = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Sorted for a deterministic answer when several siblings carry an index; the
|
|
203
|
+
// order is only a tie-break, because breadth-first already prefers the nearest.
|
|
204
|
+
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
if (!isSearchableDirectory(entry))
|
|
207
|
+
continue;
|
|
208
|
+
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
projects.sort(rankProjects);
|
|
212
|
+
return { root: absolute, projects, visited, truncated };
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=discover.js.map
|