branch-commit-compare
Version:
Git branch comparison tool
595 lines (531 loc) • 18.8 kB
JavaScript
const fs = require("fs").promises;
const http = require("http");
const path = require("path");
const chalk = require("chalk");
const inquirer = require("inquirer");
const autocomplete = require("inquirer-autocomplete-prompt");
const { getAllBranches, getAllAuthors, getAllCommits } = require("./lib/git");
const { loadIgnoredCommits, saveIgnoredCommits } = require("./lib/ignore");
const { loadRemarks, saveRemarks } = require("./lib/remark");
const { generateMarkdown, generateTimelineHTML } = require("./lib/report");
const { generateDiffReport } = require("./lib/report");
const { execSync } = require("child_process");
const { ensureDataDir, getDataFilePath } = require("./lib/config");
const packageJson = require("./package.json");
// const { startServer } = require('./lib/server'); // 不使用Express服务器
// 显示版本号
console.log(chalk.cyan(`branch-commit-compare v${packageJson.version}`));
console.log(chalk.gray("A tool for comparing git branch commits\n"));
// 注册自动完成插件
inquirer.registerPrompt("autocomplete", autocomplete);
// 获取源分支
async function getSourceBranch() {
const branches = getAllBranches();
const { sourceBranch } = await inquirer.prompt([
{
type: "autocomplete",
name: "sourceBranch",
message: "选择基准分支 (输入可搜索):",
source: async (answersSoFar, input = "") => {
return branches.filter((branch) =>
branch.toLowerCase().includes((input || "").toLowerCase())
);
}
}
]);
return sourceBranch;
}
// 获取目标分支
async function getTargetBranch(sourceBranch) {
const branches = getAllBranches();
const { targetBranch } = await inquirer.prompt([
{
type: "autocomplete",
name: "targetBranch",
message: "选择目标分支 (输入可搜索):",
source: async (answersSoFar, input = "") => {
return branches
.filter((b) => b !== sourceBranch)
.filter((branch) =>
branch.toLowerCase().includes((input || "").toLowerCase())
);
}
}
]);
return targetBranch;
}
// 获取时间范围
async function getTimeRange() {
const { timeRange } = await inquirer.prompt([
{
type: "list",
name: "timeRange",
message: "选择时间范围:",
choices: [
"全部时间",
"最近一周",
"最近两周",
"最近一个月",
"最近三个月",
"最近半年",
"最近一年"
]
}
]);
return timeRange;
}
// 获取作者
async function getAuthor() {
const authors = getAllAuthors();
const { author } = await inquirer.prompt([
{
type: "autocomplete",
name: "author",
message: "选择提交者 (输入可搜索):",
source: async (answersSoFar, input = "") => {
const authorChoices = ["全部", ...authors];
return authorChoices.filter((choice) =>
choice.toLowerCase().includes((input || "").toLowerCase())
);
}
}
]);
return author;
}
// 获取提交信息
async function getCommits(sourceBranch, targetBranch, timeRange, author) {
console.log(
`正在获取提交信息:源分支=${sourceBranch}, 目标分支=${targetBranch}, 时间范围=${timeRange}, 作者=${author}`
);
const sourceCommits = getAllCommits(sourceBranch, null, author, timeRange);
console.log(`获取到 ${sourceBranch} 分支的提交数量: ${sourceCommits.length}`);
// 调试:检查是否有 body
const sourceWithBody = sourceCommits.filter((c) => c.body && c.body.trim());
console.log(`其中包含描述的提交: ${sourceWithBody.length}`);
if (sourceWithBody.length > 0) {
console.log(
`示例: ${sourceWithBody[0].hash.substring(0, 7)} - body: "${sourceWithBody[0].body.substring(0, 50)}..."`
);
}
const targetCommits = getAllCommits(targetBranch, null, author, timeRange);
console.log(`获取到 ${targetBranch} 分支的提交数量: ${targetCommits.length}`);
// 调试:检查是否有 body
const targetWithBody = targetCommits.filter((c) => c.body && c.body.trim());
console.log(`其中包含描述的提交: ${targetWithBody.length}`);
if (targetWithBody.length > 0) {
console.log(
`示例: ${targetWithBody[0].hash.substring(0, 7)} - body: "${targetWithBody[0].body.substring(0, 50)}..."`
);
}
// 获取cherry-pick时间映射
const cherryPickTimes = new Map();
try {
// 使用git reflog show targetBranch获取目标分支的操作历史
const reflogOutput = execSync(
`git reflog show ${targetBranch} --date=iso --pretty=format:"%h|%gd|%ad|%gs" --grep="cherry-pick"`,
{ encoding: "utf-8" }
);
if (reflogOutput) {
reflogOutput.split("\n").forEach((line) => {
if (!line) return;
const [shortHash, ref, date, message] = line.split("|");
// 尝试从消息中提取被cherry-pick的提交hash
const cherryPickMatch = message.match(/cherry-pick: ([a-f0-9]+)/);
if (cherryPickMatch) {
const originalHash = cherryPickMatch[1];
const pickDate = new Date(date);
cherryPickTimes.set(originalHash, pickDate.toISOString());
}
});
}
} catch (error) {
console.error("获取cherry-pick时间失败:", error);
}
// 合并所有提交信息
const commitsMap = new Map();
const messageMap = new Map();
// 首先处理源分支提交
sourceCommits.forEach((commit) => {
const normalizedMessage = commit.message.trim();
commitsMap.set(commit.hash, {
...commit,
status: "source",
branches: [sourceBranch],
normalizedMessage
});
if (!messageMap.has(normalizedMessage)) {
messageMap.set(normalizedMessage, [commit.hash]);
} else {
messageMap.get(normalizedMessage).push(commit.hash);
}
});
// 然后处理目标分支提交,同时检查消息相似性
targetCommits.forEach((commit) => {
const normalizedMessage = commit.message.trim();
if (commitsMap.has(commit.hash)) {
const existingCommit = commitsMap.get(commit.hash);
existingCommit.status = "both";
existingCommit.branches = [sourceBranch, targetBranch];
return;
}
if (messageMap.has(normalizedMessage)) {
const sourceHashes = messageMap.get(normalizedMessage);
sourceHashes.forEach((sourceHash) => {
const sourceCommit = commitsMap.get(sourceHash);
if (sourceCommit && sourceCommit.status === "source") {
sourceCommit.status = "both";
sourceCommit.branches = [sourceBranch, targetBranch];
sourceCommit.matchedByMessage = true;
if (cherryPickTimes.has(sourceHash)) {
sourceCommit.cherryPickTime = cherryPickTimes.get(sourceHash);
} else {
sourceCommit.cherryPickTime = commit.date;
}
sourceCommit.targetHash = commit.hash;
}
});
return;
}
commitsMap.set(commit.hash, {
...commit,
status: "target",
branches: [targetBranch],
normalizedMessage
});
});
const mergedCommits = Array.from(commitsMap.values());
// 统计各类提交数量
const sourceOnly = mergedCommits.filter((c) => c.status === "source").length;
const targetOnly = mergedCommits.filter((c) => c.status === "target").length;
const common = mergedCommits.filter((c) => c.status === "both").length;
console.log(`合并后的总提交数量: ${mergedCommits.length}`);
console.log(`- 仅在 ${sourceBranch} 分支: ${sourceOnly}`);
console.log(`- 仅在 ${targetBranch} 分支: ${targetOnly}`);
console.log(`- 共同提交: ${common} (包含通过消息匹配的提交)`);
if (mergedCommits.length === 0) {
console.warn("警告: 没有找到任何提交记录!");
}
return mergedCommits;
}
// 创建服务器处理忽略提交的保存
const server = http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Cache-Control, Pragma"
);
if (req.method === "OPTIONS") {
res.writeHead(200);
res.end();
return;
}
// 解析URL和查询参数
const url = new URL(req.url, `http://${req.headers.host}`);
const pathname = url.pathname;
const params = Object.fromEntries(url.searchParams);
if (req.method === "GET" && pathname === "/ignored-commits") {
try {
const ignoredCommits = await loadIgnoredCommits();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ignoredCommits }));
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: error.message }));
}
} else if (req.method === "POST" && pathname === "/ignore-commit") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
const { ignoredCommits } = JSON.parse(body);
await saveIgnoredCommits(ignoredCommits);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true }));
} catch (error) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: error.message }));
}
});
} else if (req.method === "GET" && pathname === "/commit-remarks") {
try {
// 使用新的备注模块加载备注
const commitRemarks = await loadRemarks();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ commitRemarks }));
} catch (error) {
console.error("获取备注失败:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: error.message }));
}
} else if (req.method === "POST" && pathname === "/commit-remarks") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
try {
console.log("收到备注保存请求");
// 解析请求体
let commitRemarks;
try {
const data = JSON.parse(body);
console.log(
"请求体解析成功:",
`${JSON.stringify(data).substring(0, 200)}...`
);
if (!data) {
throw new Error("请求体为空");
}
commitRemarks = data.commitRemarks;
console.log(
"解析备注数据成功, 类型:",
Array.isArray(commitRemarks) ? "Array" : typeof commitRemarks
);
} catch (parseError) {
console.error("解析请求体失败:", parseError);
throw new Error(`无效的请求格式: ${parseError.message}`);
}
// 验证备注数据
if (!Array.isArray(commitRemarks)) {
// 如果前端还是发送对象格式, 转换为数组格式
if (typeof commitRemarks === "object" && commitRemarks !== null) {
const remarkArray = [];
for (const hash in commitRemarks) {
if (Object.prototype.hasOwnProperty.call(commitRemarks, hash)) {
remarkArray.push({
hash: hash,
content: commitRemarks[hash] || "",
timestamp: new Date().toISOString()
});
}
}
commitRemarks = remarkArray;
console.log(
"已将对象格式的备注转换为数组格式, 条数:",
commitRemarks.length
);
} else {
console.warn("备注数据格式不正确, 将使用空数组");
commitRemarks = [];
}
}
// 过滤掉无效的备注
commitRemarks = commitRemarks.filter(
(remark) =>
remark &&
remark.hash &&
typeof remark.hash === "string" &&
remark.hash.trim().length > 0
);
console.log("过滤后的备注数量:", commitRemarks.length);
try {
// 使用新的备注模块保存备注
const result = await saveRemarks(commitRemarks);
console.log("备注保存结果:", result ? "成功" : "失败");
// 返回成功响应
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: true,
message: "备注保存成功",
timestamp: new Date().toISOString(),
commitRemarks: commitRemarks,
count: commitRemarks.length
})
);
} catch (saveError) {
console.error("调用saveRemarks失败:", saveError);
throw saveError;
}
} catch (error) {
console.error("保存备注失败:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
error: error.message,
timestamp: new Date().toISOString()
})
);
}
});
} else if (req.method === "GET" && pathname === "/git/show") {
try {
// 获取提交哈希
const hash = params.hash;
if (!hash) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
message: "缺少提交哈希参数"
})
);
return;
}
console.log(`获取提交 ${hash} 的变更信息`);
// 使用Git命令获取提交的差异信息
try {
// 验证提交哈希是否存在和有效
const objectType = execSync(`git cat-file -t ${hash}`, {
encoding: "utf-8"
}).trim();
console.log(`提交 ${hash} 的对象类型: ${objectType}`);
// 获取提交差异 - 添加参数使输出更完整
const diff = execSync(`git show --patch --stat ${hash}`, {
encoding: "utf-8",
maxBuffer: 5 * 1024 * 1024 // 5MB缓冲区
});
console.log(`获取到提交 ${hash} 的差异数据, 长度: ${diff.length} 字节`);
// 打印差异数据的前100个字符作为调试信息
console.log(`差异数据预览: ${diff.substring(0, 100)}...`);
// 构造响应对象
const responseData = {
success: true,
data: {
hash,
diff: diff
}
};
console.log(
`响应数据结构: ${JSON.stringify(Object.keys(responseData))}`
);
console.log(
`数据对象结构: ${JSON.stringify(Object.keys(responseData.data))}`
);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(responseData));
} catch (gitError) {
console.error("Git操作失败:", gitError.message);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
message: `Git操作失败: ${gitError.message}`
})
);
}
} catch (error) {
console.error("获取提交差异失败:", error);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
success: false,
message: `服务器错误: ${error.message}`
})
);
}
} else {
res.writeHead(404);
res.end();
}
});
// 添加获取可用端口的函数
function getAvailablePort(startPort) {
return new Promise((resolve, reject) => {
const server = require("net").createServer();
server.unref();
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
server.listen(++startPort);
} else {
reject(err);
}
});
server.listen(startPort, () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
}
async function main() {
try {
// 确保数据目录已初始化
await ensureDataDir();
// 获取可用端口
const port = await getAvailablePort(3000);
// 启动服务器
await new Promise((resolve) => {
server.listen(port, () => {
console.log(chalk.blue(`服务器已启动在端口 ${port}`));
resolve();
});
});
console.log(chalk.blue("开始比较分支差异..."));
// 获取分支信息
const sourceBranch = await getSourceBranch();
const targetBranch = await getTargetBranch(sourceBranch);
const timeRange = await getTimeRange();
const author = await getAuthor();
// 获取提交信息
const commits = await getCommits(
sourceBranch,
targetBranch,
timeRange,
author
);
// 生成报告
const comparison = {
sourceBranch,
targetBranch,
author,
timeRange,
commits
};
// 生成并保存 Markdown 报告
const markdown = await generateMarkdown(comparison);
const markdownPath = getDataFilePath("branch-diff.md");
await fs.writeFile(markdownPath, markdown);
// 获取备注信息
const commitRemarks = await loadRemarks();
// 生成并保存 HTML 时间轴,传入端口
const html = await generateTimelineHTML(
commits,
sourceBranch,
targetBranch,
author,
timeRange,
commitRemarks,
port
);
const htmlPath = getDataFilePath("branch-timeline.html");
await fs.writeFile(htmlPath, html);
console.log(chalk.green("\n报告生成完成:"));
console.log(chalk.green(`- Markdown 报告: ${markdownPath}`));
console.log(chalk.green(`- HTML 时间轴: ${htmlPath}`));
// 尝试打开浏览器
try {
const { exec } = require("child_process");
const command =
process.platform === "darwin"
? `open "${htmlPath}"`
: process.platform === "win32"
? `start "" "${htmlPath}"`
: `xdg-open "${htmlPath}"`;
exec(command, (error) => {
if (error) {
console.log(chalk.yellow("\n无法自动打开浏览器,请手动打开HTML文件"));
} else {
console.log(chalk.green("\n已在浏览器中打开报告"));
}
});
} catch (openError) {
console.log(chalk.yellow("\n无法自动打开浏览器,请手动打开HTML文件"));
}
} catch (error) {
console.error(chalk.red("发生错误:"), error);
process.exit(1);
}
}
// 添加清理函数
process.on("SIGINT", () => {
server.close(() => {
console.log(chalk.yellow("\n已关闭服务器"));
process.exit();
});
});
// 启动程序
main();