ctxman 3.0.0 → 3.1.0
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/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +380 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -13
- package/bin/cm-audit.js +124 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +683 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +675 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +113 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +294 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +52 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/sources/FileDataSource.js +6 -6
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +243 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +255 -0
- package/lib/wizards/project-detector.js +571 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for the git output `ctxman pr` reads: `--name-status -z`, `--numstat -z` and
|
|
3
|
+
* unified diffs (split per file, then hunks mapped to new-side line numbers)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} NameStatusEntry
|
|
8
|
+
* @property {string} status - A, M, D, R, C, T (U for unmerged)
|
|
9
|
+
* @property {string} path - Path at head (the old path for a deleted file)
|
|
10
|
+
* @property {string|null} oldPath - Path at base (null for an added file)
|
|
11
|
+
* @property {number|null} similarity - Rename/copy similarity in percent
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse `git diff --name-status -z`
|
|
16
|
+
* @param {string} output
|
|
17
|
+
* @returns {NameStatusEntry[]}
|
|
18
|
+
*/
|
|
19
|
+
export function parseNameStatus(output) {
|
|
20
|
+
const parts = output.split('\0');
|
|
21
|
+
const entries = [];
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < parts.length) {
|
|
24
|
+
const code = parts[i++];
|
|
25
|
+
if (!code) continue;
|
|
26
|
+
const status = code[0];
|
|
27
|
+
if (status === 'R' || status === 'C') {
|
|
28
|
+
const oldPath = parts[i++];
|
|
29
|
+
const newPath = parts[i++];
|
|
30
|
+
entries.push({ status, path: newPath, oldPath, similarity: Number(code.slice(1)) || 0 });
|
|
31
|
+
} else {
|
|
32
|
+
const filePath = parts[i++];
|
|
33
|
+
entries.push({
|
|
34
|
+
status,
|
|
35
|
+
path: filePath,
|
|
36
|
+
oldPath: status === 'A' ? null : filePath,
|
|
37
|
+
similarity: null,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse `git diff --numstat -z` into per-path line counts (binary files count 0/0)
|
|
46
|
+
* @param {string} output
|
|
47
|
+
* @returns {Map<string, {additions: number, deletions: number, binary: boolean}>}
|
|
48
|
+
* Keyed by the head-side path
|
|
49
|
+
*/
|
|
50
|
+
export function parseNumstat(output) {
|
|
51
|
+
const parts = output.split('\0');
|
|
52
|
+
const stats = new Map();
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < parts.length) {
|
|
55
|
+
const record = parts[i++];
|
|
56
|
+
if (!record) continue;
|
|
57
|
+
const firstTab = record.indexOf('\t');
|
|
58
|
+
const secondTab = record.indexOf('\t', firstTab + 1);
|
|
59
|
+
if (firstTab === -1 || secondTab === -1) continue;
|
|
60
|
+
const added = record.slice(0, firstTab);
|
|
61
|
+
const deleted = record.slice(firstTab + 1, secondTab);
|
|
62
|
+
let filePath = record.slice(secondTab + 1);
|
|
63
|
+
if (filePath === '') {
|
|
64
|
+
// Rename or copy: old and new paths follow as separate fields
|
|
65
|
+
i++;
|
|
66
|
+
filePath = parts[i++];
|
|
67
|
+
}
|
|
68
|
+
const binary = added === '-' || deleted === '-';
|
|
69
|
+
stats.set(filePath, {
|
|
70
|
+
additions: binary ? 0 : Number(added) || 0,
|
|
71
|
+
deletions: binary ? 0 : Number(deleted) || 0,
|
|
72
|
+
binary,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return stats;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Undo git's C-style quoting of a path ("a/t\303\244b\tc" → a/täb<TAB>c)
|
|
80
|
+
* @param {string} text
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
export function unquoteGitPath(text) {
|
|
84
|
+
if (!(text.length >= 2 && text.startsWith('"') && text.endsWith('"'))) return text;
|
|
85
|
+
const body = text.slice(1, -1);
|
|
86
|
+
const bytes = [];
|
|
87
|
+
const escapes = { n: 10, t: 9, r: 13, a: 7, b: 8, f: 12, v: 11, '"': 34, '\\': 92 };
|
|
88
|
+
for (let i = 0; i < body.length; i++) {
|
|
89
|
+
const ch = body[i];
|
|
90
|
+
if (ch !== '\\') {
|
|
91
|
+
bytes.push(...Buffer.from(ch, 'utf8'));
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const next = body[i + 1];
|
|
95
|
+
if (/[0-7]/.test(next)) {
|
|
96
|
+
const octal = body.slice(i + 1, i + 4);
|
|
97
|
+
bytes.push(parseInt(octal, 8));
|
|
98
|
+
i += 3;
|
|
99
|
+
} else {
|
|
100
|
+
bytes.push(escapes[next] ?? next.charCodeAt(0));
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return Buffer.from(bytes).toString('utf8');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Strip the a/ or b/ prefix (and the tab git appends to names with spaces) */
|
|
108
|
+
function headerPath(raw, prefix) {
|
|
109
|
+
let value = raw.replace(/\t$/, '');
|
|
110
|
+
if (value === '/dev/null') return null;
|
|
111
|
+
value = unquoteGitPath(value);
|
|
112
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Paths from a `diff --git a/x b/y` line when nothing else names them (binary files,
|
|
117
|
+
* mode-only changes); exact only when both sides are equal, which is the common case
|
|
118
|
+
* @param {string} rest - Text after "diff --git "
|
|
119
|
+
*/
|
|
120
|
+
function pathsFromGitLine(rest) {
|
|
121
|
+
if (rest.startsWith('"')) {
|
|
122
|
+
const match = /^("(?:[^"\\]|\\.)*") ("(?:[^"\\]|\\.)*"|\S.*)$/.exec(rest);
|
|
123
|
+
if (match) return [headerPath(match[1], 'a/'), headerPath(match[2], 'b/')];
|
|
124
|
+
}
|
|
125
|
+
const length = (rest.length - 5) / 2;
|
|
126
|
+
if (Number.isInteger(length) && length > 0) {
|
|
127
|
+
const oldPath = rest.slice(2, 2 + length);
|
|
128
|
+
if (rest.slice(2 + length) === ` b/${oldPath}`) return [oldPath, oldPath];
|
|
129
|
+
}
|
|
130
|
+
const split = rest.indexOf(' b/');
|
|
131
|
+
return split === -1 ? [null, null] : [rest.slice(2, split), rest.slice(split + 3)];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @typedef {object} FileDiff
|
|
136
|
+
* @property {string|null} oldPath
|
|
137
|
+
* @property {string|null} newPath
|
|
138
|
+
* @property {string} text - The block from its `diff --git` line on
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Split a multi-file unified diff (made with --src-prefix=a/ --dst-prefix=b/) per file
|
|
143
|
+
* @param {string} diffText
|
|
144
|
+
* @returns {FileDiff[]}
|
|
145
|
+
*/
|
|
146
|
+
export function splitDiff(diffText) {
|
|
147
|
+
if (!diffText) return [];
|
|
148
|
+
const lines = diffText.split('\n');
|
|
149
|
+
const blocks = [];
|
|
150
|
+
let current = null;
|
|
151
|
+
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
if (line.startsWith('diff --git ')) {
|
|
154
|
+
current = { lines: [line], header: true };
|
|
155
|
+
blocks.push(current);
|
|
156
|
+
} else if (current) {
|
|
157
|
+
current.lines.push(line);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return blocks.map(({ lines: blockLines }) => {
|
|
162
|
+
let oldPath;
|
|
163
|
+
let newPath;
|
|
164
|
+
let renameFrom = null;
|
|
165
|
+
let renameTo = null;
|
|
166
|
+
for (const line of blockLines.slice(1)) {
|
|
167
|
+
if (line.startsWith('@@')) break;
|
|
168
|
+
if (line.startsWith('rename from ') || line.startsWith('copy from ')) {
|
|
169
|
+
renameFrom = unquoteGitPath(line.slice(line.indexOf(' from ') + 6));
|
|
170
|
+
} else if (line.startsWith('rename to ') || line.startsWith('copy to ')) {
|
|
171
|
+
renameTo = unquoteGitPath(line.slice(line.indexOf(' to ') + 4));
|
|
172
|
+
} else if (line.startsWith('--- ')) {
|
|
173
|
+
oldPath = headerPath(line.slice(4), 'a/');
|
|
174
|
+
} else if (line.startsWith('+++ ')) {
|
|
175
|
+
newPath = headerPath(line.slice(4), 'b/');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (oldPath === undefined || newPath === undefined) {
|
|
179
|
+
const [gitOld, gitNew] = pathsFromGitLine(blockLines[0].slice('diff --git '.length));
|
|
180
|
+
const isNew = blockLines.some((line) => line.startsWith('new file mode'));
|
|
181
|
+
const isDeleted = blockLines.some((line) => line.startsWith('deleted file mode'));
|
|
182
|
+
if (oldPath === undefined) oldPath = isNew ? null : (renameFrom ?? gitOld);
|
|
183
|
+
if (newPath === undefined) newPath = isDeleted ? null : (renameTo ?? gitNew);
|
|
184
|
+
}
|
|
185
|
+
// Drop the empty string left by the diff's final newline
|
|
186
|
+
while (blockLines.length > 1 && blockLines[blockLines.length - 1] === '') blockLines.pop();
|
|
187
|
+
return { oldPath, newPath, text: blockLines.join('\n') };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* New-side lines a file diff touches: the line numbers of added lines, and for each run
|
|
193
|
+
* of deleted lines the new-side number of the line that follows it
|
|
194
|
+
* @param {string} fileDiffText
|
|
195
|
+
* @returns {{addedLines: number[], deletionAnchors: number[]}}
|
|
196
|
+
*/
|
|
197
|
+
export function parseHunks(fileDiffText) {
|
|
198
|
+
const addedLines = [];
|
|
199
|
+
const deletionAnchors = [];
|
|
200
|
+
let newLine = 0;
|
|
201
|
+
let inHunk = false;
|
|
202
|
+
let previousWasDeletion = false;
|
|
203
|
+
|
|
204
|
+
for (const line of fileDiffText.split('\n')) {
|
|
205
|
+
if (line.startsWith('@@')) {
|
|
206
|
+
const match = /^@@+ (?:-\d+(?:,\d+)? )+\+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
207
|
+
inHunk = Boolean(match);
|
|
208
|
+
if (match) {
|
|
209
|
+
const start = Number(match[1]);
|
|
210
|
+
// An empty new side (-U0 pure deletion) names the line before the gap
|
|
211
|
+
newLine = match[2] === '0' ? start + 1 : start;
|
|
212
|
+
}
|
|
213
|
+
previousWasDeletion = false;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!inHunk) continue;
|
|
217
|
+
if (line.startsWith('+')) {
|
|
218
|
+
addedLines.push(newLine);
|
|
219
|
+
newLine++;
|
|
220
|
+
previousWasDeletion = false;
|
|
221
|
+
} else if (line.startsWith('-')) {
|
|
222
|
+
if (!previousWasDeletion) deletionAnchors.push(newLine);
|
|
223
|
+
previousWasDeletion = true;
|
|
224
|
+
} else if (line.startsWith(' ')) {
|
|
225
|
+
newLine++;
|
|
226
|
+
previousWasDeletion = false;
|
|
227
|
+
} else if (!line.startsWith('\\')) {
|
|
228
|
+
// Anything else ends the hunk (the next file's header)
|
|
229
|
+
inHunk = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { addedLines, deletionAnchors };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A unified diff that adds a whole file, for untracked files in --working mode
|
|
238
|
+
* @param {string} filePath
|
|
239
|
+
* @param {string|null} content - null for binary content
|
|
240
|
+
* @returns {string}
|
|
241
|
+
*/
|
|
242
|
+
export function syntheticAddedDiff(filePath, content) {
|
|
243
|
+
const header = `diff --git a/${filePath} b/${filePath}\nnew file mode 100644`;
|
|
244
|
+
if (content === null) {
|
|
245
|
+
return `${header}\nBinary files /dev/null and b/${filePath} differ`;
|
|
246
|
+
}
|
|
247
|
+
if (content === '') return header;
|
|
248
|
+
const lines = content.split('\n');
|
|
249
|
+
const missingNewline = lines[lines.length - 1] !== '';
|
|
250
|
+
if (!missingNewline) lines.pop();
|
|
251
|
+
const body = lines.map((line) => `+${line}`).join('\n');
|
|
252
|
+
return (
|
|
253
|
+
`${header}\n--- /dev/null\n+++ b/${filePath}\n@@ -0,0 +1${lines.length === 1 ? '' : `,${lines.length}`} @@\n` +
|
|
254
|
+
body +
|
|
255
|
+
(missingNewline ? '\n\' : '')
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Number of lines in file content, as git counts them for --numstat
|
|
261
|
+
* @param {string} content
|
|
262
|
+
*/
|
|
263
|
+
export function countLines(content) {
|
|
264
|
+
if (!content) return 0;
|
|
265
|
+
const newlines = content.split('\n').length - 1;
|
|
266
|
+
return content.endsWith('\n') ? newlines : newlines + 1;
|
|
267
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map diff lines to the methods that contain them, and find methods a change removed,
|
|
3
|
+
* with the existing regex method analyzers (lib/analyzers/method-analyzer.js, which hands
|
|
4
|
+
* Go files to go-method-analyzer.js)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import MethodAnalyzer from '../analyzers/method-analyzer.js';
|
|
9
|
+
import FileUtils from '../utils/file-utils.js';
|
|
10
|
+
|
|
11
|
+
// The analyzer treats unknown extensions as JavaScript; these are JavaScript too
|
|
12
|
+
const EXTRA_JS_EXTENSIONS = new Set(['.mjs', '.cjs', '.mts', '.cts']);
|
|
13
|
+
|
|
14
|
+
const analyzer = new MethodAnalyzer();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {object} MethodRange
|
|
18
|
+
* @property {string} name
|
|
19
|
+
* @property {number} start - First line (1-based)
|
|
20
|
+
* @property {number} end - Last line (inclusive)
|
|
21
|
+
* @property {string} source - Lines start..end
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether method extraction knows the file's language
|
|
26
|
+
* @param {string} filePath
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export function supportsMethods(filePath) {
|
|
30
|
+
return (
|
|
31
|
+
FileUtils.isCode(filePath) || EXTRA_JS_EXTENSIONS.has(path.extname(filePath).toLowerCase())
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Methods of a file with their line ranges, sorted by start line
|
|
37
|
+
* @param {string} content
|
|
38
|
+
* @param {string} filePath - Used to pick the language
|
|
39
|
+
* @returns {MethodRange[]}
|
|
40
|
+
*/
|
|
41
|
+
export function extractMethodRanges(content, filePath) {
|
|
42
|
+
let methods;
|
|
43
|
+
let sources;
|
|
44
|
+
try {
|
|
45
|
+
methods = analyzer.extractMethods(content, filePath);
|
|
46
|
+
sources = analyzer.extractMethodSources(content, methods, filePath);
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return methods
|
|
51
|
+
.map((method, i) => ({
|
|
52
|
+
name: method.name,
|
|
53
|
+
start: method.line,
|
|
54
|
+
end: method.line + sources[i].split('\n').length - 1,
|
|
55
|
+
source: sources[i],
|
|
56
|
+
}))
|
|
57
|
+
.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The smallest method that satisfies `contains` */
|
|
61
|
+
function innermost(methods, contains) {
|
|
62
|
+
let best = null;
|
|
63
|
+
for (const method of methods) {
|
|
64
|
+
if (contains(method) && (!best || method.end - method.start < best.end - best.start)) {
|
|
65
|
+
best = method;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return best;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Methods whose lines a diff touches. Each added line belongs to the innermost method
|
|
73
|
+
* around it; a deletion belongs to a method when it happened after the method's first
|
|
74
|
+
* line and before or at its last. A touched method nested in another touched method is
|
|
75
|
+
* left out, since the outer one's source already contains it.
|
|
76
|
+
* @param {MethodRange[]} methods - Methods of the head version
|
|
77
|
+
* @param {{addedLines: number[], deletionAnchors: number[]}} hunks - From parseHunks()
|
|
78
|
+
* @returns {Array<MethodRange & {changedLines: number}>}
|
|
79
|
+
*/
|
|
80
|
+
export function findTouchedMethods(methods, { addedLines, deletionAnchors }) {
|
|
81
|
+
if (methods.length === 0) return [];
|
|
82
|
+
const touched = new Map();
|
|
83
|
+
const mark = (method) => method && touched.set(method, (touched.get(method) || 0) + 1);
|
|
84
|
+
|
|
85
|
+
for (const line of addedLines) {
|
|
86
|
+
mark(innermost(methods, (m) => m.start <= line && line <= m.end));
|
|
87
|
+
}
|
|
88
|
+
for (const anchor of deletionAnchors) {
|
|
89
|
+
mark(innermost(methods, (m) => m.start < anchor && anchor <= m.end));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const list = [...touched.keys()];
|
|
93
|
+
const containedIn = (inner) =>
|
|
94
|
+
list.find(
|
|
95
|
+
(outer) =>
|
|
96
|
+
outer !== inner &&
|
|
97
|
+
outer.start <= inner.start &&
|
|
98
|
+
inner.end <= outer.end &&
|
|
99
|
+
outer.end - outer.start > inner.end - inner.start
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const result = new Map();
|
|
103
|
+
for (const method of list) {
|
|
104
|
+
let target = method;
|
|
105
|
+
let outer;
|
|
106
|
+
while ((outer = containedIn(target))) target = outer;
|
|
107
|
+
result.set(target, (result.get(target) || 0) + touched.get(method));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...result.entries()]
|
|
111
|
+
.map(([method, changedLines]) => ({ ...method, changedLines }))
|
|
112
|
+
.sort((a, b) => a.start - b.start);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Methods of the base version that the head version no longer has. A name that still
|
|
117
|
+
* exists fewer times than before reports the base occurrences whose source no longer
|
|
118
|
+
* appears in the head content.
|
|
119
|
+
* @param {MethodRange[]} baseMethods
|
|
120
|
+
* @param {MethodRange[]} headMethods
|
|
121
|
+
* @param {string} headContent
|
|
122
|
+
* @returns {MethodRange[]}
|
|
123
|
+
*/
|
|
124
|
+
export function findDeletedMethods(baseMethods, headMethods, headContent = '') {
|
|
125
|
+
const headCount = new Map();
|
|
126
|
+
for (const method of headMethods) {
|
|
127
|
+
headCount.set(method.name, (headCount.get(method.name) || 0) + 1);
|
|
128
|
+
}
|
|
129
|
+
const byName = new Map();
|
|
130
|
+
for (const method of baseMethods) {
|
|
131
|
+
if (!byName.has(method.name)) byName.set(method.name, []);
|
|
132
|
+
byName.get(method.name).push(method);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const deleted = [];
|
|
136
|
+
for (const [name, occurrences] of byName) {
|
|
137
|
+
const missing = occurrences.length - (headCount.get(name) || 0);
|
|
138
|
+
if (missing <= 0) continue;
|
|
139
|
+
if (missing === occurrences.length) {
|
|
140
|
+
deleted.push(...occurrences);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const gone = occurrences.filter((method) => !headContent.includes(method.source));
|
|
144
|
+
const rest = occurrences.filter((method) => !gone.includes(method));
|
|
145
|
+
deleted.push(...[...gone, ...rest].slice(0, missing));
|
|
146
|
+
}
|
|
147
|
+
return deleted.sort((a, b) => a.start - b.start);
|
|
148
|
+
}
|