repo-dive 0.18.0 → 0.18.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/dist/cli.js CHANGED
@@ -44237,7 +44237,7 @@ var runWith = (command, config) => {
44237
44237
  };
44238
44238
  var package_default = {
44239
44239
  name: "repo-dive",
44240
- version: "0.18.0",
44240
+ version: "0.18.1",
44241
44241
  description: "Dive into a git repo's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
44242
44242
  keywords: [
44243
44243
  "git",
@@ -44395,7 +44395,7 @@ var captureStream = (stream) => stream.pipe(decodeText$1(), mkString);
44395
44395
  * the invocation explicit.
44396
44396
  */
44397
44397
  var runCommand = fnUntraced(function* (command, args, options) {
44398
- const handle = yield* make$33(command, [...args], { stdin: "ignore" });
44398
+ const handle = yield* make$33(command, [...args], { stdin: options?.input === void 0 ? "ignore" : make$38(new TextEncoder().encode(options.input)) });
44399
44399
  const { stdout, stderr, exitCode } = yield* all({
44400
44400
  stdout: captureStream(handle.stdout),
44401
44401
  stderr: captureStream(handle.stderr),
@@ -53596,6 +53596,135 @@ var inferAliases = (resolveContributor, authors) => {
53596
53596
  return (email, name) => resolveContributor(anchorEmailByKey.get(keyOf(email)) ?? email, name);
53597
53597
  };
53598
53598
  //#endregion
53599
+ //#region src/cli/shared/indexing/parse-identity.ts
53600
+ /**
53601
+ * Splits a `Co-authored-by:` trailer into its parts:
53602
+ * `"Claude Fable 5 <noreply@anthropic.com>"` → name + email.
53603
+ *
53604
+ * Keeping the email (rather than the display name alone) is what lets a
53605
+ * co-author resolve through the very same `resolveContributor` an author goes
53606
+ * through — so alias groups, `displayName`/`url` overrides and kind derivation
53607
+ * apply identically whether a person wrote a commit or helped with one.
53608
+ */
53609
+ var parseIdentity = (identity) => {
53610
+ const open = identity.indexOf("<");
53611
+ const close = identity.lastIndexOf(">");
53612
+ if (open === -1 || close < open) return {
53613
+ name: identity.trim(),
53614
+ email: ""
53615
+ };
53616
+ return {
53617
+ name: identity.slice(0, open).trim(),
53618
+ email: identity.slice(open + 1, close).trim()
53619
+ };
53620
+ };
53621
+ //#endregion
53622
+ //#region src/cli/shared/indexing/read-mailmap.ts
53623
+ var CheckMailmapOutputError = class extends TaggedError$1("CheckMailmapOutputError") {
53624
+ get message() {
53625
+ return `git check-mailmap answered ${this.contactCount} contacts with ${this.lineCount} lines`;
53626
+ }
53627
+ };
53628
+ /**
53629
+ * The line `git check-mailmap` looks an identity up by: `Name <email>`, or
53630
+ * `<email>` alone, which matches the `.mailmap` entries that don't name the
53631
+ * old identity. `undefined` for what it can't look up: no email (a bare
53632
+ * `Co-authored-by: Some Name`), an email with a `>` in it, which git cuts
53633
+ * short there, or a line break, which would split one contact in two.
53634
+ */
53635
+ var contactOf = (name, email) => {
53636
+ if (email === "" || /[>\n]/.test(email) || name.includes("\n")) return;
53637
+ return name === "" ? `<${email}>` : `${name} <${email}>`;
53638
+ };
53639
+ /**
53640
+ * Looks up the identities `facts` name in the repository's current `.mailmap`
53641
+ * and returns a function that rewrites a fact through it, or `undefined` when
53642
+ * the `.mailmap` changes none of them.
53643
+ *
53644
+ * Collectors record authors under `author` as a bare email and co-authors
53645
+ * under `coAuthor` as the trailer's `Name <email>`, which is looked up by name
53646
+ * too, the way `git shortlog --group=trailer:co-authored-by` does. Neither is
53647
+ * mapped when collected — file survival reads the raw `%ae`, trailers are
53648
+ * kept verbatim — except line survival, which `git blame` maps through the
53649
+ * `.mailmap` of the day. Mapping them all again here, through the `.mailmap`
53650
+ * that `listCommits` reads commit authors through, brings entries added since
53651
+ * a snapshot was collected to every chart without a re-scan.
53652
+ *
53653
+ * Only additions reach back that way: line survival collected under an entry
53654
+ * keeps the address `git blame` mapped it to, so an entry removed or changed
53655
+ * since takes a re-scan to undo there. That address is looked up once more as
53656
+ * well, which only moves it if the `.mailmap` maps it in turn — a chain git
53657
+ * itself never follows.
53658
+ */
53659
+ var readMailmap = fn("readMailmap")(function* (repoRoot, facts) {
53660
+ const authors = /* @__PURE__ */ new Set();
53661
+ const coAuthors = /* @__PURE__ */ new Set();
53662
+ for (const fact of facts) {
53663
+ const author = fact.categories?.["author"];
53664
+ const coAuthor = fact.categories?.["coAuthor"];
53665
+ if (author !== void 0) authors.add(author);
53666
+ else if (coAuthor !== void 0) coAuthors.add(coAuthor);
53667
+ }
53668
+ const lookups = [];
53669
+ for (const value of authors) {
53670
+ const contact = contactOf("", value);
53671
+ if (contact !== void 0) lookups.push({
53672
+ key: "author",
53673
+ value,
53674
+ contact
53675
+ });
53676
+ }
53677
+ for (const value of coAuthors) {
53678
+ const { name, email } = parseIdentity(value);
53679
+ const contact = contactOf(name, email);
53680
+ if (contact !== void 0) lookups.push({
53681
+ key: "coAuthor",
53682
+ value,
53683
+ contact
53684
+ });
53685
+ }
53686
+ if (lookups.length === 0) return;
53687
+ const answers = (yield* runGit([
53688
+ "-C",
53689
+ repoRoot,
53690
+ "check-mailmap",
53691
+ "--stdin"
53692
+ ], { input: lookups.map(({ contact }) => `${contact}\n`).join("") })).split("\n");
53693
+ if (answers.length !== lookups.length + 1) return yield* new CheckMailmapOutputError({
53694
+ contactCount: lookups.length,
53695
+ lineCount: answers.length - 1
53696
+ });
53697
+ /** Category value → what replaces it, for the values the `.mailmap` changes. */
53698
+ const replacements = {
53699
+ author: /* @__PURE__ */ new Map(),
53700
+ coAuthor: /* @__PURE__ */ new Map()
53701
+ };
53702
+ for (const [index, { key, value, contact }] of lookups.entries()) {
53703
+ const answer = answers[index] ?? contact;
53704
+ if (key === "coAuthor") {
53705
+ if (answer !== contact) replacements.coAuthor.set(value, answer);
53706
+ continue;
53707
+ }
53708
+ const { email } = parseIdentity(answer);
53709
+ if (email !== value) replacements.author.set(value, email);
53710
+ }
53711
+ if (replacements.author.size === 0 && replacements.coAuthor.size === 0) return;
53712
+ return (fact) => {
53713
+ for (const key of ["author", "coAuthor"]) {
53714
+ const value = fact.categories?.[key];
53715
+ const replacement = value === void 0 ? void 0 : replacements[key].get(value);
53716
+ if (replacement !== void 0) return {
53717
+ ...fact,
53718
+ categories: {
53719
+ ...fact.categories,
53720
+ [key]: replacement
53721
+ }
53722
+ };
53723
+ }
53724
+ return fact;
53725
+ };
53726
+ });
53727
+ //#endregion
53599
53728
  //#region src/cli/shared/indexing/remote.ts
53600
53729
  /**
53601
53730
  * Turns whatever `remote.origin.url` holds into a URL a browser can open, or
@@ -53653,27 +53782,6 @@ var kindGroupLabels = {
53653
53782
  bot: "Bots",
53654
53783
  ai: "AI agents"
53655
53784
  };
53656
- /**
53657
- * Splits a `Co-authored-by:` trailer into its parts:
53658
- * `"Claude Fable 5 <noreply@anthropic.com>"` → name + email.
53659
- *
53660
- * Keeping the email (rather than the display name alone) is what lets a
53661
- * co-author resolve through the very same `resolveContributor` an author goes
53662
- * through — so alias groups, `displayName`/`url` overrides and kind derivation
53663
- * apply identically whether a person wrote a commit or helped with one.
53664
- */
53665
- var parseIdentity = (identity) => {
53666
- const open = identity.indexOf("<");
53667
- const close = identity.lastIndexOf(">");
53668
- if (open === -1 || close < open) return {
53669
- name: identity.trim(),
53670
- email: ""
53671
- };
53672
- return {
53673
- name: identity.slice(0, open).trim(),
53674
- email: identity.slice(open + 1, close).trim()
53675
- };
53676
- };
53677
53785
  var sumMetric = (commit, metric, filter) => {
53678
53786
  let total = 0;
53679
53787
  for (const facts of commit.factsByCollector.values()) for (const fact of facts) if (fact.metric === metric && (filter === void 0 || filter(fact.categories ?? {}))) total += fact.value;
@@ -53878,8 +53986,8 @@ var buildDashboardData = (repoRoot, commits, config, remoteUrl, lineageOf) => {
53878
53986
  * config merges them back into one.
53879
53987
  */
53880
53988
  const contributorFor = (resolved, observedName) => {
53881
- const name = nameOf(resolved, observedName);
53882
53989
  const email = resolved.canonicalEmail.toLowerCase();
53990
+ const name = (resolved.kind === "human" ? nameByCanonicalEmail.get(email) : void 0) ?? nameOf(resolved, observedName);
53883
53991
  const key = resolved.kind === "human" ? email : `${name.toLowerCase()}\u{1F}${email}`;
53884
53992
  const contributor = contributorMap.get(key) ?? {
53885
53993
  index: contributorMap.size,
@@ -54052,7 +54160,11 @@ var runIndex = fn("runIndex")(function* ({ repoPath }) {
54052
54160
  incompleteOutputs
54053
54161
  };
54054
54162
  }), { concurrency: 16 });
54055
- const commitFacts = readOutcomes.map((outcome) => outcome.facts);
54163
+ const remapIdentities = yield* readMailmap(repoRoot, readOutcomes.values().flatMap(({ facts }) => facts.factsByCollector.values().flatMap((collectorFacts) => collectorFacts.values())));
54164
+ const commitFacts = readOutcomes.map(({ facts }) => remapIdentities === void 0 ? facts : {
54165
+ ...facts,
54166
+ factsByCollector: new Map(facts.factsByCollector.entries().map(([collector, collectorFacts]) => [collector, collectorFacts.map(remapIdentities)]))
54167
+ });
54056
54168
  const unknownCollectorDirs = readOutcomes.reduce((total, outcome) => total + outcome.unknownCollectorDirs, 0);
54057
54169
  const offMainlineSnapshots = readOutcomes.reduce((total, outcome) => total + outcome.offMainlineSnapshots, 0);
54058
54170
  const incompleteOutputs = readOutcomes.reduce((total, outcome) => total + outcome.incompleteOutputs, 0);