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.
1 lines • 7.13 kB
Source Map (JSON)
{"version":3,"file":"git-BxSpsWYT.mjs","sources":["../../src/git/parser.ts","../../src/git/utils.ts"],"sourcesContent":["interface FileChange {\n added: number;\n deleted: number;\n filename: string;\n}\n\nexport interface Commit {\n hash: string;\n authorName: string;\n authorEmail: string;\n date?: Date;\n additions: number;\n deletions: number;\n filesChanged: number;\n files: FileChange[];\n}\n\nfunction createCommitFromHeader(headerLine: string): Commit | null {\n const [hash, name, email, date] = headerLine.split('\\x00');\n if (!hash) return null;\n\n return {\n hash,\n authorName: name || '',\n authorEmail: email || '',\n date: date ? new Date(date) : undefined,\n additions: 0,\n deletions: 0,\n filesChanged: 0,\n files: []\n };\n}\n\nfunction parseFileChangeLine(line: string): FileChange | null {\n const parts = line.split(/\\t+/);\n if (parts.length < 3) return null;\n\n const added = parts[0] === '-' ? 0 : Number.parseInt(parts[0], 10) || 0;\n const deleted = parts[1] === '-' ? 0 : Number.parseInt(parts[1], 10) || 0;\n const filename = parts.slice(2).join('\\t');\n\n return { added, deleted, filename };\n}\n\nfunction addFileChangeToCommit(commit: Commit, fileChange: FileChange): void {\n commit.additions += fileChange.added;\n commit.deletions += fileChange.deleted;\n commit.filesChanged += 1;\n commit.files.push(fileChange);\n}\n\nfunction processHeaderLine(line: string): Commit | null {\n if (!line) return null;\n return createCommitFromHeader(line);\n}\n\nfunction processFileChangeLine(commit: Commit, line: string): void {\n if (!line) return;\n const fileChange = parseFileChangeLine(line);\n if (fileChange) {\n addFileChangeToCommit(commit, fileChange);\n }\n}\n\nexport function parseGitLog(stdout?: string): Commit[] {\n if (!stdout) return [];\n\n const lines = stdout.split(/\\r?\\n/);\n const commits: Commit[] = [];\n let current: Commit | null = null;\n let expectHeader = false;\n\n for (const line of lines) {\n if (line === '---') {\n if (current) commits.push(current);\n current = null;\n expectHeader = true;\n } else if (expectHeader) {\n current = processHeaderLine(line);\n expectHeader = false;\n } else if (current) {\n processFileChangeLine(current, line);\n }\n }\n\n if (current) commits.push(current);\n return commits;\n}\n","import { spawnSync } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nexport interface GitResult {\n ok: boolean;\n stdout?: string;\n error?: string;\n code?: number;\n}\n\nexport function runGit(repoPath: string, args: string[]): GitResult {\n const res = spawnSync('git', args, {\n cwd: repoPath,\n encoding: 'utf8',\n maxBuffer: 1024 * 1024 * 1024\n });\n\n if (res.error) {\n const err = res.error as NodeJS.ErrnoException;\n const msg =\n err.code === 'ENOENT'\n ? 'Git is not installed or not in PATH.'\n : `Failed to execute git: ${res.error.message}`;\n return { ok: false, error: msg, code: res.status ?? 2 };\n }\n\n if (res.status !== 0) {\n const stderr = (res.stderr || '').trim();\n if (\n /does not have any commits yet/i.test(stderr) ||\n /bad default revision 'HEAD'/i.test(stderr) ||\n /ambiguous argument 'HEAD'/i.test(stderr)\n ) {\n return { ok: true, stdout: '' };\n }\n return {\n ok: false,\n error: (stderr || res.stdout || 'Unknown git error').trim(),\n code: res.status || 2\n };\n }\n\n return { ok: true, stdout: res.stdout };\n}\n\nexport function isGitRepo(repoPath: string): boolean {\n try {\n const gitFolder = path.join(repoPath, '.git');\n return fs.existsSync(gitFolder);\n } catch {\n return false;\n }\n}\n\nexport interface GitLogArgsOptions {\n branch?: string;\n since?: string;\n until?: string;\n author?: string;\n includeMerges?: boolean;\n paths?: string[];\n}\n\nexport function buildGitLogArgs(opts: GitLogArgsOptions): string[] {\n const { branch, since, until, author, includeMerges, paths } = opts;\n const args = ['log', '--numstat', '--date=iso-strict', '--no-color'];\n args.push('--pretty=format:---%n%H%x00%an%x00%ae%x00%ad');\n\n if (!includeMerges) args.push('--no-merges');\n if (since) args.push(`--since=${since}`);\n if (until) args.push(`--until=${until}`);\n if (author) args.push(`--author=${author}`);\n if (branch) args.push(branch);\n args.push('--');\n if (paths?.length) for (const p of paths) args.push(p);\n\n return args;\n}\n"],"names":[],"mappings":";;;AAiBA,SAAS,uBAAuB,YAAmC;AACjE,QAAM,CAAC,MAAM,MAAM,OAAO,IAAI,IAAI,WAAW,MAAM,IAAM;AACzD,MAAI,CAAC,KAAM,QAAO;AAElB,SAAO;AAAA,IACL;AAAA,IACA,YAAY,QAAQ;AAAA,IACpB,aAAa,SAAS;AAAA,IACtB,MAAM,OAAO,IAAI,KAAK,IAAI,IAAI;AAAA,IAC9B,WAAW;AAAA,IACX,WAAW;AAAA,IACX,cAAc;AAAA,IACd,OAAO,CAAA;AAAA,EAAC;AAEZ;AAEA,SAAS,oBAAoB,MAAiC;AAC5D,QAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,MAAI,MAAM,SAAS,EAAG,QAAO;AAE7B,QAAM,QAAQ,MAAM,CAAC,MAAM,MAAM,IAAI,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,KAAK;AACtE,QAAM,UAAU,MAAM,CAAC,MAAM,MAAM,IAAI,OAAO,SAAS,MAAM,CAAC,GAAG,EAAE,KAAK;AACxE,QAAM,WAAW,MAAM,MAAM,CAAC,EAAE,KAAK,GAAI;AAEzC,SAAO,EAAE,OAAO,SAAS,SAAA;AAC3B;AAEA,SAAS,sBAAsB,QAAgB,YAA8B;AAC3E,SAAO,aAAa,WAAW;AAC/B,SAAO,aAAa,WAAW;AAC/B,SAAO,gBAAgB;AACvB,SAAO,MAAM,KAAK,UAAU;AAC9B;AAEA,SAAS,kBAAkB,MAA6B;AACtD,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,uBAAuB,IAAI;AACpC;AAEA,SAAS,sBAAsB,QAAgB,MAAoB;AACjE,MAAI,CAAC,KAAM;AACX,QAAM,aAAa,oBAAoB,IAAI;AAC3C,MAAI,YAAY;AACd,0BAAsB,QAAQ,UAAU;AAAA,EAC1C;AACF;AAEO,SAAS,YAAY,QAA2B;AACrD,MAAI,CAAC,OAAQ,QAAO,CAAA;AAEpB,QAAM,QAAQ,OAAO,MAAM,OAAO;AAClC,QAAM,UAAoB,CAAA;AAC1B,MAAI,UAAyB;AAC7B,MAAI,eAAe;AAEnB,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,OAAO;AAClB,UAAI,QAAS,SAAQ,KAAK,OAAO;AACjC,gBAAU;AACV,qBAAe;AAAA,IACjB,WAAW,cAAc;AACvB,gBAAU,kBAAkB,IAAI;AAChC,qBAAe;AAAA,IACjB,WAAW,SAAS;AAClB,4BAAsB,SAAS,IAAI;AAAA,IACrC;AAAA,EACF;AAEA,MAAI,QAAS,SAAQ,KAAK,OAAO;AACjC,SAAO;AACT;AC5EO,SAAS,OAAO,UAAkB,MAA2B;AAClE,QAAM,MAAM,UAAU,OAAO,MAAM;AAAA,IACjC,KAAK;AAAA,IACL,UAAU;AAAA,IACV,WAAW,OAAO,OAAO;AAAA,EAAA,CAC1B;AAED,MAAI,IAAI,OAAO;AACb,UAAM,MAAM,IAAI;AAChB,UAAM,MACJ,IAAI,SAAS,WACT,yCACA,0BAA0B,IAAI,MAAM,OAAO;AACjD,WAAO,EAAE,IAAI,OAAO,OAAO,KAAK,MAAM,IAAI,UAAU,EAAA;AAAA,EACtD;AAEA,MAAI,IAAI,WAAW,GAAG;AACpB,UAAM,UAAU,IAAI,UAAU,IAAI,KAAA;AAClC,QACE,iCAAiC,KAAK,MAAM,KAC5C,+BAA+B,KAAK,MAAM,KAC1C,6BAA6B,KAAK,MAAM,GACxC;AACA,aAAO,EAAE,IAAI,MAAM,QAAQ,GAAA;AAAA,IAC7B;AACA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,QAAQ,UAAU,IAAI,UAAU,qBAAqB,KAAA;AAAA,MACrD,MAAM,IAAI,UAAU;AAAA,IAAA;AAAA,EAExB;AAEA,SAAO,EAAE,IAAI,MAAM,QAAQ,IAAI,OAAA;AACjC;AAEO,SAAS,UAAU,UAA2B;AACnD,MAAI;AACF,UAAM,YAAY,KAAK,KAAK,UAAU,MAAM;AAC5C,WAAO,GAAG,WAAW,SAAS;AAAA,EAChC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,gBAAgB,MAAmC;AACjE,QAAM,EAAE,QAAQ,OAAO,OAAO,QAAQ,eAAe,UAAU;AAC/D,QAAM,OAAO,CAAC,OAAO,aAAa,qBAAqB,YAAY;AACnE,OAAK,KAAK,8CAA8C;AAExD,MAAI,CAAC,cAAe,MAAK,KAAK,aAAa;AAC3C,MAAI,MAAO,MAAK,KAAK,WAAW,KAAK,EAAE;AACvC,MAAI,MAAO,MAAK,KAAK,WAAW,KAAK,EAAE;AACvC,MAAI,OAAQ,MAAK,KAAK,YAAY,MAAM,EAAE;AAC1C,MAAI,OAAQ,MAAK,KAAK,MAAM;AAC5B,OAAK,KAAK,IAAI;AACd,MAAI,OAAO,OAAQ,YAAW,KAAK,MAAO,MAAK,KAAK,CAAC;AAErD,SAAO;AACT;"}