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 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.0",
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$1(collector.defaultSampling)}`,
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(selected, policy).map((commit) => commit.hash))
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(commits, (commit) => isCollected(catalog, commit.hash, collector, cacheKey).pipe(map$7((done) => {
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
  * ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repo-dive",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Dive into a git repository's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
5
5
  "keywords": [
6
6
  "git",