seer-mcp 0.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/.vscode/settings.json +3 -0
- package/LICENSE +176 -0
- package/README.md +272 -0
- package/README_dev.md +199 -0
- package/dist/bundle/ci.d.ts +47 -0
- package/dist/bundle/ci.d.ts.map +1 -0
- package/dist/bundle/ci.js +113 -0
- package/dist/bundle/ci.js.map +1 -0
- package/dist/bundle/contract.d.ts +111 -0
- package/dist/bundle/contract.d.ts.map +1 -0
- package/dist/bundle/contract.js +352 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/export.d.ts +36 -0
- package/dist/bundle/export.d.ts.map +1 -0
- package/dist/bundle/export.js +152 -0
- package/dist/bundle/export.js.map +1 -0
- package/dist/bundle/external.d.ts +66 -0
- package/dist/bundle/external.d.ts.map +1 -0
- package/dist/bundle/external.js +238 -0
- package/dist/bundle/external.js.map +1 -0
- package/dist/bundle/format.d.ts +94 -0
- package/dist/bundle/format.d.ts.map +1 -0
- package/dist/bundle/format.js +42 -0
- package/dist/bundle/format.js.map +1 -0
- package/dist/bundle/import.d.ts +49 -0
- package/dist/bundle/import.d.ts.map +1 -0
- package/dist/bundle/import.js +116 -0
- package/dist/bundle/import.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +1402 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +48 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +284 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/db/schema.d.ts +3 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +616 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/store.d.ts +1011 -0
- package/dist/db/store.d.ts.map +1 -0
- package/dist/db/store.js +3888 -0
- package/dist/db/store.js.map +1 -0
- package/dist/graph/pagerank.d.ts +9 -0
- package/dist/graph/pagerank.d.ts.map +1 -0
- package/dist/graph/pagerank.js +47 -0
- package/dist/graph/pagerank.js.map +1 -0
- package/dist/indexer/architecture.d.ts +72 -0
- package/dist/indexer/architecture.d.ts.map +1 -0
- package/dist/indexer/architecture.js +112 -0
- package/dist/indexer/architecture.js.map +1 -0
- package/dist/indexer/behavior.d.ts +75 -0
- package/dist/indexer/behavior.d.ts.map +1 -0
- package/dist/indexer/behavior.js +395 -0
- package/dist/indexer/behavior.js.map +1 -0
- package/dist/indexer/boundaries.d.ts +60 -0
- package/dist/indexer/boundaries.d.ts.map +1 -0
- package/dist/indexer/boundaries.js +366 -0
- package/dist/indexer/boundaries.js.map +1 -0
- package/dist/indexer/churn.d.ts +15 -0
- package/dist/indexer/churn.d.ts.map +1 -0
- package/dist/indexer/churn.js +49 -0
- package/dist/indexer/churn.js.map +1 -0
- package/dist/indexer/classify.d.ts +9 -0
- package/dist/indexer/classify.d.ts.map +1 -0
- package/dist/indexer/classify.js +90 -0
- package/dist/indexer/classify.js.map +1 -0
- package/dist/indexer/context.d.ts +176 -0
- package/dist/indexer/context.d.ts.map +1 -0
- package/dist/indexer/context.js +193 -0
- package/dist/indexer/context.js.map +1 -0
- package/dist/indexer/continuity.d.ts +67 -0
- package/dist/indexer/continuity.d.ts.map +1 -0
- package/dist/indexer/continuity.js +288 -0
- package/dist/indexer/continuity.js.map +1 -0
- package/dist/indexer/detectchanges.d.ts +32 -0
- package/dist/indexer/detectchanges.d.ts.map +1 -0
- package/dist/indexer/detectchanges.js +74 -0
- package/dist/indexer/detectchanges.js.map +1 -0
- package/dist/indexer/discovery.d.ts +37 -0
- package/dist/indexer/discovery.d.ts.map +1 -0
- package/dist/indexer/discovery.js +136 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/externaldeps.d.ts +18 -0
- package/dist/indexer/externaldeps.d.ts.map +1 -0
- package/dist/indexer/externaldeps.js +288 -0
- package/dist/indexer/externaldeps.js.map +1 -0
- package/dist/indexer/freshness.d.ts +48 -0
- package/dist/indexer/freshness.d.ts.map +1 -0
- package/dist/indexer/freshness.js +128 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/git.d.ts +144 -0
- package/dist/indexer/git.d.ts.map +1 -0
- package/dist/indexer/git.js +444 -0
- package/dist/indexer/git.js.map +1 -0
- package/dist/indexer/index.d.ts +145 -0
- package/dist/indexer/index.d.ts.map +1 -0
- package/dist/indexer/index.js +930 -0
- package/dist/indexer/index.js.map +1 -0
- package/dist/indexer/modules.d.ts +62 -0
- package/dist/indexer/modules.d.ts.map +1 -0
- package/dist/indexer/modules.js +293 -0
- package/dist/indexer/modules.js.map +1 -0
- package/dist/indexer/preflight.d.ts +154 -0
- package/dist/indexer/preflight.d.ts.map +1 -0
- package/dist/indexer/preflight.js +399 -0
- package/dist/indexer/preflight.js.map +1 -0
- package/dist/indexer/protoScanner.d.ts +34 -0
- package/dist/indexer/protoScanner.d.ts.map +1 -0
- package/dist/indexer/protoScanner.js +133 -0
- package/dist/indexer/protoScanner.js.map +1 -0
- package/dist/indexer/risk.d.ts +115 -0
- package/dist/indexer/risk.d.ts.map +1 -0
- package/dist/indexer/risk.js +194 -0
- package/dist/indexer/risk.js.map +1 -0
- package/dist/indexer/serviceHostScanner.d.ts +25 -0
- package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
- package/dist/indexer/serviceHostScanner.js +95 -0
- package/dist/indexer/serviceHostScanner.js.map +1 -0
- package/dist/indexer/serviceLinks.d.ts +105 -0
- package/dist/indexer/serviceLinks.d.ts.map +1 -0
- package/dist/indexer/serviceLinks.js +509 -0
- package/dist/indexer/serviceLinks.js.map +1 -0
- package/dist/indexer/shapehash.d.ts +98 -0
- package/dist/indexer/shapehash.d.ts.map +1 -0
- package/dist/indexer/shapehash.js +354 -0
- package/dist/indexer/shapehash.js.map +1 -0
- package/dist/indexer/skeleton.d.ts +15 -0
- package/dist/indexer/skeleton.d.ts.map +1 -0
- package/dist/indexer/skeleton.js +136 -0
- package/dist/indexer/skeleton.js.map +1 -0
- package/dist/indexer/symbolhistory.d.ts +41 -0
- package/dist/indexer/symbolhistory.d.ts.map +1 -0
- package/dist/indexer/symbolhistory.js +124 -0
- package/dist/indexer/symbolhistory.js.map +1 -0
- package/dist/indexer/watcher.d.ts +68 -0
- package/dist/indexer/watcher.d.ts.map +1 -0
- package/dist/indexer/watcher.js +179 -0
- package/dist/indexer/watcher.js.map +1 -0
- package/dist/mcp/server.d.ts +80 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +1610 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/parser/index.d.ts +8 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +33 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/languages/cpp.d.ts +3 -0
- package/dist/parser/languages/cpp.d.ts.map +1 -0
- package/dist/parser/languages/cpp.js +350 -0
- package/dist/parser/languages/cpp.js.map +1 -0
- package/dist/parser/languages/csharp.d.ts +3 -0
- package/dist/parser/languages/csharp.d.ts.map +1 -0
- package/dist/parser/languages/csharp.js +239 -0
- package/dist/parser/languages/csharp.js.map +1 -0
- package/dist/parser/languages/go.d.ts +3 -0
- package/dist/parser/languages/go.d.ts.map +1 -0
- package/dist/parser/languages/go.js +259 -0
- package/dist/parser/languages/go.js.map +1 -0
- package/dist/parser/languages/java.d.ts +3 -0
- package/dist/parser/languages/java.d.ts.map +1 -0
- package/dist/parser/languages/java.js +391 -0
- package/dist/parser/languages/java.js.map +1 -0
- package/dist/parser/languages/python.d.ts +3 -0
- package/dist/parser/languages/python.d.ts.map +1 -0
- package/dist/parser/languages/python.js +396 -0
- package/dist/parser/languages/python.js.map +1 -0
- package/dist/parser/languages/rust.d.ts +3 -0
- package/dist/parser/languages/rust.d.ts.map +1 -0
- package/dist/parser/languages/rust.js +159 -0
- package/dist/parser/languages/rust.js.map +1 -0
- package/dist/parser/languages/typescript.d.ts +3 -0
- package/dist/parser/languages/typescript.d.ts.map +1 -0
- package/dist/parser/languages/typescript.js +1442 -0
- package/dist/parser/languages/typescript.js.map +1 -0
- package/dist/parser/parserContext.d.ts +77 -0
- package/dist/parser/parserContext.d.ts.map +1 -0
- package/dist/parser/parserContext.js +354 -0
- package/dist/parser/parserContext.js.map +1 -0
- package/dist/parser/walker.d.ts +81 -0
- package/dist/parser/walker.d.ts.map +1 -0
- package/dist/parser/walker.js +217 -0
- package/dist/parser/walker.js.map +1 -0
- package/dist/parser/worker.d.ts +66 -0
- package/dist/parser/worker.d.ts.map +1 -0
- package/dist/parser/worker.js +129 -0
- package/dist/parser/worker.js.map +1 -0
- package/dist/parser/workerpool.d.ts +107 -0
- package/dist/parser/workerpool.d.ts.map +1 -0
- package/dist/parser/workerpool.js +383 -0
- package/dist/parser/workerpool.js.map +1 -0
- package/dist/scip/format.d.ts +87 -0
- package/dist/scip/format.d.ts.map +1 -0
- package/dist/scip/format.js +31 -0
- package/dist/scip/format.js.map +1 -0
- package/dist/scip/import.d.ts +37 -0
- package/dist/scip/import.d.ts.map +1 -0
- package/dist/scip/import.js +180 -0
- package/dist/scip/import.js.map +1 -0
- package/dist/types.d.ts +392 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.md +105 -0
- package/docs/benchmarks/methodology.md +134 -0
- package/docs/benchmarks/raw-results.md +71 -0
- package/docs/benchmarks.md +74 -0
- package/docs/cli.md +148 -0
- package/docs/examples/behavior-tests.md +70 -0
- package/docs/examples/change-history.md +85 -0
- package/docs/examples/pre-edit-context.md +81 -0
- package/docs/examples/service-links.md +88 -0
- package/docs/examples.md +80 -0
- package/docs/faq.md +70 -0
- package/docs/internals.md +104 -0
- package/docs/languages.md +70 -0
- package/docs/limits.md +52 -0
- package/docs/mcp.md +199 -0
- package/docs/quickstart.md +119 -0
- package/docs/testing.md +123 -0
- package/docs/tools.md +115 -0
- package/package.json +52 -0
- package/research-codebase.md +578 -0
- package/seer-cli-docs.md +326 -0
- package/seer-master-guide.md +246 -0
- package/src/bundle/ci.ts +141 -0
- package/src/bundle/contract.ts +387 -0
- package/src/bundle/export.ts +175 -0
- package/src/bundle/external.ts +285 -0
- package/src/bundle/format.ts +92 -0
- package/src/bundle/import.ts +157 -0
- package/src/cli/index.ts +1249 -0
- package/src/cli/init.ts +389 -0
- package/src/db/schema.ts +614 -0
- package/src/db/store.ts +4306 -0
- package/src/graph/pagerank.ts +53 -0
- package/src/indexer/architecture.ts +148 -0
- package/src/indexer/behavior.ts +466 -0
- package/src/indexer/boundaries.ts +374 -0
- package/src/indexer/churn.ts +58 -0
- package/src/indexer/classify.ts +96 -0
- package/src/indexer/context.ts +340 -0
- package/src/indexer/continuity.ts +322 -0
- package/src/indexer/detectchanges.ts +94 -0
- package/src/indexer/discovery.ts +176 -0
- package/src/indexer/externaldeps.ts +243 -0
- package/src/indexer/freshness.ts +166 -0
- package/src/indexer/git.ts +453 -0
- package/src/indexer/index.ts +1092 -0
- package/src/indexer/modules.ts +358 -0
- package/src/indexer/preflight.ts +548 -0
- package/src/indexer/protoScanner.ts +147 -0
- package/src/indexer/risk.ts +304 -0
- package/src/indexer/serviceHostScanner.ts +92 -0
- package/src/indexer/serviceLinks.ts +543 -0
- package/src/indexer/shapehash.ts +370 -0
- package/src/indexer/skeleton.ts +169 -0
- package/src/indexer/symbolhistory.ts +172 -0
- package/src/indexer/watcher.ts +206 -0
- package/src/mcp/server.ts +1659 -0
- package/src/parser/index.ts +37 -0
- package/src/parser/languages/cpp.ts +361 -0
- package/src/parser/languages/csharp.ts +235 -0
- package/src/parser/languages/go.ts +259 -0
- package/src/parser/languages/java.ts +382 -0
- package/src/parser/languages/python.ts +370 -0
- package/src/parser/languages/rust.ts +164 -0
- package/src/parser/languages/typescript.ts +1435 -0
- package/src/parser/parserContext.ts +392 -0
- package/src/parser/walker.ts +306 -0
- package/src/parser/worker.ts +181 -0
- package/src/parser/workerpool.ts +448 -0
- package/src/scip/format.ts +83 -0
- package/src/scip/import.ts +216 -0
- package/src/types.ts +457 -0
- package/tests/benchmark-service-links.ts +244 -0
- package/tests/bug-regressions.ts +626 -0
- package/tests/filters.ts +264 -0
- package/tests/fixtures/Counter.tsx +38 -0
- package/tests/fixtures/caller.ts +7 -0
- package/tests/fixtures/collisions.ts +23 -0
- package/tests/fixtures/local_helper.ts +5 -0
- package/tests/fixtures/overloads.java +17 -0
- package/tests/fixtures/remote_helper.ts +4 -0
- package/tests/fixtures/sample.c +15 -0
- package/tests/fixtures/sample.cpp +47 -0
- package/tests/fixtures/sample.cs +62 -0
- package/tests/fixtures/sample.go +68 -0
- package/tests/fixtures/sample.h +30 -0
- package/tests/fixtures/sample.java +85 -0
- package/tests/fixtures/sample.py +46 -0
- package/tests/fixtures/sample.rs +78 -0
- package/tests/fixtures/sample.ts +76 -0
- package/tests/fixtures-service/HttpClients.cs +30 -0
- package/tests/fixtures-service/HttpClients.java +24 -0
- package/tests/fixtures-service/billing.ts +15 -0
- package/tests/fixtures-service/docker-compose.yml +15 -0
- package/tests/fixtures-service/gateway.ts +10 -0
- package/tests/fixtures-service/get_user.ts +11 -0
- package/tests/fixtures-service/graphql_client.ts +63 -0
- package/tests/fixtures-service/graphql_server.ts +30 -0
- package/tests/fixtures-service/grpc_client.go +30 -0
- package/tests/fixtures-service/http_clients.go +23 -0
- package/tests/fixtures-service/http_clients.py +38 -0
- package/tests/fixtures-service/http_clients.ts +49 -0
- package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
- package/tests/fixtures-service/k8s_calls.ts +20 -0
- package/tests/fixtures-service/messaging.ts +87 -0
- package/tests/fixtures-service/trpc_client.ts +39 -0
- package/tests/fixtures-service/trpc_server.ts +39 -0
- package/tests/fixtures-service/user_service.proto +33 -0
- package/tests/fixtures-trackcd/Cargo.toml +11 -0
- package/tests/fixtures-trackcd/SpringController.java +36 -0
- package/tests/fixtures-trackcd/auth_service.ts +19 -0
- package/tests/fixtures-trackcd/complex_module.py +50 -0
- package/tests/fixtures-trackcd/express_app.js +30 -0
- package/tests/fixtures-trackcd/fastapi_app.py +49 -0
- package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
- package/tests/fixtures-trackcd/go.mod +8 -0
- package/tests/fixtures-trackcd/package.json +15 -0
- package/tests/fixtures-trackcd/requirements.txt +4 -0
- package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
- package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
- package/tests/fixtures-tracke/auth/crypto.ts +7 -0
- package/tests/fixtures-tracke/billing/Billing.ts +20 -0
- package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
- package/tests/fixtures-tracke/billing/server.ts +17 -0
- package/tests/fixtures-tracke/package.json +7 -0
- package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
- package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
- package/tests/fixtures-trackf/package.json +5 -0
- package/tests/fixtures-trackf/src/auth.ts +26 -0
- package/tests/fixtures-trackf/src/handlers.ts +35 -0
- package/tests/fixtures-tracki/billing/routes.ts +12 -0
- package/tests/fixtures-tracki/gateway/client.ts +13 -0
- package/tests/git-features.ts +267 -0
- package/tests/init.ts +141 -0
- package/tests/mcp-jit.ts +130 -0
- package/tests/mcp-smoke.ts +191 -0
- package/tests/mcp-trackcd.ts +169 -0
- package/tests/mcp-tracke.ts +229 -0
- package/tests/mcp-trackf.ts +330 -0
- package/tests/mcp-trackg.ts +219 -0
- package/tests/mcp-tracki.ts +174 -0
- package/tests/mcp-watcher.ts +126 -0
- package/tests/optspec.ts +194 -0
- package/tests/parallel-index.ts +333 -0
- package/tests/parallel-read.ts +125 -0
- package/tests/parallel-recovery.ts +241 -0
- package/tests/perf-callers.ts +145 -0
- package/tests/query-parity.ts +184 -0
- package/tests/query-perf.ts +55 -0
- package/tests/scale-parallel-parity.ts +225 -0
- package/tests/scale-test.ts +523 -0
- package/tests/smoke.ts +396 -0
- package/tests/trackcd.ts +325 -0
- package/tests/tracke-collisions.ts +255 -0
- package/tests/tracke.ts +314 -0
- package/tests/trackf-bugs.ts +406 -0
- package/tests/trackf.ts +390 -0
- package/tests/trackg.ts +1372 -0
- package/tests/tracki-boundaries.ts +202 -0
- package/tests/tracki-continuity.ts +253 -0
- package/tests/tracki-contract-diff.ts +249 -0
- package/tests/tracki-external-bundles.ts +341 -0
- package/tests/tracki-preflight.ts +251 -0
- package/tests/verify-roles.ts +51 -0
- package/tests/worker-parity.ts +286 -0
- package/tests/worker-pool.ts +262 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { spawn, spawnSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thin async wrapper around `git` so the rest of the indexer doesn't deal
|
|
7
|
+
* with child_process directly. All commands run with `cwd = repoRoot`.
|
|
8
|
+
*
|
|
9
|
+
* Errors are surfaced as `null` returns (not throws) so a non-git workspace
|
|
10
|
+
* silently no-ops. Callers should check the return value.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function isGitRepo(repoRoot: string): boolean {
|
|
14
|
+
try {
|
|
15
|
+
const r = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--is-inside-work-tree'], { encoding: 'utf8' });
|
|
16
|
+
return r.status === 0 && r.stdout.trim() === 'true';
|
|
17
|
+
} catch { return false; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function gitHeadSha(repoRoot: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
const r = spawnSync('git', ['-C', repoRoot, 'rev-parse', 'HEAD'], { encoding: 'utf8' });
|
|
23
|
+
if (r.status !== 0) return null;
|
|
24
|
+
return r.stdout.trim() || null;
|
|
25
|
+
} catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function gitRemoteUrl(repoRoot: string, remote = 'origin'): string | null {
|
|
29
|
+
try {
|
|
30
|
+
const r = spawnSync('git', ['-C', repoRoot, 'config', '--get', `remote.${remote}.url`], { encoding: 'utf8' });
|
|
31
|
+
if (r.status !== 0) return null;
|
|
32
|
+
return r.stdout.trim() || null;
|
|
33
|
+
} catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FileChurnStats {
|
|
37
|
+
commitCount: number;
|
|
38
|
+
lastCommitSha: string | null;
|
|
39
|
+
lastCommitAt: number | null; // unix seconds
|
|
40
|
+
topAuthor: string | null;
|
|
41
|
+
secondAuthor: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collect file-level churn stats by streaming `git log` once and bucketing
|
|
46
|
+
* per file. Uses `--follow` per-file would be ~slow on big repos, so we use
|
|
47
|
+
* a single `git log --name-only` pass and aggregate in JS. The trade-off is
|
|
48
|
+
* that renames lose their pre-rename history; callers documenting that fact
|
|
49
|
+
* matches the master guide's "honest about rename limits" stance.
|
|
50
|
+
*/
|
|
51
|
+
export async function collectFileChurn(
|
|
52
|
+
repoRoot: string,
|
|
53
|
+
filesAbs: Iterable<string>,
|
|
54
|
+
): Promise<Map<string, FileChurnStats>> {
|
|
55
|
+
const result = new Map<string, FileChurnStats>();
|
|
56
|
+
if (!isGitRepo(repoRoot)) return result;
|
|
57
|
+
|
|
58
|
+
// Build a quick lookup: relPath (forward slashes, normalized) → absPath.
|
|
59
|
+
// `git log` reports paths relative to repo root, so we have to translate
|
|
60
|
+
// back to absolute paths the indexer keyed off.
|
|
61
|
+
const absSet = new Set<string>();
|
|
62
|
+
for (const a of filesAbs) absSet.add(normalize(a));
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const proc = spawn(
|
|
66
|
+
'git',
|
|
67
|
+
['-C', repoRoot, 'log', '--name-only', '--pretty=format:__COMMIT__%H%x09%an%x09%aI', '--no-merges'],
|
|
68
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] },
|
|
69
|
+
);
|
|
70
|
+
let buf = '';
|
|
71
|
+
let currentSha: string | null = null;
|
|
72
|
+
let currentAuthor: string | null = null;
|
|
73
|
+
let currentDateSec: number | null = null;
|
|
74
|
+
|
|
75
|
+
const perFile = new Map<string, {
|
|
76
|
+
count: number;
|
|
77
|
+
lastSha: string | null;
|
|
78
|
+
lastAt: number | null;
|
|
79
|
+
authors: Map<string, number>;
|
|
80
|
+
}>();
|
|
81
|
+
|
|
82
|
+
const handleLine = (line: string): void => {
|
|
83
|
+
if (line.startsWith('__COMMIT__')) {
|
|
84
|
+
const parts = line.slice('__COMMIT__'.length).split('\t');
|
|
85
|
+
currentSha = parts[0] || null;
|
|
86
|
+
currentAuthor = parts[1] || null;
|
|
87
|
+
currentDateSec = parts[2] ? Math.floor(Date.parse(parts[2]) / 1000) : null;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (!line.trim()) return;
|
|
91
|
+
const rel = normalize(line);
|
|
92
|
+
// Resolve to absolute. Path may use forward slashes; we compare against
|
|
93
|
+
// absSet directly. Also fall back to repoRoot-joined.
|
|
94
|
+
const cand1 = normalize(path.resolve(repoRoot, rel));
|
|
95
|
+
let key: string | null = null;
|
|
96
|
+
if (absSet.has(cand1)) key = cand1;
|
|
97
|
+
else if (absSet.has(rel)) key = rel;
|
|
98
|
+
if (!key) return;
|
|
99
|
+
let entry = perFile.get(key);
|
|
100
|
+
if (!entry) {
|
|
101
|
+
entry = { count: 0, lastSha: null, lastAt: null, authors: new Map() };
|
|
102
|
+
perFile.set(key, entry);
|
|
103
|
+
}
|
|
104
|
+
entry.count++;
|
|
105
|
+
if (entry.lastSha === null) {
|
|
106
|
+
entry.lastSha = currentSha;
|
|
107
|
+
entry.lastAt = currentDateSec;
|
|
108
|
+
}
|
|
109
|
+
if (currentAuthor) {
|
|
110
|
+
entry.authors.set(currentAuthor, (entry.authors.get(currentAuthor) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
proc.stdout.on('data', (chunk: Buffer) => {
|
|
115
|
+
buf += chunk.toString('utf8');
|
|
116
|
+
let nl: number;
|
|
117
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
118
|
+
const line = buf.slice(0, nl);
|
|
119
|
+
buf = buf.slice(nl + 1);
|
|
120
|
+
handleLine(line);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
proc.stderr.on('data', () => { /* swallow */ });
|
|
124
|
+
proc.on('error', reject);
|
|
125
|
+
proc.on('close', () => {
|
|
126
|
+
if (buf.length > 0) handleLine(buf);
|
|
127
|
+
for (const [key, e] of perFile) {
|
|
128
|
+
const sortedAuthors = Array.from(e.authors.entries()).sort((a, b) => b[1] - a[1]);
|
|
129
|
+
result.set(key, {
|
|
130
|
+
commitCount: e.count,
|
|
131
|
+
lastCommitSha: e.lastSha,
|
|
132
|
+
lastCommitAt: e.lastAt,
|
|
133
|
+
topAuthor: sortedAuthors[0]?.[0] ?? null,
|
|
134
|
+
secondAuthor: sortedAuthors[1]?.[0] ?? null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
resolve(result);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CommitMeta {
|
|
143
|
+
sha: string;
|
|
144
|
+
authorName: string | null;
|
|
145
|
+
authorEmail: string | null;
|
|
146
|
+
committedAt: number; // unix seconds
|
|
147
|
+
message: string;
|
|
148
|
+
/**
|
|
149
|
+
* The path the file had AT THIS COMMIT (forward-slash, repo-relative).
|
|
150
|
+
* Resolved from `git log --follow --name-status` so commits prior to a
|
|
151
|
+
* rename can still be looked up by the historical path. Null if the path
|
|
152
|
+
* couldn't be determined from log output (in which case callers should
|
|
153
|
+
* fall back to the current path).
|
|
154
|
+
*/
|
|
155
|
+
pathAtCommit: string | null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* `git log` commits that touched a single file, newest first. Each commit
|
|
160
|
+
* includes its author info and full message. Used by the symbol-history pass.
|
|
161
|
+
*
|
|
162
|
+
* `--name-status` is added on top of `--follow` so we get a per-commit path —
|
|
163
|
+
* critical for the rename case: the historical commits touched the OLD path,
|
|
164
|
+
* but our usual `git show <sha> -- <currentPath>` would look at the wrong
|
|
165
|
+
* path and return empty hunks. With pathAtCommit threaded through to
|
|
166
|
+
* fileDiffInfo(), pre-rename history is preserved.
|
|
167
|
+
*
|
|
168
|
+
* Parser shape: we ask git for a per-commit header line prefixed `__C__`
|
|
169
|
+
* (sha, author, email, ISO date — all NUL-free fields tab-separated), then
|
|
170
|
+
* the commit body (terminated by a unique end marker `__BODY_END__`), then
|
|
171
|
+
* the name-status lines that git appends after each commit. The name-status
|
|
172
|
+
* block has one path entry per commit relative to this file's --follow chain:
|
|
173
|
+
* either `M\tpath`, `A\tpath`, `D\tpath`, or `R<score>\toldPath\tnewPath`.
|
|
174
|
+
*
|
|
175
|
+
* Using a custom body terminator (instead of git's default blank-line
|
|
176
|
+
* separator) lets us handle commit messages that themselves contain blank
|
|
177
|
+
* lines or `__C__` literals without re-introducing the old `__C__ff8` bug.
|
|
178
|
+
*/
|
|
179
|
+
export async function commitsForFile(
|
|
180
|
+
repoRoot: string,
|
|
181
|
+
filePath: string,
|
|
182
|
+
options: { limit?: number; since?: number } = {},
|
|
183
|
+
): Promise<CommitMeta[]> {
|
|
184
|
+
if (!isGitRepo(repoRoot)) return [];
|
|
185
|
+
const rel = path.relative(repoRoot, filePath);
|
|
186
|
+
const args = ['-C', repoRoot, 'log',
|
|
187
|
+
'--pretty=format:__C__%H%x09%an%x09%ae%x09%aI%n%B%n__BODY_END__',
|
|
188
|
+
'--no-merges',
|
|
189
|
+
'--follow',
|
|
190
|
+
'--name-status',
|
|
191
|
+
];
|
|
192
|
+
if (options.limit) args.push(`-n${options.limit}`);
|
|
193
|
+
if (options.since) args.push(`--since=${new Date(options.since * 1000).toISOString()}`);
|
|
194
|
+
args.push('--', rel);
|
|
195
|
+
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
const proc = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
198
|
+
let buf = '';
|
|
199
|
+
proc.stdout.on('data', (c: Buffer) => { buf += c.toString('utf8'); });
|
|
200
|
+
proc.stderr.on('data', () => { /* */ });
|
|
201
|
+
proc.on('error', () => resolve([]));
|
|
202
|
+
proc.on('close', () => {
|
|
203
|
+
resolve(parseFollowLog(buf));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Walk the line stream from `git log --follow --name-status` (with the
|
|
210
|
+
* `__C__` header / `__BODY_END__` body terminator format above) and emit
|
|
211
|
+
* one CommitMeta per `__C__` header. Exposed only for tests; the live
|
|
212
|
+
* pipeline goes through commitsForFile().
|
|
213
|
+
*/
|
|
214
|
+
export function parseFollowLog(buf: string): CommitMeta[] {
|
|
215
|
+
const out: CommitMeta[] = [];
|
|
216
|
+
// Normalize CRLF that Windows git may inject in some setups so the line
|
|
217
|
+
// walker doesn't end up with trailing \r in messages or paths.
|
|
218
|
+
const lines = buf.replace(/\r\n/g, '\n').split('\n');
|
|
219
|
+
let i = 0;
|
|
220
|
+
while (i < lines.length) {
|
|
221
|
+
const line = lines[i];
|
|
222
|
+
if (!line.startsWith('__C__')) { i++; continue; }
|
|
223
|
+
const headerParts = line.slice('__C__'.length).split('\t');
|
|
224
|
+
if (headerParts.length < 4) { i++; continue; }
|
|
225
|
+
const [sha, author, email, dateStr] = headerParts;
|
|
226
|
+
const committedAt = Math.floor(Date.parse(dateStr) / 1000);
|
|
227
|
+
if (!sha || isNaN(committedAt)) { i++; continue; }
|
|
228
|
+
// Collect message body until __BODY_END__.
|
|
229
|
+
i++;
|
|
230
|
+
const msgLines: string[] = [];
|
|
231
|
+
while (i < lines.length && lines[i] !== '__BODY_END__') {
|
|
232
|
+
msgLines.push(lines[i]);
|
|
233
|
+
i++;
|
|
234
|
+
}
|
|
235
|
+
// Skip the __BODY_END__ line itself.
|
|
236
|
+
if (i < lines.length) i++;
|
|
237
|
+
// Collect name-status lines until the next __C__ header or EOF. Blank
|
|
238
|
+
// lines (which git emits between commit body and name-status) are
|
|
239
|
+
// skipped; we never have to interpret them.
|
|
240
|
+
let pathAtCommit: string | null = null;
|
|
241
|
+
while (i < lines.length && !lines[i].startsWith('__C__')) {
|
|
242
|
+
const nl = lines[i];
|
|
243
|
+
if (nl.length === 0) { i++; continue; }
|
|
244
|
+
const fields = nl.split('\t');
|
|
245
|
+
if (fields.length >= 2) {
|
|
246
|
+
const code = fields[0];
|
|
247
|
+
if (code.startsWith('R') || code.startsWith('C')) {
|
|
248
|
+
// Rename/copy: code, oldPath, newPath. The NEW path is what this
|
|
249
|
+
// commit produced (and the path subsequent commits see).
|
|
250
|
+
if (fields.length >= 3) pathAtCommit = fields[2];
|
|
251
|
+
} else {
|
|
252
|
+
pathAtCommit = fields[1];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
const message = msgLines.join('\n').trimEnd();
|
|
258
|
+
out.push({
|
|
259
|
+
sha,
|
|
260
|
+
authorName: author || null,
|
|
261
|
+
authorEmail: email || null,
|
|
262
|
+
committedAt,
|
|
263
|
+
message,
|
|
264
|
+
pathAtCommit,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface DiffHunk {
|
|
271
|
+
oldStart: number; oldLines: number;
|
|
272
|
+
newStart: number; newLines: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface FileDiffInfo {
|
|
276
|
+
hunks: DiffHunk[];
|
|
277
|
+
/**
|
|
278
|
+
* True when the commit created the file (or — symmetrically — deleted it).
|
|
279
|
+
* In that case the diff has `--- /dev/null` (added) or `+++ /dev/null`
|
|
280
|
+
* (deleted) and EVERY symbol currently in the file should be attributed,
|
|
281
|
+
* because the file's current shape didn't exist before.
|
|
282
|
+
*/
|
|
283
|
+
isFileAddition: boolean;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Diff hunks for a file changed by a single commit. `git show -U0 <sha>`
|
|
288
|
+
* handles the root commit transparently (no parent → diff against the empty
|
|
289
|
+
* tree), so the caller doesn't need to pass parentSha. `parentSha` is kept
|
|
290
|
+
* for compatibility but ignored.
|
|
291
|
+
*/
|
|
292
|
+
export async function fileDiffHunks(
|
|
293
|
+
repoRoot: string, _parentSha: string | null, sha: string, filePath: string,
|
|
294
|
+
): Promise<DiffHunk[]> {
|
|
295
|
+
const info = await fileDiffInfo(repoRoot, sha, filePath);
|
|
296
|
+
return info.hunks;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Like fileDiffHunks but returns extra metadata used for "this commit
|
|
300
|
+
* created the file → match every symbol" heuristic.
|
|
301
|
+
*
|
|
302
|
+
* `filePath` may be either an absolute path or a repo-relative one. When the
|
|
303
|
+
* file was renamed across history, callers should pass the PATH-AT-COMMIT
|
|
304
|
+
* (resolved from `git log --follow --name-status`) so `git show` looks at
|
|
305
|
+
* the right side of the rename — passing the current path would return
|
|
306
|
+
* empty hunks for pre-rename commits and silently lose history.
|
|
307
|
+
*/
|
|
308
|
+
export async function fileDiffInfo(
|
|
309
|
+
repoRoot: string, sha: string, filePath: string,
|
|
310
|
+
): Promise<FileDiffInfo> {
|
|
311
|
+
if (!isGitRepo(repoRoot)) return { hunks: [], isFileAddition: false };
|
|
312
|
+
const rel = path.isAbsolute(filePath) ? path.relative(repoRoot, filePath) : filePath;
|
|
313
|
+
const args = ['-C', repoRoot, 'show', '--format=', '-U0', sha, '--', rel];
|
|
314
|
+
return new Promise(resolve => {
|
|
315
|
+
const proc = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
316
|
+
let buf = '';
|
|
317
|
+
proc.stdout.on('data', (c: Buffer) => { buf += c.toString('utf8'); });
|
|
318
|
+
proc.stderr.on('data', () => { /* */ });
|
|
319
|
+
proc.on('error', () => resolve({ hunks: [], isFileAddition: false }));
|
|
320
|
+
proc.on('close', () => {
|
|
321
|
+
const out: DiffHunk[] = [];
|
|
322
|
+
const re = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
|
|
323
|
+
let m;
|
|
324
|
+
while ((m = re.exec(buf)) !== null) {
|
|
325
|
+
out.push({
|
|
326
|
+
oldStart: parseInt(m[1], 10),
|
|
327
|
+
oldLines: m[2] ? parseInt(m[2], 10) : 1,
|
|
328
|
+
newStart: parseInt(m[3], 10),
|
|
329
|
+
newLines: m[4] ? parseInt(m[4], 10) : 1,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const isFileAddition = /^--- \/dev\/null$/m.test(buf) || /^new file mode/m.test(buf);
|
|
333
|
+
resolve({ hunks: out, isFileAddition });
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Diff numstat for one commit: returns added/removed line counts per file
|
|
340
|
+
* (or aggregated when filePath is given).
|
|
341
|
+
*/
|
|
342
|
+
export async function commitNumstat(
|
|
343
|
+
repoRoot: string, sha: string, filePath?: string,
|
|
344
|
+
): Promise<{ added: number; removed: number }> {
|
|
345
|
+
if (!isGitRepo(repoRoot)) return { added: 0, removed: 0 };
|
|
346
|
+
const args = ['-C', repoRoot, 'show', '--numstat', '--format=', sha];
|
|
347
|
+
if (filePath) args.push('--', path.relative(repoRoot, filePath));
|
|
348
|
+
return new Promise(resolve => {
|
|
349
|
+
const proc = spawn('git', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
350
|
+
let buf = '';
|
|
351
|
+
proc.stdout.on('data', (c: Buffer) => { buf += c.toString('utf8'); });
|
|
352
|
+
proc.on('error', () => resolve({ added: 0, removed: 0 }));
|
|
353
|
+
proc.on('close', () => {
|
|
354
|
+
let added = 0, removed = 0;
|
|
355
|
+
for (const line of buf.split('\n')) {
|
|
356
|
+
const parts = line.trim().split(/\s+/);
|
|
357
|
+
if (parts.length < 3) continue;
|
|
358
|
+
const a = parseInt(parts[0], 10);
|
|
359
|
+
const r = parseInt(parts[1], 10);
|
|
360
|
+
if (!isNaN(a)) added += a;
|
|
361
|
+
if (!isNaN(r)) removed += r;
|
|
362
|
+
}
|
|
363
|
+
resolve({ added, removed });
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* git diff name-only between two refs. Defaults to HEAD vs the working
|
|
370
|
+
* tree (uncommitted changes) — used by `detect_changes`. Returns absolute
|
|
371
|
+
* paths (after path.resolve(repoRoot, rel)).
|
|
372
|
+
*/
|
|
373
|
+
export function gitChangedFiles(repoRoot: string, fromRef?: string, toRef?: string): string[] {
|
|
374
|
+
if (!isGitRepo(repoRoot)) return [];
|
|
375
|
+
const args = ['-C', repoRoot, 'diff', '--name-only'];
|
|
376
|
+
if (fromRef && toRef) args.push(`${fromRef}..${toRef}`);
|
|
377
|
+
else if (fromRef) args.push(fromRef);
|
|
378
|
+
// No refs → working-tree diff against HEAD.
|
|
379
|
+
const r = spawnSync('git', args, { encoding: 'utf8' });
|
|
380
|
+
if (r.status !== 0) return [];
|
|
381
|
+
return r.stdout.split('\n').filter(Boolean).map(rel => path.resolve(repoRoot, rel));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* git diff -U0 hunks for one file between two refs (or working tree if refs
|
|
386
|
+
* omitted). Returns parsed hunk headers (line ranges in the new file) so
|
|
387
|
+
* `detect_changes` can compute which symbols overlap.
|
|
388
|
+
*/
|
|
389
|
+
export function fileDiffHunksSync(
|
|
390
|
+
repoRoot: string, filePath: string, fromRef?: string, toRef?: string,
|
|
391
|
+
): DiffHunk[] {
|
|
392
|
+
if (!isGitRepo(repoRoot)) return [];
|
|
393
|
+
if (!fs.existsSync(filePath)) return [];
|
|
394
|
+
const rel = path.relative(repoRoot, filePath);
|
|
395
|
+
const args = ['-C', repoRoot, 'diff', '-U0'];
|
|
396
|
+
if (fromRef && toRef) args.push(`${fromRef}..${toRef}`);
|
|
397
|
+
else if (fromRef) args.push(fromRef);
|
|
398
|
+
args.push('--', rel);
|
|
399
|
+
const r = spawnSync('git', args, { encoding: 'utf8' });
|
|
400
|
+
if (r.status !== 0) return [];
|
|
401
|
+
const out: DiffHunk[] = [];
|
|
402
|
+
const re = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/gm;
|
|
403
|
+
let m;
|
|
404
|
+
while ((m = re.exec(r.stdout)) !== null) {
|
|
405
|
+
out.push({
|
|
406
|
+
oldStart: parseInt(m[1], 10),
|
|
407
|
+
oldLines: m[2] ? parseInt(m[2], 10) : 1,
|
|
408
|
+
newStart: parseInt(m[3], 10),
|
|
409
|
+
newLines: m[4] ? parseInt(m[4], 10) : 1,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return out;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function normalize(p: string): string {
|
|
416
|
+
const n = p.replace(/\\/g, '/');
|
|
417
|
+
return process.platform === 'win32' ? n.toLowerCase() : n;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Extract GitHub-style PR numbers from a commit message. We accept the
|
|
422
|
+
* common conventions:
|
|
423
|
+
* "Merge pull request #1234 from ..."
|
|
424
|
+
* "Fix something (#1234)"
|
|
425
|
+
* "#1234"
|
|
426
|
+
* "PR #1234"
|
|
427
|
+
*
|
|
428
|
+
* Returns the first matched PR number, or null.
|
|
429
|
+
*/
|
|
430
|
+
export function extractPrNumber(message: string): number | null {
|
|
431
|
+
if (!message) return null;
|
|
432
|
+
const merge = message.match(/Merge pull request #(\d+)/i);
|
|
433
|
+
if (merge) return parseInt(merge[1], 10);
|
|
434
|
+
// Trailing or inline `#1234` but not part of a hex-ish word
|
|
435
|
+
const m = message.match(/(?:^|[\s(])#(\d{1,7})\b/);
|
|
436
|
+
if (m) return parseInt(m[1], 10);
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* If `remoteUrl` is a GitHub URL (HTTPS or SSH), return the matching
|
|
442
|
+
* `https://github.com/<owner>/<repo>/pull/<n>` URL for a PR number.
|
|
443
|
+
* Returns null for non-GitHub remotes so we don't fabricate links to
|
|
444
|
+
* GitLab/Bitbucket/etc.
|
|
445
|
+
*/
|
|
446
|
+
export function githubPrUrl(remoteUrl: string | null, prNumber: number): string | null {
|
|
447
|
+
if (!remoteUrl || !prNumber) return null;
|
|
448
|
+
// git@github.com:owner/repo.git
|
|
449
|
+
let m = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
450
|
+
if (!m) m = remoteUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?\/?$/);
|
|
451
|
+
if (!m) return null;
|
|
452
|
+
return `https://github.com/${m[1]}/${m[2]}/pull/${prNumber}`;
|
|
453
|
+
}
|