repo-dive 0.13.0 → 0.14.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/dist/cli.js
CHANGED
|
@@ -42694,7 +42694,7 @@ var runWith = (command, config) => {
|
|
|
42694
42694
|
};
|
|
42695
42695
|
var package_default = {
|
|
42696
42696
|
name: "repo-dive",
|
|
42697
|
-
version: "0.
|
|
42697
|
+
version: "0.14.0",
|
|
42698
42698
|
description: "Dive into a git repo's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
|
|
42699
42699
|
keywords: [
|
|
42700
42700
|
"git",
|
|
@@ -42956,10 +42956,11 @@ var languageOfExtension = (extension) => languageByExtension[extension] ?? exten
|
|
|
42956
42956
|
/**
|
|
42957
42957
|
* Whether a collector describes the *state of the tree* at a commit rather than
|
|
42958
42958
|
* facts about the commit itself. Such snapshots are only meaningful on the
|
|
42959
|
-
* first-parent chain
|
|
42960
|
-
*
|
|
42961
|
-
*
|
|
42962
|
-
*
|
|
42959
|
+
* mainline (the first-parent chain, extended backwards across founding
|
|
42960
|
+
* grafts — see `listMainlineShas`): a commit that lives on a side branch — or
|
|
42961
|
+
* that arrived with a foreign history absorbed mid-life by an
|
|
42962
|
+
* unrelated-histories merge — carries a tree that was never the repository's
|
|
42963
|
+
* state, so charting it puts a cliff into every timeline.
|
|
42963
42964
|
*/
|
|
42964
42965
|
var describesTreeState = (collector) => collector.strategy !== "log";
|
|
42965
42966
|
/** File extension used as a category key, e.g. ".ts"; files without one map to "(none)". */
|
|
@@ -50809,18 +50810,104 @@ var listCommits = (repoRoot) => runGit([
|
|
|
50809
50810
|
"log",
|
|
50810
50811
|
`--format=${gitLogFormat}`
|
|
50811
50812
|
]).pipe(succeedIfNoCommitsYet, map$7(parseGitLog));
|
|
50813
|
+
var chainLogFormat = [
|
|
50814
|
+
"%H",
|
|
50815
|
+
"%P",
|
|
50816
|
+
"%cI"
|
|
50817
|
+
].join("%x1f");
|
|
50812
50818
|
/**
|
|
50813
|
-
*
|
|
50814
|
-
* passed
|
|
50815
|
-
*
|
|
50819
|
+
* The first-parent chain of `tip` (HEAD when omitted), newest first. HEAD is
|
|
50820
|
+
* left implicit rather than passed: an explicit rev makes git fail an empty
|
|
50821
|
+
* repository with "unknown revision" instead of the "does not have any commits
|
|
50822
|
+
* yet" that {@link succeedIfNoCommitsYet} recovers from.
|
|
50816
50823
|
*/
|
|
50817
|
-
var
|
|
50824
|
+
var listFirstParentChain = (repoRoot, tip) => runGit([
|
|
50818
50825
|
"-C",
|
|
50819
50826
|
repoRoot,
|
|
50820
50827
|
"log",
|
|
50821
50828
|
"--first-parent",
|
|
50822
|
-
|
|
50823
|
-
|
|
50829
|
+
`--format=${chainLogFormat}`,
|
|
50830
|
+
...tip === void 0 ? [] : [tip]
|
|
50831
|
+
]).pipe(succeedIfNoCommitsYet, map$7((stdout) => stdout.split("\n").filter(Boolean).map((line) => {
|
|
50832
|
+
const [hash = "", parents = "", committerDate = ""] = line.split(fieldSeparator);
|
|
50833
|
+
return {
|
|
50834
|
+
hash,
|
|
50835
|
+
parentHashes: parents.split(" ").filter(Boolean),
|
|
50836
|
+
committerDate
|
|
50837
|
+
};
|
|
50838
|
+
})));
|
|
50839
|
+
/** No common ancestor means the two commits come from unrelated histories. */
|
|
50840
|
+
var haveUnrelatedHistories = (repoRoot, left, right) => runGit([
|
|
50841
|
+
"-C",
|
|
50842
|
+
repoRoot,
|
|
50843
|
+
"merge-base",
|
|
50844
|
+
left,
|
|
50845
|
+
right
|
|
50846
|
+
], { okExitCodes: [1] }).pipe(map$7((stdout) => stdout.trim() === ""));
|
|
50847
|
+
/**
|
|
50848
|
+
* The first-parent chain that continues `chain` backwards in time across a
|
|
50849
|
+
* founding graft, or `[]` when there is none.
|
|
50850
|
+
*
|
|
50851
|
+
* A repository migration (monorepo assembly, host move, history rewrite)
|
|
50852
|
+
* leaves a recognizable signature: a fresh root commit followed immediately by
|
|
50853
|
+
* merges that absorb the project's previous histories. Both conditions below
|
|
50854
|
+
* are required, so ordinary absorptions stay excluded:
|
|
50855
|
+
*
|
|
50856
|
+
* - the merge sits in the founding window — the unbroken run of merges
|
|
50857
|
+
* directly above the root, before the first ordinary commit. A foreign
|
|
50858
|
+
* history vendored later in the repository's life does not qualify.
|
|
50859
|
+
* - the absorbed history ends before the root begins, so it occupies the
|
|
50860
|
+
* stretch of timeline where the current chain has nothing to say. A side
|
|
50861
|
+
* history that overlaps the chain (e.g. a plugin repository absorbed while
|
|
50862
|
+
* mainline development continued) does not qualify.
|
|
50863
|
+
*
|
|
50864
|
+
* When several absorbed histories qualify (effect's monorepo assembly merged
|
|
50865
|
+
* eight), the timeline can only continue into one of them: the one reaching
|
|
50866
|
+
* back furthest wins.
|
|
50867
|
+
*/
|
|
50868
|
+
var findFoundingGraftChain = (repoRoot, chain, alreadyOnMainline) => gen(function* () {
|
|
50869
|
+
const root = chain.at(-1);
|
|
50870
|
+
if (root === void 0) return [];
|
|
50871
|
+
const rootDate = Date.parse(root.committerDate);
|
|
50872
|
+
const candidateTips = [];
|
|
50873
|
+
for (let index = chain.length - 2; index >= 0; index -= 1) {
|
|
50874
|
+
const entry = chain[index];
|
|
50875
|
+
if (entry === void 0 || entry.parentHashes.length < 2) break;
|
|
50876
|
+
candidateTips.push(...entry.parentHashes.slice(1));
|
|
50877
|
+
}
|
|
50878
|
+
let best = [];
|
|
50879
|
+
let bestRootDate = Number.POSITIVE_INFINITY;
|
|
50880
|
+
for (const tip of candidateTips) {
|
|
50881
|
+
if (alreadyOnMainline.has(tip)) continue;
|
|
50882
|
+
if (!(yield* haveUnrelatedHistories(repoRoot, root.hash, tip))) continue;
|
|
50883
|
+
const candidate = yield* listFirstParentChain(repoRoot, tip);
|
|
50884
|
+
const tipDate = Date.parse(candidate.at(0)?.committerDate ?? "");
|
|
50885
|
+
const candidateRootDate = Date.parse(candidate.at(-1)?.committerDate ?? "");
|
|
50886
|
+
if (Number.isNaN(tipDate) || Number.isNaN(candidateRootDate) || tipDate >= rootDate) continue;
|
|
50887
|
+
if (candidateRootDate < bestRootDate) {
|
|
50888
|
+
best = candidate;
|
|
50889
|
+
bestRootDate = candidateRootDate;
|
|
50890
|
+
}
|
|
50891
|
+
}
|
|
50892
|
+
return best;
|
|
50893
|
+
});
|
|
50894
|
+
/**
|
|
50895
|
+
* Shas on the mainline — the states the repository actually passed through.
|
|
50896
|
+
* That is HEAD's first-parent chain, extended backwards across founding grafts
|
|
50897
|
+
* (see {@link findFoundingGraftChain}): when a migration absorbed the
|
|
50898
|
+
* project's previous history behind a fresh root, the absorbed mainline is the
|
|
50899
|
+
* continuation of the timeline. See {@link describesTreeState} for why
|
|
50900
|
+
* snapshot collectors are restricted to the mainline.
|
|
50901
|
+
*/
|
|
50902
|
+
var listMainlineShas = (repoRoot) => gen(function* () {
|
|
50903
|
+
const shas = /* @__PURE__ */ new Set();
|
|
50904
|
+
let chain = yield* listFirstParentChain(repoRoot);
|
|
50905
|
+
while (chain.length > 0) {
|
|
50906
|
+
for (const entry of chain) shas.add(entry.hash);
|
|
50907
|
+
chain = yield* findFoundingGraftChain(repoRoot, chain, shas);
|
|
50908
|
+
}
|
|
50909
|
+
return shas;
|
|
50910
|
+
});
|
|
50824
50911
|
var summarizeCommits = (commits) => {
|
|
50825
50912
|
const authorEmails = new Set(commits.map((commit) => commit.authorEmail));
|
|
50826
50913
|
const dates = commits.flatMap((commit) => [commit.authorDate, commit.committerDate]).filter((date) => !Number.isNaN(Date.parse(date))).toSorted((left, right) => Date.parse(left) - Date.parse(right));
|
|
@@ -50900,10 +50987,10 @@ var runScan = ({ repoPath, collectorNames, maxCommits, sample, force = false })
|
|
|
50900
50987
|
const summary = summarizeCommits(commits);
|
|
50901
50988
|
const cacheKeys = new Map(collectors.map((collector) => [collector.name, collectorCacheKey(collector, config)]));
|
|
50902
50989
|
const cacheKeyOf = (collector) => cacheKeys.get(collector.name) ?? collectorCacheKey(collector, config);
|
|
50903
|
-
const
|
|
50990
|
+
const mainlineShas = yield* listMainlineShas(repoRoot);
|
|
50904
50991
|
const plans = collectors.map((collector) => {
|
|
50905
50992
|
const policy = sampleOverride ?? collector.defaultSampling;
|
|
50906
|
-
const candidates = describesTreeState(collector) ? selected.filter((commit) =>
|
|
50993
|
+
const candidates = describesTreeState(collector) ? selected.filter((commit) => mainlineShas.has(commit.hash)) : selected;
|
|
50907
50994
|
return {
|
|
50908
50995
|
collector,
|
|
50909
50996
|
policy,
|
|
@@ -51101,7 +51188,7 @@ var buildPlan = (repoRoot) => gen(function* () {
|
|
|
51101
51188
|
"rev-list",
|
|
51102
51189
|
"HEAD"
|
|
51103
51190
|
])).split("\n").filter(Boolean));
|
|
51104
|
-
const
|
|
51191
|
+
const mainlineShas = yield* listMainlineShas(repoRoot);
|
|
51105
51192
|
const currentCacheKeys = new Map(builtInCollectors.map((collector) => [collector.name, collectorCacheKey(collector, config)]));
|
|
51106
51193
|
const snapshotCollectorNames = new Set(builtInCollectors.filter((collector) => describesTreeState(collector)).map((collector) => collector.name));
|
|
51107
51194
|
const unreachableShas = [];
|
|
@@ -51116,7 +51203,7 @@ var buildPlan = (repoRoot) => gen(function* () {
|
|
|
51116
51203
|
const collectorNames = yield* readdirIfExists(path.join(commitsPath, sha));
|
|
51117
51204
|
for (const collectorName of collectorNames) {
|
|
51118
51205
|
countsByCollector.set(collectorName, (countsByCollector.get(collectorName) ?? 0) + 1);
|
|
51119
|
-
if (!
|
|
51206
|
+
if (!mainlineShas.has(sha) && snapshotCollectorNames.has(collectorName)) offMainlineOutputs.push({
|
|
51120
51207
|
sha,
|
|
51121
51208
|
collectorName
|
|
51122
51209
|
});
|
|
@@ -51737,7 +51824,7 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
51737
51824
|
const commitsPath = path.join(catalogPath, "commits");
|
|
51738
51825
|
const registry = new Map(builtInCollectors.map((collector) => [collector.name, collector]));
|
|
51739
51826
|
const gitCommits = yield* listCommits(repoRoot);
|
|
51740
|
-
const
|
|
51827
|
+
const mainlineShas = yield* listMainlineShas(repoRoot);
|
|
51741
51828
|
const remoteUrl = yield* readRemoteUrl(repoRoot);
|
|
51742
51829
|
const catalogShas = new Set(yield* tryPromise(async () => {
|
|
51743
51830
|
try {
|
|
@@ -51750,7 +51837,7 @@ var runIndex = ({ repoPath }) => gen(function* () {
|
|
|
51750
51837
|
if (orderedCommits.length === 0) return yield* new NoCollectedCommitsError({ commitsPath });
|
|
51751
51838
|
const readOutcomes = yield* forEach$1(orderedCommits, (commit) => tryPromise(async () => {
|
|
51752
51839
|
const commitDir = path.join(commitsPath, commit.hash);
|
|
51753
|
-
const onMainline =
|
|
51840
|
+
const onMainline = mainlineShas.has(commit.hash);
|
|
51754
51841
|
const factsByCollector = /* @__PURE__ */ new Map();
|
|
51755
51842
|
let unknownCollectorDirs = 0;
|
|
51756
51843
|
let offMainlineSnapshots = 0;
|
|
@@ -56767,7 +56854,7 @@ var exists = (filePath) => promise(() => access(filePath).then(() => true, () =>
|
|
|
56767
56854
|
var runStatus = ({ repoPath }) => gen(function* () {
|
|
56768
56855
|
const repoRoot = yield* resolveRepoRoot(repoPath);
|
|
56769
56856
|
const commits = yield* listCommits(repoRoot);
|
|
56770
|
-
const
|
|
56857
|
+
const mainlineShas = yield* listMainlineShas(repoRoot);
|
|
56771
56858
|
const config = yield* loadConfig(repoRoot);
|
|
56772
56859
|
const catalogPath = config.catalogPath;
|
|
56773
56860
|
if (!(yield* exists(path.join(catalogPath, "catalog.json")))) {
|
|
@@ -56789,7 +56876,7 @@ var runStatus = ({ repoPath }) => gen(function* () {
|
|
|
56789
56876
|
`Catalog: ${catalogPath}`
|
|
56790
56877
|
];
|
|
56791
56878
|
for (const collector of builtInCollectors) {
|
|
56792
|
-
const target = sampleCommits(describesTreeState(collector) ? commits.filter((commit) =>
|
|
56879
|
+
const target = sampleCommits(describesTreeState(collector) ? commits.filter((commit) => mainlineShas.has(commit.hash)) : commits, collector.defaultSampling);
|
|
56793
56880
|
const cacheKey = collectorCacheKey(collector, config);
|
|
56794
56881
|
const collected = (yield* forEach$1(target, (commit) => isCollected(catalog, commit.hash, collector, cacheKey), { concurrency: 16 })).filter(Boolean).length;
|
|
56795
56882
|
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})`);
|