UNPKG

code996

Version:

通过分析 Git commit 的时间分布,计算出项目的'996指数'

421 lines 15.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitCollector = void 0; const child_process_1 = require("child_process"); const chalk_1 = __importDefault(require("chalk")); class GitCollector { /** * 执行git命令并返回输出 */ async execGitCommand(args, cwd) { return new Promise((resolve, reject) => { // 确保路径是绝对路径 const absolutePath = require('path').resolve(cwd); const child = (0, child_process_1.spawn)('git', args, { cwd: absolutePath, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_DIR: `${absolutePath}/.git`, GIT_WORK_TREE: absolutePath, }, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { if (code === 0) { resolve(stdout); } else { reject(new Error(`Git命令执行失败 (退出码: ${code}): ${stderr}`)); } }); child.on('error', (err) => { reject(new Error(`无法执行git命令: ${err.message}`)); }); }); } /** * 检查是否为有效的Git仓库 */ async isValidGitRepo(path) { try { await this.execGitCommand(['status'], path); return true; } catch { return false; } } /** * 按小时统计commit数据 */ async getCommitsByHour(options) { const { path } = options; const args = ['log', '--format=%cd', `--date=format-local:%H`]; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); return this.parseTimeData(output, 'hour'); } /** * 按星期统计commit数据 */ async getCommitsByDay(options) { const { path } = options; const args = ['log', '--format=%cd', `--date=format-local:%u`]; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); return this.parseTimeData(output, 'day'); } /** * 解析时间数据 */ parseTimeData(output, type) { const lines = output.split('\n').filter((line) => line.trim()); const timeCounts = []; for (const line of lines) { const trimmedLine = line.trim(); const parts = trimmedLine.split(/\s+/); if (parts.length === 1) { const time = parts[0]; if (time) { // 查找是否已存在该时间点的计数 const existingIndex = timeCounts.findIndex((item) => item.time === time); if (existingIndex >= 0) { timeCounts[existingIndex].count++; } else { timeCounts.push({ time, count: 1, }); } } } } // 确保所有时间点都有数据(补0) if (type === 'hour') { return this.fillMissingHours(timeCounts); } return this.fillMissingDays(timeCounts); } /** * 按星期几和小时统计commit数据 */ async getCommitsByDayAndHour(options) { const { path } = options; // 使用 --date=format 同时获取星期几和小时 const args = ['log', '--format=%cd', '--date=format-local:%u %H']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\n').filter((line) => line.trim()); // 统计每个 weekday+hour 组合的提交数 const commitMap = new Map(); for (const line of lines) { const trimmed = line.trim(); const parts = trimmed.split(/\s+/); if (parts.length >= 2) { const weekday = parseInt(parts[0], 10); const hour = parseInt(parts[1], 10); if (!isNaN(weekday) && !isNaN(hour) && weekday >= 1 && weekday <= 7 && hour >= 0 && hour <= 23) { const key = `${weekday}-${hour}`; commitMap.set(key, (commitMap.get(key) || 0) + 1); } } } // 转换为数组格式 const result = []; commitMap.forEach((count, key) => { const [weekday, hour] = key.split('-').map((v) => parseInt(v, 10)); result.push({ weekday, hour, count }); }); return result; } /** * 获取每日最晚的提交时间 */ async getDailyLatestCommits(options) { const { path } = options; const args = ['log', '--format=%cd', '--date=format-local:%Y-%m-%dT%H:%M:%S']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\n').filter((line) => line.trim()); const dailyLatest = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) { continue; } const parsed = this.parseLocalTimestamp(trimmed); if (!parsed) { continue; } const minutesFromMidnight = parsed.hour * 60 + parsed.minute; const current = dailyLatest.get(parsed.dateKey); // 保存最晚的小时 if (current === undefined || minutesFromMidnight > current) { dailyLatest.set(parsed.dateKey, minutesFromMidnight); } } return Array.from(dailyLatest.entries()) .map(([date, minutes]) => ({ date, hour: Math.floor(minutes / 60), })) .sort((a, b) => a.date.localeCompare(b.date)); } /** * 获取每日所有提交的小时列表 */ async getDailyCommitHours(options) { const { path } = options; const args = ['log', '--format=%cd', '--date=format-local:%Y-%m-%dT%H:%M:%S']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\n').filter((line) => line.trim()); const dailyHours = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) { continue; } const parsed = this.parseLocalTimestamp(trimmed); if (!parsed) { continue; } if (!dailyHours.has(parsed.dateKey)) { dailyHours.set(parsed.dateKey, new Set()); } dailyHours.get(parsed.dateKey).add(parsed.hour); } return Array.from(dailyHours.entries()) .map(([date, hours]) => ({ date, hours, })) .sort((a, b) => a.date.localeCompare(b.date)); } /** * 获取每日最早的提交时间(分钟数表示) */ async getDailyFirstCommits(options) { const { path } = options; const args = ['log', '--format=%cd', '--date=format-local:%Y-%m-%dT%H:%M:%S']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\n').filter((line) => line.trim()); const dailyEarliest = new Map(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) { continue; } const parsed = this.parseLocalTimestamp(trimmed); if (!parsed) { continue; } const minutesFromMidnight = parsed.hour * 60 + parsed.minute; const current = dailyEarliest.get(parsed.dateKey); if (current === undefined || minutesFromMidnight < current) { dailyEarliest.set(parsed.dateKey, minutesFromMidnight); } } return Array.from(dailyEarliest.entries()) .map(([date, minutesFromMidnight]) => ({ date, minutesFromMidnight, })) .sort((a, b) => a.date.localeCompare(b.date)); } /** * 补全缺失的小时数据 */ fillMissingHours(data) { const hours = []; for (let i = 0; i < 24; i++) { const hour = i.toString().padStart(2, '0'); const existing = data.find((item) => item.time === hour); hours.push({ time: hour, count: existing ? existing.count : 0, }); } return hours; } /** * 补全缺失的星期数据 */ fillMissingDays(data) { const days = []; for (let i = 1; i <= 7; i++) { const day = i.toString(); const existing = data.find((item) => item.time === day); days.push({ time: day, count: existing ? existing.count : 0, }); } return days; } /** * 根据 CLI 选项解析作者身份,生成正则用于 git --author 过滤 */ async resolveSelfAuthor(path) { const email = await this.getGitConfigValue('user.email', path); const name = await this.getGitConfigValue('user.name', path); if (!email && !name) { throw new Error('启用 --self 需要先配置 git config user.name 或 user.email'); } const hasEmail = Boolean(email); const hasName = Boolean(name); const displayLabel = hasEmail && hasName ? `${name} <${email}>` : email || name || '未知用户'; const pattern = hasEmail ? this.escapeAuthorPattern(email) : this.escapeAuthorPattern(name); // hasName must be true here,缺邮箱时退回姓名 return { pattern, displayLabel, }; } /** 统计符合过滤条件的 commit 数量 */ async countCommits(options) { const { path } = options; const args = ['rev-list', '--count', 'HEAD']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const count = parseInt(output.trim(), 10); return isNaN(count) ? 0 : count; } /** * 获取最早的commit时间 */ async getFirstCommitDate(options) { const { path } = options; const args = ['log', '--format=%cd', '--date=format:%Y-%m-%d', '--reverse', '--max-parents=0']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\\n').filter((line) => line.trim()); return lines[0] || ''; } /** * 获取最新的commit时间 */ async getLastCommitDate(options) { const { path } = options; const args = ['log', '--format=%cd', '--date=format:%Y-%m-%d', '-1']; this.applyCommonFilters(args, options); const output = await this.execGitCommand(args, path); const lines = output.split('\\n').filter((line) => line.trim()); return lines[0] || ''; } /** * 收集Git数据 */ async collect(options) { if (!options.silent) { console.log(chalk_1.default.blue(`正在分析仓库: ${options.path}`)); } // 检查是否为有效的Git仓库 if (!(await this.isValidGitRepo(options.path))) { throw new Error(`路径 "${options.path}" 不是一个有效的Git仓库`); } try { const [byHour, byDay, totalCommits, dailyFirstCommits, dayHourCommits, dailyLatestCommits, dailyCommitHours] = await Promise.all([ this.getCommitsByHour(options), this.getCommitsByDay(options), this.countCommits(options), this.getDailyFirstCommits(options), this.getCommitsByDayAndHour(options), this.getDailyLatestCommits(options), this.getDailyCommitHours(options), ]); if (!options.silent) { console.log(chalk_1.default.green(`数据采集完成: ${totalCommits} 个commit`)); } return { byHour, byDay, totalCommits, dailyFirstCommits: dailyFirstCommits.length > 0 ? dailyFirstCommits : undefined, dayHourCommits: dayHourCommits.length > 0 ? dayHourCommits : undefined, dailyLatestCommits: dailyLatestCommits.length > 0 ? dailyLatestCommits : undefined, dailyCommitHours: dailyCommitHours.length > 0 ? dailyCommitHours : undefined, }; } catch (error) { if (!options.silent) { console.error(chalk_1.default.red(`数据采集失败: ${error.message}`)); } throw error; } } /** * 为 git 命令附加通用过滤条件(时间范围与作者) */ applyCommonFilters(args, options) { if (options.since) { args.push(`--since=${options.since}`); } if (options.until) { args.push(`--until=${options.until}`); } if (options.authorPattern) { args.push('--regexp-ignore-case'); args.push('--extended-regexp'); args.push(`--author=${options.authorPattern}`); } } /** * 解析 format-local 输出的时间戳,提取日期和小时信息 */ parseLocalTimestamp(timestamp) { const match = timestamp.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})/); if (!match) { return null; } const [, year, month, day, hourStr, minuteStr] = match; const hour = parseInt(hourStr, 10); const minute = parseInt(minuteStr, 10); if (Number.isNaN(hour) || Number.isNaN(minute)) { return null; } return { dateKey: `${year}-${month}-${day}`, hour, minute, }; } /** * 读取 git config 配置项(不存在时返回 null) */ async getGitConfigValue(key, path) { try { const value = await this.execGitCommand(['config', '--get', key], path); const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } catch { return null; } } /** * 转义正则特殊字符,构造安全的 --author 匹配模式 */ escapeAuthorPattern(source) { return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } } exports.GitCollector = GitCollector; GitCollector.DEFAULT_SINCE = '1970-01-01'; GitCollector.DEFAULT_UNTIL = '2100-01-01'; //# sourceMappingURL=git-collector.js.map