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.
116 lines (115 loc) • 3.5 kB
JavaScript
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
function createCommitFromHeader(headerLine) {
const [hash, name, email, date] = headerLine.split("\0");
if (!hash) return null;
return {
hash,
authorName: name || "",
authorEmail: email || "",
date: date ? new Date(date) : void 0,
additions: 0,
deletions: 0,
filesChanged: 0,
files: []
};
}
function parseFileChangeLine(line) {
const parts = line.split(/\t+/);
if (parts.length < 3) return null;
const added = parts[0] === "-" ? 0 : Number.parseInt(parts[0], 10) || 0;
const deleted = parts[1] === "-" ? 0 : Number.parseInt(parts[1], 10) || 0;
const filename = parts.slice(2).join(" ");
return { added, deleted, filename };
}
function addFileChangeToCommit(commit, fileChange) {
commit.additions += fileChange.added;
commit.deletions += fileChange.deleted;
commit.filesChanged += 1;
commit.files.push(fileChange);
}
function processHeaderLine(line) {
if (!line) return null;
return createCommitFromHeader(line);
}
function processFileChangeLine(commit, line) {
if (!line) return;
const fileChange = parseFileChangeLine(line);
if (fileChange) {
addFileChangeToCommit(commit, fileChange);
}
}
function parseGitLog(stdout) {
if (!stdout) return [];
const lines = stdout.split(/\r?\n/);
const commits = [];
let current = null;
let expectHeader = false;
for (const line of lines) {
if (line === "---") {
if (current) commits.push(current);
current = null;
expectHeader = true;
} else if (expectHeader) {
current = processHeaderLine(line);
expectHeader = false;
} else if (current) {
processFileChangeLine(current, line);
}
}
if (current) commits.push(current);
return commits;
}
function runGit(repoPath, args) {
const res = spawnSync("git", args, {
cwd: repoPath,
encoding: "utf8",
maxBuffer: 1024 * 1024 * 1024
});
if (res.error) {
const err = res.error;
const msg = err.code === "ENOENT" ? "Git is not installed or not in PATH." : `Failed to execute git: ${res.error.message}`;
return { ok: false, error: msg, code: res.status ?? 2 };
}
if (res.status !== 0) {
const stderr = (res.stderr || "").trim();
if (/does not have any commits yet/i.test(stderr) || /bad default revision 'HEAD'/i.test(stderr) || /ambiguous argument 'HEAD'/i.test(stderr)) {
return { ok: true, stdout: "" };
}
return {
ok: false,
error: (stderr || res.stdout || "Unknown git error").trim(),
code: res.status || 2
};
}
return { ok: true, stdout: res.stdout };
}
function isGitRepo(repoPath) {
try {
const gitFolder = path.join(repoPath, ".git");
return fs.existsSync(gitFolder);
} catch {
return false;
}
}
function buildGitLogArgs(opts) {
const { branch, since, until, author, includeMerges, paths } = opts;
const args = ["log", "--numstat", "--date=iso-strict", "--no-color"];
args.push("--pretty=format:---%n%H%x00%an%x00%ae%x00%ad");
if (!includeMerges) args.push("--no-merges");
if (since) args.push(`--since=${since}`);
if (until) args.push(`--until=${until}`);
if (author) args.push(`--author=${author}`);
if (branch) args.push(branch);
args.push("--");
if (paths?.length) for (const p of paths) args.push(p);
return args;
}
export {
buildGitLogArgs as b,
isGitRepo as i,
parseGitLog as p,
runGit as r
};
//# sourceMappingURL=git-BxSpsWYT.mjs.map