code996
Version:
通过分析 Git commit 的时间分布,计算出项目的'996指数'
421 lines • 15.5 kB
JavaScript
;
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