branch-commit-compare
Version:
Git branch comparison tool
445 lines (391 loc) • 12.8 kB
JavaScript
/**
* Render Module 渲染模块
* 负责UI的渲染和更新
*/
/**
* 渲染提交列表
* @param {boolean} shouldRestoreState - 是否恢复筛选状态(默认不恢复)
* @param {string} changedHash - 改变状态的提交哈希(用于动画)
* @param {boolean} isInitialLoad - 是否首次加载(用于动画)
*/
function renderCommits(
shouldRestoreState = false,
changedHash = null,
isInitialLoad = false
) {
console.log(
"渲染提交列表, shouldRestoreState:",
shouldRestoreState,
"changedHash:",
changedHash,
"isInitialLoad:",
isInitialLoad
);
// 调试:检查是否有 commit 包含 body
const commitsWithBody = appState.commits.filter(
(c) => c.body && c.body.trim()
);
console.log(`发现 ${commitsWithBody.length} 个包含描述的提交`);
if (commitsWithBody.length > 0) {
console.log(
"示例:",
commitsWithBody[0].hash.substring(0, 7),
commitsWithBody[0].body
);
}
const timeline = document.getElementById("timeline");
const currentLayout = document.body.getAttribute("data-layout") || "flat";
// 按时间排序(从新到旧)
const sortedCommits = [...appState.commits].sort(
(a, b) => new Date(b.date || b.dateIso) - new Date(a.date || a.dateIso)
);
// 如果有changedHash,只更新那个特定的卡片
if (changedHash) {
const existingRow = timeline
.querySelector(`[data-hash="${changedHash}"]`)
?.closest(".commit-row");
if (existingRow) {
const commit = sortedCommits.find((c) => c.hash === changedHash);
if (commit) {
const newRow = renderCommitCard(commit, currentLayout);
newRow.classList.add("state-changed");
existingRow.replaceWith(newRow);
// 移除动画类,以便下次可以再次触发
setTimeout(() => {
newRow.classList.remove("state-changed");
}, 300);
}
updateCherryPickCommands();
console.log("已更新单个提交卡片");
return;
}
}
// 完全重新渲染(首次加载或无法找到特定卡片)
timeline.innerHTML = "";
sortedCommits.forEach((commit) => {
const commitRow = renderCommitCard(commit, currentLayout);
// 只在首次加载时添加动画类
if (isInitialLoad) {
commitRow.classList.add("initial-load");
}
timeline.appendChild(commitRow);
});
updateCherryPickCommands();
console.log("提交列表渲染完成");
}
/**
* 渲染单个提交卡片
* @param {Object} commit - 提交对象
* @param {string} layout - 布局模式
* @returns {HTMLElement} 提交行元素
*/
function renderCommitCard(commit, layout) {
const isIgnored = appState.ignoredCommits.some(
(item) => item.hash === commit.hash
);
const ignoredCommit = appState.ignoredCommits.find(
(item) => item.hash === commit.hash
);
const hasRemarkForCommit = hasRemark(commit.hash);
const remarkContent = getRemarkContent(commit.hash);
const isMatchedByMessage = commit.matchedByMessage === true;
const commitRow = document.createElement("div");
commitRow.className = "commit-row";
const commitContainer = document.createElement("div");
if (commit.status === "both") {
commitContainer.className = "commit-container both";
} else if (commit.status === "source") {
commitContainer.className = "commit-container source";
} else {
commitContainer.className = "commit-container target";
}
let badgeText;
let badgeClass = "";
if (commit.status === "both") {
badgeText = isMatchedByMessage ? "已同步(commit 消息匹配)" : "共同提交";
if (isMatchedByMessage) {
badgeClass = "message-matched";
}
} else if (commit.status === "source") {
badgeText = appState.sourceBranch;
} else {
badgeText = appState.targetBranch;
}
let commitClasses = `commit${isIgnored ? " ignored" : ""}`;
if (isMatchedByMessage) {
commitClasses += " matched-by-message";
}
let hashContent = "";
const vscodeIconSvg = `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M21.29 4.1L17.47.28a1 1 0 0 0-.71-.28h-.09a1 1 0 0 0-.67.3l-10.48 9.5-4.23-3.2a.67.67 0 0 0-.89.01l-.8.72a.67.67 0 0 0 0 .99l3.45 3.12L.6 14.56a.67.67 0 0 0 0 .99l.8.72a.67.67 0 0 0 .89.01l4.23-3.2 10.48 9.5a1 1 0 0 0 .67.3h.09a1 1 0 0 0 .71-.28l3.82-3.82a1 1 0 0 0 .3-.71V4.81a1 1 0 0 0-.3-.71zM17 18.5l-9-6.75L17 5v13.5z"/></svg>`;
function getHashWithButtons(hash) {
const shortHash = hash.substring(0, 7);
return `
<span class="commit-hash" title="${hash}">${shortHash}</span>
<button class="vscode-button" title="在VS Code中查看">
${vscodeIconSvg}
</button>
<button class="view-changes-btn">
查看变更
</button>
`;
}
if (isMatchedByMessage && commit.targetHash) {
const cherryPickTimeHtml = commit.cherryPickTime
? `
<div class="cherry-pick-time">
<span class="cherry-pick-label">🍒 Cherry-pick 时间:</span>
<span class="cherry-pick-timestamp">${formatDate(
commit.cherryPickTime
)}</span>
</div>
`
: "";
hashContent = `
<div class="commit-hash-pair">
<div class="commit-hash-branch">
<span class="branch-label">${appState.sourceBranch}:</span>
${getHashWithButtons(commit.hash)}
</div>
<div class="commit-hash-branch">
<span class="branch-label">${appState.targetBranch}:</span>
${getHashWithButtons(commit.targetHash)}
</div>
${cherryPickTimeHtml}
<button class="compare-in-vscode">
${vscodeIconSvg} 在VS Code中比较两个提交
</button>
</div>
`;
} else {
hashContent = getHashWithButtons(commit.hash);
}
const branchIndicator =
layout === "flat"
? `<div class="branch-indicator ${badgeClass}"></div>`
: "";
commitContainer.innerHTML = `
<div class="${commitClasses}" data-hash="${commit.hash}" data-status="${
commit.status
}">
${branchIndicator}
<span class="commit-badge ${badgeClass}">${badgeText}</span>
<div class="commit-time">${commit.formattedDate}</div>
<div class="commit-info">
<div class="commit-message">${escapeHtml(commit.message)}</div>
${
commit.body && commit.body.trim()
? `<div class="commit-body">${escapeHtml(commit.body)}</div>`
: ""
}
<div class="commit-details">
<span class="commit-author">作者: ${escapeHtml(
commit.authorName
)}</span>
${hashContent}
<button class="ignore-button">
${isIgnored ? "取消忽略" : "忽略"}
</button>
<button class="remark-button">
${hasRemarkForCommit ? "编辑备注" : "添加备注"}
</button>
</div>
${
isIgnored && ignoredCommit
? `
<div class="ignore-reason">
<span class="ignore-reason-title">🚫 忽略原因</span>
<span class="ignore-reason-content">${escapeHtml(
ignoredCommit.reason
)}</span>
</div>`
: ""
}
${
remarkContent
? `
<div class="commit-remark">
<span class="commit-remark-title">📝 备注</span>
<span class="commit-remark-content">${escapeHtml(
remarkContent
)}</span>
</div>`
: ""
}
</div>
</div>
`;
commitRow.appendChild(commitContainer);
return commitRow;
}
/**
* 更新cherry-pick命令
*/
function updateCherryPickCommands() {
const sourceOnlyCommits = appState.commits.filter(
(commit) =>
commit.status === "source" &&
!appState.ignoredCommits.some((item) => item.hash === commit.hash)
);
const targetOnlyCommits = appState.commits.filter(
(commit) =>
commit.status === "target" &&
!appState.ignoredCommits.some((item) => item.hash === commit.hash)
);
updateDirectionalCommands(
sourceOnlyCommits,
appState.sourceBranch,
appState.targetBranch,
"source-to-target"
);
updateDirectionalCommands(
targetOnlyCommits,
appState.targetBranch,
appState.sourceBranch,
"target-to-source"
);
}
/**
* 生成特定方向的cherry-pick命令
* @param {Array} directionCommits - 提交列表
* @param {string} fromBranch - 源分支
* @param {string} toBranch - 目标分支
* @param {string} direction - 方向标识
*/
function updateDirectionalCommands(
directionCommits,
fromBranch,
toBranch,
direction
) {
directionCommits.sort((a, b) => new Date(b.date) - new Date(a.date));
let commandsArray = [];
commandsArray.push(`# 切换到 ${toBranch} 分支`);
commandsArray.push(`git checkout ${toBranch}`);
commandsArray.push("");
commandsArray.push(`# Cherry-pick 从 ${fromBranch} 分支的提交`);
directionCommits.forEach((commit) => {
commandsArray.push(`git cherry-pick ${commit.hash} # ${commit.message}`);
});
const commands = commandsArray.join("\n");
const commandsContainer = document.getElementById(
`${direction}-commands-container`
);
commandsContainer.innerHTML = "";
commandsArray.forEach((cmd) => {
// 跳过空行,不渲染
if (!cmd || cmd.trim() === "") {
return;
}
const cmdLine = document.createElement("div");
cmdLine.className = "command-line";
const cmdText = document.createElement("pre");
cmdText.innerHTML = highlightGitCommand(cmd);
cmdLine.appendChild(cmdText);
if (cmd && !cmd.startsWith("#")) {
const copyBtn = document.createElement("button");
copyBtn.className = "copy-line-button";
copyBtn.textContent = "复制";
copyBtn.onclick = function () {
copyToClipboard(cmd, this);
};
cmdLine.appendChild(copyBtn);
}
commandsContainer.appendChild(cmdLine);
});
document.getElementById(`${direction}-commands`).textContent = commands;
}
/**
* Git 命令语法高亮
* @param {string} command - Git 命令字符串
* @returns {string} 高亮后的 HTML
*/
function highlightGitCommand(command) {
// 如果是注释行
if (command.startsWith("#")) {
return `<span class="git-comment">${escapeHtml(command)}</span>`;
}
// 分离命令和注释
const commentIndex = command.indexOf("#");
let cmdPart = command;
let commentPart = "";
if (commentIndex !== -1) {
cmdPart = command.substring(0, commentIndex);
commentPart = command.substring(commentIndex);
}
// 转义 HTML 特殊字符
cmdPart = escapeHtml(cmdPart);
// 高亮 git 关键字
cmdPart = cmdPart.replace(
/\b(git)\b/g,
'<span class="git-keyword">$1</span>'
);
// 高亮子命令 (checkout, cherry-pick, etc.)
cmdPart = cmdPart.replace(
/\b(checkout|cherry-pick|commit|push|pull|merge|rebase|branch|status|log|diff|add|reset|stash)\b/g,
'<span class="git-subcommand">$1</span>'
);
// 高亮哈希值 (7位或更长的十六进制字符串)
cmdPart = cmdPart.replace(
/\b([0-9a-f]{7,40})\b/g,
'<span class="git-hash">$1</span>'
);
// 添加注释部分
if (commentPart) {
cmdPart += ` <span class="git-comment">${escapeHtml(commentPart)}</span>`;
}
return cmdPart;
}
/**
* 更新备注UI
* @param {string} hash - 提交哈希
*/
function updateRemarkUI(hash) {
const remarkElement = document.querySelector(
`.remark-content[data-hash="${hash}"]`
);
if (!remarkElement) return;
const remark = getCommitRemark(hash);
if (remark && remark.content) {
remarkElement.value = remark.content;
remarkElement.classList.add("has-content");
} else {
remarkElement.value = "";
remarkElement.classList.remove("has-content");
}
}
/**
* 更新所有备注UI
*/
function updateAllRemarkUI() {
document.querySelectorAll(".commit").forEach((commitEl) => {
const hash = commitEl.getAttribute("data-hash");
if (hash) {
updateRemarkUI(hash);
}
});
}
/**
* 获取备注内容
* @param {string} hash - 提交哈希
* @returns {string} 备注内容
*/
function getRemarkContent(hash) {
const remark = appState.commitRemarks.find((r) => r.hash === hash);
return remark ? remark.content : "";
}
/**
* 判断是否有备注
* @param {string} hash - 提交哈希
* @returns {boolean} 是否有备注
*/
function hasRemark(hash) {
return appState.commitRemarks.some((r) => r.hash === hash);
}
/**
* 获取提交的备注对象
* @param {string} hash - 提交哈希
* @returns {Object|undefined} 备注对象
*/
function getCommitRemark(hash) {
return appState.commitRemarks.find((r) => r.hash === hash);
}