UNPKG

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
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