git-contributor-stats
Version:
CLI to compute contributor and repository statistics from a Git repository (commits, lines added/deleted, frequency, heatmap, bus-factor), with filters and multiple output formats.
122 lines (121 loc) • 4.65 kB
JavaScript
import path from "node:path";
import { b as buildAliasResolver, a as aggregateBasic, p as pickSortMetric, c as computeMeta, d as analyze } from "../chunks/analytics-SL4YC1kG.mjs";
import { i as isGitRepo, b as buildGitLogArgs, r as runGit, p as parseGitLog } from "../chunks/git-BxSpsWYT.mjs";
import { p as parseDateInput, a as countTotalLines, t as tryLoadJSON } from "../chunks/utils-CFufYn8A.mjs";
import "node:child_process";
import "node:fs";
function loadAliasConfig(opts, repo) {
let aliasConfig = opts.aliasConfig ?? void 0;
let aliasPath = null;
if (!aliasConfig) {
if (opts.aliasFile) {
aliasPath = path.resolve(process.cwd(), opts.aliasFile);
aliasConfig = tryLoadJSON(aliasPath);
} else {
const defaultAlias = path.join(repo, ".git-contributor-stats-aliases.json");
aliasConfig = tryLoadJSON(defaultAlias);
if (aliasConfig) aliasPath = defaultAlias;
}
}
return { config: aliasConfig, path: aliasPath };
}
function normalizePaths(paths) {
if (Array.isArray(paths)) {
return paths;
}
if (paths) {
return [paths];
}
return [];
}
function getRepoBranch(repo, optsBranch) {
if (optsBranch) return optsBranch;
const result = runGit(repo, ["rev-parse", "--abbrev-ref", "HEAD"]);
return result.stdout?.trim() || null;
}
async function getContributorStats(opts = {}) {
const repo = path.resolve(process.cwd(), opts.repo || ".");
if (!isGitRepo(repo)) throw new Error(`Not a Git repository: ${repo}`);
const debug = (...msg) => {
if (opts.verbose) console.error("[debug]", ...msg);
};
const { config: aliasConfig, path: aliasPath } = loadAliasConfig(opts, repo);
if (aliasPath) debug(`aliasFile=${aliasPath}`);
debug(`repo=${repo}`);
const { resolve: aliasResolveFn, canonicalDetails } = buildAliasResolver(aliasConfig);
const since = parseDateInput(opts.since);
const until = parseDateInput(opts.until);
const paths = normalizePaths(opts.paths);
const gitArgs = buildGitLogArgs({
branch: opts.branch,
since,
until,
author: opts.author,
includeMerges: !!opts.includeMerges,
paths
});
debug("git", gitArgs.map((a) => a.includes(" ") ? `'${a}'` : a).join(" "));
const result = runGit(repo, gitArgs);
if (!result.ok) throw new Error(result.error || "Failed to run git log");
const commits = parseGitLog(result.stdout);
debug(`parsed commits: ${commits.length}`);
const groupBy = (opts.groupBy || "email").toLowerCase() === "name" ? "name" : "email";
const labelBy = opts.labelBy?.toLowerCase() === "email" ? "email" : "name";
const similarityThreshold = opts.similarity ?? 0.85;
let contributorsBasic = aggregateBasic(commits, {
groupBy,
aliasResolver: aliasResolveFn,
canonicalDetails,
similarity: similarityThreshold
});
const sorter = pickSortMetric(opts.sortBy || "changes");
contributorsBasic.sort(sorter);
if (opts.top && Number.isFinite(opts.top) && opts.top > 0) {
contributorsBasic = contributorsBasic.slice(0, opts.top);
}
const meta = computeMeta(contributorsBasic);
const analysis = analyze(commits, similarityThreshold, aliasResolveFn, canonicalDetails, groupBy);
let topContributors = analysis.topContributors;
if (opts.sortBy && opts.sortBy !== "commits") {
const metricSorter = pickSortMetric(opts.sortBy);
topContributors = [...topContributors].sort(metricSorter);
}
if (opts.top && Number.isFinite(opts.top) && opts.top > 0) {
topContributors = topContributors.slice(0, opts.top);
}
let totalLines = 0;
if (opts.countLines !== false) {
totalLines = await countTotalLines(repo, runGit);
}
const repoRootResult = runGit(repo, ["rev-parse", "--show-toplevel"]);
const repoRoot = repoRootResult.ok ? repoRootResult.stdout?.trim() ?? repo : repo;
const branch = getRepoBranch(repo, opts.branch);
return {
meta: {
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
repo: repoRoot,
branch,
since: since || null,
until: until || null
},
totalCommits: analysis.totalCommits,
totalLines,
contributors: analysis.contributors,
topContributors,
topStats: analysis.topStats,
commitFrequency: analysis.commitFrequency,
heatmap: analysis.heatmap,
heatmapContributors: analysis.heatmapContributors,
busFactor: {
busFactor: analysis.busFactor.busFactor ?? 0,
candidates: analysis.busFactor.candidates ?? [],
details: analysis.busFactor.details,
filesSingleOwner: analysis.busFactor.filesSingleOwner
},
basic: { meta, groupBy, labelBy }
};
}
export {
getContributorStats as g
};
//# sourceMappingURL=stats.mjs.map