repo-dive 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +85 -52
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,8 +68,8 @@ export default defineConfig({
|
|
|
68
68
|
["alice@work.example", "alice@personal.example"],
|
|
69
69
|
// Rich form: a display name, a profile link and an explicit kind.
|
|
70
70
|
{
|
|
71
|
-
emails: ["bob@work.example", "12345+bob@users.noreply.github.com"],
|
|
72
71
|
displayName: "Bob",
|
|
72
|
+
emails: ["bob@work.example", "12345+bob@users.noreply.github.com"],
|
|
73
73
|
url: "https://github.com/bob",
|
|
74
74
|
},
|
|
75
75
|
],
|
package/dist/cli.js
CHANGED
|
@@ -52170,7 +52170,7 @@ var runWith = (command, config) => {
|
|
|
52170
52170
|
};
|
|
52171
52171
|
var package_default = {
|
|
52172
52172
|
name: "repo-dive",
|
|
52173
|
-
version: "0.4.
|
|
52173
|
+
version: "0.4.1",
|
|
52174
52174
|
description: "Dive into a git repository's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
|
|
52175
52175
|
keywords: [
|
|
52176
52176
|
"git",
|
|
@@ -52364,6 +52364,15 @@ var recordAt = (value, key) => asRecord$1(asRecord$1(value)?.[key]) ?? {};
|
|
|
52364
52364
|
var arrayAt = (value, key) => asArray(asRecord$1(value)?.[key]);
|
|
52365
52365
|
//#endregion
|
|
52366
52366
|
//#region src/lib/collectors/types.ts
|
|
52367
|
+
/**
|
|
52368
|
+
* Whether a collector describes the *state of the tree* at a commit rather than
|
|
52369
|
+
* facts about the commit itself. Such snapshots are only meaningful on the
|
|
52370
|
+
* first-parent chain: a commit that lives on a side branch — or that arrived
|
|
52371
|
+
* with a foreign history absorbed by an unrelated-histories merge — carries a
|
|
52372
|
+
* tree that was never the repository's state, so charting it puts a cliff into
|
|
52373
|
+
* every timeline.
|
|
52374
|
+
*/
|
|
52375
|
+
var describesTreeState = (collector) => collector.strategy !== "log";
|
|
52367
52376
|
/** File extension used as a category key, e.g. ".ts"; files without one map to "(none)". */
|
|
52368
52377
|
var extensionOf = (filePath) => {
|
|
52369
52378
|
const basename = filePath.split("/").at(-1) ?? "";
|
|
@@ -53546,12 +53555,57 @@ var resolveCollectors = (names) => {
|
|
|
53546
53555
|
return resolved;
|
|
53547
53556
|
};
|
|
53548
53557
|
//#endregion
|
|
53558
|
+
//#region src/lib/sampling.ts
|
|
53559
|
+
var parseSamplingPolicy = (input) => {
|
|
53560
|
+
if (input === "all" || input === "weekly" || input === "monthly" || input === "quarterly") return input;
|
|
53561
|
+
const everyNthMatch = /^every-nth:(\d+)$/.exec(input);
|
|
53562
|
+
if (everyNthMatch?.[1]) {
|
|
53563
|
+
const everyNth = Number(everyNthMatch[1]);
|
|
53564
|
+
if (everyNth >= 1) return { everyNth };
|
|
53565
|
+
}
|
|
53566
|
+
return /* @__PURE__ */ new Error(`Unknown sampling policy: ${input}. Expected all, weekly, monthly, quarterly or every-nth:<n>.`);
|
|
53567
|
+
};
|
|
53568
|
+
/** How a policy is spelled in CLI output (`collectors`, `scan`, `status`). */
|
|
53569
|
+
var samplingLabel = (policy) => typeof policy === "object" ? `every-nth:${policy.everyNth}` : policy;
|
|
53570
|
+
var isoWeekOf = (date) => {
|
|
53571
|
+
const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
53572
|
+
const dayOfWeek = utc.getUTCDay() || 7;
|
|
53573
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - dayOfWeek);
|
|
53574
|
+
const yearStart = Date.UTC(utc.getUTCFullYear(), 0, 1);
|
|
53575
|
+
const week = Math.ceil(((utc.getTime() - yearStart) / 864e5 + 1) / 7);
|
|
53576
|
+
return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
53577
|
+
};
|
|
53578
|
+
var bucketOf = (policy, authorDate) => {
|
|
53579
|
+
const date = new Date(authorDate);
|
|
53580
|
+
if (policy === "weekly") return isoWeekOf(date);
|
|
53581
|
+
const month = date.getUTCMonth();
|
|
53582
|
+
return policy === "monthly" ? `${date.getUTCFullYear()}-${String(month + 1).padStart(2, "0")}` : `${date.getUTCFullYear()}-Q${Math.floor(month / 3) + 1}`;
|
|
53583
|
+
};
|
|
53584
|
+
/**
|
|
53585
|
+
* Picks the sampled subset of commits for a policy. `commits` must be ordered
|
|
53586
|
+
* newest first (as `git log` emits them); period policies keep the newest
|
|
53587
|
+
* commit of each period, so HEAD is always included.
|
|
53588
|
+
*/
|
|
53589
|
+
var sampleCommits = (commits, policy) => {
|
|
53590
|
+
if (policy === "all") return [...commits];
|
|
53591
|
+
if (typeof policy === "object") return commits.filter((_, index) => index % policy.everyNth === 0);
|
|
53592
|
+
const seenBuckets = /* @__PURE__ */ new Set();
|
|
53593
|
+
const sampled = [];
|
|
53594
|
+
for (const commit of commits) {
|
|
53595
|
+
const bucket = bucketOf(policy, commit.authorDate);
|
|
53596
|
+
if (!seenBuckets.has(bucket)) {
|
|
53597
|
+
seenBuckets.add(bucket);
|
|
53598
|
+
sampled.push(commit);
|
|
53599
|
+
}
|
|
53600
|
+
}
|
|
53601
|
+
return sampled;
|
|
53602
|
+
};
|
|
53603
|
+
//#endregion
|
|
53549
53604
|
//#region src/commands/collectors.ts
|
|
53550
|
-
var samplingLabel$1 = (policy) => typeof policy === "object" ? `every-nth:${policy.everyNth}` : policy;
|
|
53551
53605
|
var collectorsCommand = make$3("collectors").pipe(withDescription("List available collectors, their versions, strategies and default sampling"), withHandler(() => gen(function* () {
|
|
53552
53606
|
for (const collector of builtInCollectors) yield* log([
|
|
53553
53607
|
`${collector.name} (v${collector.version})`,
|
|
53554
|
-
` strategy: ${collector.strategy}, sampling: ${samplingLabel
|
|
53608
|
+
` strategy: ${collector.strategy}, sampling: ${samplingLabel(collector.defaultSampling)}`,
|
|
53555
53609
|
` ${collector.description}`
|
|
53556
53610
|
].join("\n"));
|
|
53557
53611
|
})));
|
|
@@ -53817,6 +53871,7 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
53817
53871
|
const commitsPath = path.join(catalogPath, "commits");
|
|
53818
53872
|
const registry = new Map(builtInCollectors.map((collector) => [collector.name, collector]));
|
|
53819
53873
|
const gitCommits = yield* listCommits(repoRoot);
|
|
53874
|
+
const firstParentShas = yield* listFirstParentShas(repoRoot);
|
|
53820
53875
|
const catalogShas = new Set(yield* tryPromise({
|
|
53821
53876
|
try: async () => {
|
|
53822
53877
|
try {
|
|
@@ -53830,10 +53885,12 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
53830
53885
|
const orderedCommits = gitCommits.toReversed().filter((commit) => catalogShas.has(commit.hash));
|
|
53831
53886
|
if (orderedCommits.length === 0) return yield* fail$3(/* @__PURE__ */ new Error(`No collected commits found in ${commitsPath} — run \`repo-dive scan\` first.`));
|
|
53832
53887
|
let unknownCollectorDirs = 0;
|
|
53888
|
+
let offMainlineSnapshots = 0;
|
|
53833
53889
|
const commitFacts = [];
|
|
53834
53890
|
yield* forEach$1(orderedCommits, (commit) => tryPromise({
|
|
53835
53891
|
try: async () => {
|
|
53836
53892
|
const commitDir = path.join(commitsPath, commit.hash);
|
|
53893
|
+
const onMainline = firstParentShas.has(commit.hash);
|
|
53837
53894
|
const factsByCollector = /* @__PURE__ */ new Map();
|
|
53838
53895
|
for (const collectorName of await readdir(commitDir)) {
|
|
53839
53896
|
const collector = registry.get(collectorName);
|
|
@@ -53841,6 +53898,10 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
53841
53898
|
unknownCollectorDirs += 1;
|
|
53842
53899
|
continue;
|
|
53843
53900
|
}
|
|
53901
|
+
if (!onMainline && describesTreeState(collector)) {
|
|
53902
|
+
offMainlineSnapshots += 1;
|
|
53903
|
+
continue;
|
|
53904
|
+
}
|
|
53844
53905
|
const raw = JSON.parse(await readFile(path.join(commitDir, collectorName, "output.json"), "utf8"));
|
|
53845
53906
|
factsByCollector.set(collectorName, collector.normalize(raw));
|
|
53846
53907
|
}
|
|
@@ -53882,7 +53943,8 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
53882
53943
|
`Indexed ${commitFacts.length} commits into ${factCount} facts.`,
|
|
53883
53944
|
`Cube: ${dbPath}`,
|
|
53884
53945
|
`Dashboard data: ${dashboardPath}`,
|
|
53885
|
-
...unknownCollectorDirs > 0 ? [`Skipped ${unknownCollectorDirs} outputs from unknown collectors (see \`gc --stale\`).`] : []
|
|
53946
|
+
...unknownCollectorDirs > 0 ? [`Skipped ${unknownCollectorDirs} outputs from unknown collectors (see \`gc --stale\`).`] : [],
|
|
53947
|
+
...offMainlineSnapshots > 0 ? [`Skipped ${offMainlineSnapshots} tree snapshots taken off HEAD's first-parent chain.`] : []
|
|
53886
53948
|
].join("\n"));
|
|
53887
53949
|
});
|
|
53888
53950
|
//#endregion
|
|
@@ -54035,50 +54097,6 @@ var loadConfig = (repoRoot) => gen(function* () {
|
|
|
54035
54097
|
});
|
|
54036
54098
|
});
|
|
54037
54099
|
//#endregion
|
|
54038
|
-
//#region src/lib/sampling.ts
|
|
54039
|
-
var parseSamplingPolicy = (input) => {
|
|
54040
|
-
if (input === "all" || input === "weekly" || input === "monthly" || input === "quarterly") return input;
|
|
54041
|
-
const everyNthMatch = /^every-nth:(\d+)$/.exec(input);
|
|
54042
|
-
if (everyNthMatch?.[1]) {
|
|
54043
|
-
const everyNth = Number(everyNthMatch[1]);
|
|
54044
|
-
if (everyNth >= 1) return { everyNth };
|
|
54045
|
-
}
|
|
54046
|
-
return /* @__PURE__ */ new Error(`Unknown sampling policy: ${input}. Expected all, weekly, monthly, quarterly or every-nth:<n>.`);
|
|
54047
|
-
};
|
|
54048
|
-
var isoWeekOf = (date) => {
|
|
54049
|
-
const utc = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
|
54050
|
-
const dayOfWeek = utc.getUTCDay() || 7;
|
|
54051
|
-
utc.setUTCDate(utc.getUTCDate() + 4 - dayOfWeek);
|
|
54052
|
-
const yearStart = Date.UTC(utc.getUTCFullYear(), 0, 1);
|
|
54053
|
-
const week = Math.ceil(((utc.getTime() - yearStart) / 864e5 + 1) / 7);
|
|
54054
|
-
return `${utc.getUTCFullYear()}-W${String(week).padStart(2, "0")}`;
|
|
54055
|
-
};
|
|
54056
|
-
var bucketOf = (policy, authorDate) => {
|
|
54057
|
-
const date = new Date(authorDate);
|
|
54058
|
-
if (policy === "weekly") return isoWeekOf(date);
|
|
54059
|
-
const month = date.getUTCMonth();
|
|
54060
|
-
return policy === "monthly" ? `${date.getUTCFullYear()}-${String(month + 1).padStart(2, "0")}` : `${date.getUTCFullYear()}-Q${Math.floor(month / 3) + 1}`;
|
|
54061
|
-
};
|
|
54062
|
-
/**
|
|
54063
|
-
* Picks the sampled subset of commits for a policy. `commits` must be ordered
|
|
54064
|
-
* newest first (as `git log` emits them); period policies keep the newest
|
|
54065
|
-
* commit of each period, so HEAD is always included.
|
|
54066
|
-
*/
|
|
54067
|
-
var sampleCommits = (commits, policy) => {
|
|
54068
|
-
if (policy === "all") return [...commits];
|
|
54069
|
-
if (typeof policy === "object") return commits.filter((_, index) => index % policy.everyNth === 0);
|
|
54070
|
-
const seenBuckets = /* @__PURE__ */ new Set();
|
|
54071
|
-
const sampled = [];
|
|
54072
|
-
for (const commit of commits) {
|
|
54073
|
-
const bucket = bucketOf(policy, commit.authorDate);
|
|
54074
|
-
if (!seenBuckets.has(bucket)) {
|
|
54075
|
-
seenBuckets.add(bucket);
|
|
54076
|
-
sampled.push(commit);
|
|
54077
|
-
}
|
|
54078
|
-
}
|
|
54079
|
-
return sampled;
|
|
54080
|
-
};
|
|
54081
|
-
//#endregion
|
|
54082
54100
|
//#region src/lib/worktree.ts
|
|
54083
54101
|
var toError$4 = (error) => error instanceof Error ? error : new Error(String(error));
|
|
54084
54102
|
/**
|
|
@@ -54157,6 +54175,18 @@ var listCommits = (repoRoot) => runGit([
|
|
|
54157
54175
|
"log",
|
|
54158
54176
|
`--format=${gitLogFormat}`
|
|
54159
54177
|
]).pipe(catch_$2((error) => error instanceof GitCommandError && error.stderr.includes("does not have any commits yet") ? succeed$3("") : fail$3(error)), map$7(parseGitLog));
|
|
54178
|
+
/**
|
|
54179
|
+
* Shas on HEAD's first-parent chain, i.e. the states the repository actually
|
|
54180
|
+
* passed through. See {@link describesTreeState} for why snapshot collectors
|
|
54181
|
+
* are restricted to them.
|
|
54182
|
+
*/
|
|
54183
|
+
var listFirstParentShas = (repoRoot) => runGit([
|
|
54184
|
+
"-C",
|
|
54185
|
+
repoRoot,
|
|
54186
|
+
"log",
|
|
54187
|
+
"--first-parent",
|
|
54188
|
+
"--format=%H"
|
|
54189
|
+
]).pipe(catch_$2((error) => error instanceof GitCommandError && error.stderr.includes("does not have any commits yet") ? succeed$3("") : fail$3(error)), map$7((stdout) => new Set(stdout.split("\n").filter(Boolean))));
|
|
54160
54190
|
var summarizeCommits = (commits) => {
|
|
54161
54191
|
const authorEmails = new Set(commits.map((commit) => commit.authorEmail));
|
|
54162
54192
|
const dates = commits.map((commit) => commit.authorDate).filter(Boolean).toSorted();
|
|
@@ -54220,7 +54250,6 @@ var collectCommit = ({ catalog, sha, collectors, cacheKeyOf, force, failures })
|
|
|
54220
54250
|
skipped
|
|
54221
54251
|
};
|
|
54222
54252
|
});
|
|
54223
|
-
var samplingLabel = (policy) => typeof policy === "object" ? `every-nth:${policy.everyNth}` : policy;
|
|
54224
54253
|
var runScan = ({ repoPath, collectorNames, maxCommits, sample, force = false }) => gen(function* () {
|
|
54225
54254
|
const collectors = resolveCollectors(collectorNames);
|
|
54226
54255
|
if (collectors instanceof Error) return yield* fail$3(collectors);
|
|
@@ -54238,12 +54267,14 @@ var runScan = ({ repoPath, collectorNames, maxCommits, sample, force = false })
|
|
|
54238
54267
|
const config = yield* loadConfig(repoRoot);
|
|
54239
54268
|
const cacheKeys = new Map(collectors.map((collector) => [collector.name, collectorCacheKey(collector, config)]));
|
|
54240
54269
|
const cacheKeyOf = (collector) => cacheKeys.get(collector.name) ?? collectorCacheKey(collector, config);
|
|
54270
|
+
const firstParentShas = yield* listFirstParentShas(repoRoot);
|
|
54241
54271
|
const plans = collectors.map((collector) => {
|
|
54242
54272
|
const policy = sampleOverride ?? collector.defaultSampling;
|
|
54273
|
+
const candidates = describesTreeState(collector) ? selected.filter((commit) => firstParentShas.has(commit.hash)) : selected;
|
|
54243
54274
|
return {
|
|
54244
54275
|
collector,
|
|
54245
54276
|
policy,
|
|
54246
|
-
shas: new Set(sampleCommits(
|
|
54277
|
+
shas: new Set(sampleCommits(candidates, policy).map((commit) => commit.hash))
|
|
54247
54278
|
};
|
|
54248
54279
|
});
|
|
54249
54280
|
yield* log(`Plan: ${plans.map((plan) => `${plan.collector.name} → ${plan.shas.size} commits (${samplingLabel(plan.policy)})`).join(", ")}`);
|
|
@@ -59199,6 +59230,7 @@ var exists = (filePath) => promise(() => access(filePath).then(() => true, () =>
|
|
|
59199
59230
|
var runStatus = ({ repoPath }) => gen(function* () {
|
|
59200
59231
|
const repoRoot = yield* resolveRepoRoot(repoPath);
|
|
59201
59232
|
const commits = yield* listCommits(repoRoot);
|
|
59233
|
+
const firstParentShas = yield* listFirstParentShas(repoRoot);
|
|
59202
59234
|
const config = yield* loadConfig(repoRoot);
|
|
59203
59235
|
const catalogPath = path.join(repoRoot, catalogDirName);
|
|
59204
59236
|
if (!(yield* exists(path.join(catalogPath, "catalog.json")))) {
|
|
@@ -59220,15 +59252,16 @@ var runStatus = ({ repoPath }) => gen(function* () {
|
|
|
59220
59252
|
`Catalog: ${catalogPath}`
|
|
59221
59253
|
];
|
|
59222
59254
|
for (const collector of builtInCollectors) {
|
|
59255
|
+
const target = sampleCommits(describesTreeState(collector) ? commits.filter((commit) => firstParentShas.has(commit.hash)) : commits, collector.defaultSampling);
|
|
59223
59256
|
let collected = 0;
|
|
59224
59257
|
const cacheKey = collectorCacheKey(collector, config);
|
|
59225
|
-
yield* forEach$1(
|
|
59258
|
+
yield* forEach$1(target, (commit) => isCollected(catalog, commit.hash, collector, cacheKey).pipe(map$7((done) => {
|
|
59226
59259
|
if (done) collected += 1;
|
|
59227
59260
|
})), {
|
|
59228
59261
|
concurrency: 16,
|
|
59229
59262
|
discard: true
|
|
59230
59263
|
});
|
|
59231
|
-
lines.push(` ${collector.name}: ${collected}/${commits.length} commits collected`);
|
|
59264
|
+
lines.push(collector.defaultSampling === "all" ? ` ${collector.name}: ${collected}/${target.length} commits collected` : ` ${collector.name}: ${collected}/${target.length} commits collected (${samplingLabel(collector.defaultSampling)} sample of ${commits.length})`);
|
|
59232
59265
|
}
|
|
59233
59266
|
yield* log(lines.join("\n"));
|
|
59234
59267
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* ["alice@work.example", "alice@personal.example"],
|
|
16
16
|
* // Rich form: a display name, a profile link and a kind.
|
|
17
17
|
* {
|
|
18
|
-
* emails: ["bob@work.example", "bob@personal.example"],
|
|
19
18
|
* displayName: "Bob",
|
|
19
|
+
* emails: ["bob@work.example", "bob@personal.example"],
|
|
20
20
|
* url: "https://github.com/bob",
|
|
21
21
|
* },
|
|
22
22
|
* ],
|
package/dist/config.js
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* ["alice@work.example", "alice@personal.example"],
|
|
16
16
|
* // Rich form: a display name, a profile link and a kind.
|
|
17
17
|
* {
|
|
18
|
-
* emails: ["bob@work.example", "bob@personal.example"],
|
|
19
18
|
* displayName: "Bob",
|
|
19
|
+
* emails: ["bob@work.example", "bob@personal.example"],
|
|
20
20
|
* url: "https://github.com/bob",
|
|
21
21
|
* },
|
|
22
22
|
* ],
|