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.

1 lines 12.4 kB
{"version":3,"file":"utils-CFufYn8A.mjs","sources":["../../src/utils/files.ts","../../src/utils/formatting.ts","../../src/utils/normalization.ts","../../src/utils/similarity.ts","../../src/utils/dates.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\ninterface GitResult {\n ok: boolean;\n stdout?: string;\n error?: string;\n code?: number;\n}\n\ntype RunGitFunction = (repoPath: string, args: string[]) => GitResult;\n\nexport function safeReadPackageJson(): Record<string, unknown> {\n try {\n const pkgPath = path.join(process.cwd(), 'package.json');\n return JSON.parse(fs.readFileSync(pkgPath, { encoding: 'utf8' }));\n } catch {\n return {};\n }\n}\n\nexport function ensureDir(dir: string): void {\n if (!dir) return;\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n}\n\nexport function tryLoadJSON(filePath: string): Record<string, unknown> | null {\n try {\n const txt = fs.readFileSync(filePath, 'utf8');\n return JSON.parse(txt);\n } catch {\n return null;\n }\n}\n\nexport async function countTotalLines(repoPath: string, runGit: RunGitFunction): Promise<number> {\n try {\n const res = runGit(repoPath, ['ls-files']);\n if (!res.ok) return 0;\n\n const files = res.stdout?.split(/\\r?\\n/).filter(Boolean);\n if (!files) return 0;\n\n let total = 0;\n\n for (const rel of files) {\n const abs = path.join(repoPath, rel);\n try {\n const stat = fs.statSync(abs);\n if (!stat.isFile() || stat.size > 50 * 1024 * 1024) continue;\n const content = fs.readFileSync(abs, 'utf8');\n total += content.split(/\\r?\\n/).length;\n } catch {\n /* ignore */\n }\n }\n return total;\n } catch {\n return 0;\n }\n}\n","export function formatNumber(n: number): string {\n return new Intl.NumberFormat().format(n);\n}\n\nexport function svgEscape(s: string): string {\n return String(s).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\nexport function parseTopStatsMetrics(input?: string): string[] {\n const all = ['commits', 'additions', 'deletions', 'net', 'changes'];\n if (!input) return all;\n\n const set = new Set<string>();\n for (const part of String(input)\n .split(',')\n .map((s) => s.trim().toLowerCase())\n .filter(Boolean)) {\n if (all.includes(part)) set.add(part);\n }\n return set.size ? Array.from(set) : all;\n}\n\nexport interface TopStatsEntry {\n name?: string;\n email?: string;\n commits?: number;\n added?: number;\n deleted?: number;\n net?: number;\n changes?: number;\n}\n\nexport function getMetricValue(entry: TopStatsEntry, metricKey: string): number | undefined {\n if (typeof entry[metricKey as keyof TopStatsEntry] === 'number') {\n return entry[metricKey as keyof TopStatsEntry] as number;\n }\n if (metricKey === 'net') {\n return (entry.added || 0) - (entry.deleted || 0);\n }\n return undefined;\n}\n\nexport function formatTopStatsLines(\n ts: Record<string, TopStatsEntry>,\n metrics: string[]\n): string[] {\n const lines: string[] = [];\n const want = new Set(\n metrics?.length ? metrics : ['commits', 'additions', 'deletions', 'net', 'changes']\n );\n\n function line(label: string, entry: TopStatsEntry | undefined, metricKey: string): string {\n if (!entry) return `${label}: —`;\n const metricVal = getMetricValue(entry, metricKey);\n const suffix = typeof metricVal === 'number' ? ` (${metricVal})` : '';\n const emailPart = entry.email ? ` <${entry.email}>` : '';\n const who = `${entry.name || '—'}${emailPart}`;\n return `${label}: ${who}${suffix}`;\n }\n\n if (want.has('commits')) lines.push(line('Most commits', ts.byCommits, 'commits'));\n if (want.has('additions')) lines.push(line('Most additions', ts.byAdditions, 'added'));\n if (want.has('deletions')) lines.push(line('Most deletions', ts.byDeletions, 'deleted'));\n if (want.has('net')) lines.push(line('Best net lines', ts.byNet, 'net'));\n if (want.has('changes')) lines.push(line('Most changes (±)', ts.byChanges, 'changes'));\n return lines;\n}\n","export function normalizeContributorName(rawName?: string): string {\n const nameAsString = String(rawName || '');\n\n const withoutEmailDomain = nameAsString.replace(/@.*$/, '');\n const onlyAlphanumericAndSeparators = withoutEmailDomain.replaceAll(/[^a-zA-Z0-9\\s._-]/g, '');\n const collapsedWhitespace = onlyAlphanumericAndSeparators.replaceAll(/\\s+/g, ' ');\n const trimmedName = collapsedWhitespace.trim();\n\n return trimmedName.toLowerCase();\n}\n","export function levenshteinDistance(firstString: string, secondString: string): number {\n const firstLength = firstString.length;\n const secondLength = secondString.length;\n\n if (firstLength === 0) return secondLength;\n if (secondLength === 0) return firstLength;\n\n const distanceMatrix: number[][] = Array.from({ length: firstLength + 1 }, () =>\n new Array(secondLength + 1).fill(0)\n );\n\n for (let rowIndex = 0; rowIndex <= firstLength; rowIndex++) {\n distanceMatrix[rowIndex][0] = rowIndex;\n }\n\n for (let columnIndex = 0; columnIndex <= secondLength; columnIndex++) {\n distanceMatrix[0][columnIndex] = columnIndex;\n }\n\n for (let rowIndex = 1; rowIndex <= firstLength; rowIndex++) {\n for (let columnIndex = 1; columnIndex <= secondLength; columnIndex++) {\n const charactersMatch = firstString[rowIndex - 1] === secondString[columnIndex - 1];\n const substitutionCost = charactersMatch ? 0 : 1;\n\n const deletionCost = distanceMatrix[rowIndex - 1][columnIndex] + 1;\n const insertionCost = distanceMatrix[rowIndex][columnIndex - 1] + 1;\n const substitutionTotalCost =\n distanceMatrix[rowIndex - 1][columnIndex - 1] + substitutionCost;\n\n distanceMatrix[rowIndex][columnIndex] = Math.min(\n deletionCost,\n insertionCost,\n substitutionTotalCost\n );\n }\n }\n\n return distanceMatrix[firstLength][secondLength];\n}\n\nexport function calculateSimilarityScore(firstString: string, secondString: string): number {\n const longerStringLength = Math.max(firstString.length, secondString.length) || 1;\n const editDistance = levenshteinDistance(firstString.toLowerCase(), secondString.toLowerCase());\n\n return 1 - editDistance / longerStringLength;\n}\n","// Date parsing utilities for git-contributor-stats\n\nexport function parseDateInput(input?: string): string | undefined {\n if (!input) return undefined;\n\n const rel = /^(\\d+)\\.(day|days|week|weeks|month|months|year|years)$/i.exec(input.trim());\n if (rel) {\n const qty = Number.parseInt(rel[1], 10);\n const unit = rel[2].toLowerCase();\n const now = new Date();\n const d = new Date(now);\n\n function getUnitMultiplier(unitStr: string): number {\n if (unitStr.startsWith('day')) return 1;\n if (unitStr.startsWith('week')) return 7;\n if (unitStr.startsWith('month')) return 30;\n return 365;\n }\n\n const mult = getUnitMultiplier(unit);\n d.setDate(now.getDate() - qty * mult);\n return d.toISOString();\n }\n\n const d = new Date(input);\n if (!Number.isNaN(d.getTime())) return d.toISOString();\n return input;\n}\n\nexport function isoWeekKey(date: Date): string {\n const d = new Date(date);\n const target = new Date(d.valueOf());\n const dayNumber = (d.getDay() + 6) % 7;\n target.setDate(target.getDate() - dayNumber + 3);\n const firstThursday = new Date(target.getFullYear(), 0, 4);\n const diff = (target.getTime() - firstThursday.getTime()) / 86400000;\n const week = 1 + Math.round(diff / 7);\n return `${target.getFullYear()}-W${String(week).padStart(2, '0')}`;\n}\n"],"names":["d"],"mappings":"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"}