branch-commit-compare
Version:
Git branch comparison tool
375 lines (344 loc) • 10.6 kB
JavaScript
/**
* Utility Functions 工具函数
* 通用的辅助函数
*/
/**
* 复制命令块
* @param {string} direction - 方向标识 ('source-to-target' 或 'target-to-source')
*/
function copyCommandBlock(direction) {
const commandsElement = document.getElementById(`${direction}-commands`);
if (commandsElement) {
const text = commandsElement.textContent;
const button = document.querySelector(`.copy-${direction}-button`);
copyToClipboard(text, button);
}
}
/**
* 复制文本到剪贴板
* @param {string} text - 要复制的文本
* @param {HTMLElement} element - 触发复制的元素(用于显示提示)
*/
function copyToClipboard(text, element) {
navigator.clipboard
.writeText(text)
.then(() => {
// 创建Neumorphism风格的toast提示
const toast = document.createElement("div");
toast.className = "copy-toast";
toast.innerHTML = `
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="flex-shrink: 0;">
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="2"/>
<path d="M6 10L9 13L14 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>已复制到剪贴板</span>
`;
// 设置样式
toast.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
background: var(--color-bg);
color: var(--color-success);
padding: var(--spacing-4) var(--spacing-5);
border-radius: var(--radius-base);
box-shadow:
8px 8px 16px rgba(163, 177, 198, 0.5),
-8px -8px 16px rgba(255, 255, 255, 0.8),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
font-family: var(--font-body);
z-index: 10000;
display: flex;
align-items: center;
gap: var(--spacing-3);
opacity: 0;
transform: translateY(20px) scale(0.9);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
`;
document.body.appendChild(toast);
// 触发动画
requestAnimationFrame(() => {
requestAnimationFrame(() => {
toast.style.opacity = "1";
toast.style.transform = "translateY(0) scale(1)";
});
});
// 2秒后淡出并移除
setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateY(-20px) scale(0.9)";
setTimeout(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
}, 300);
}, 2000);
})
.catch((err) => {
console.error("复制失败:", err);
// 显示错误提示
const errorToast = document.createElement("div");
errorToast.className = "copy-toast error";
errorToast.innerHTML = `
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" style="flex-shrink: 0;">
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="2"/>
<path d="M10 6V11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="10" cy="14" r="1" fill="currentColor"/>
</svg>
<span>复制失败</span>
`;
errorToast.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
background: var(--color-bg);
color: var(--color-danger);
padding: var(--spacing-4) var(--spacing-5);
border-radius: var(--radius-base);
box-shadow:
8px 8px 16px rgba(163, 177, 198, 0.5),
-8px -8px 16px rgba(255, 255, 255, 0.8),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
font-family: var(--font-body);
z-index: 10000;
display: flex;
align-items: center;
gap: var(--spacing-3);
opacity: 0;
transform: translateY(20px) scale(0.9);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
pointer-events: none;
`;
document.body.appendChild(errorToast);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
errorToast.style.opacity = "1";
errorToast.style.transform = "translateY(0) scale(1)";
});
});
setTimeout(() => {
errorToast.style.opacity = "0";
errorToast.style.transform = "translateY(-20px) scale(0.9)";
setTimeout(() => {
if (errorToast.parentNode) {
document.body.removeChild(errorToast);
}
}, 300);
}, 2000);
});
}
/**
* HTML转义,防止XSS攻击
* @param {string} str - 要转义的字符串
* @returns {string} 转义后的字符串
*/
function escapeHtml(str) {
if (!str) return "";
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* 格式化日期
* @param {string|Date} date - 日期对象或字符串
* @returns {string} 格式化后的日期字符串
*/
function formatDate(date) {
const d = new Date(date);
return d.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
/**
* 从diff --git行提取文件路径
* @param {string} diffGitLine - diff --git行
* @returns {string} 文件路径
*/
function extractFilePath(diffGitLine) {
try {
const parts = diffGitLine.split(" ");
if (parts.length >= 4) {
let path = parts[3];
if (path.startsWith("b/")) {
path = path.substring(2);
}
return path;
}
} catch (e) {
console.error("提取文件路径失败:", e);
}
return "unknown-file";
}
/**
* 将Git差异文本解析为文件块
* @param {string} diffText - Git diff文本
* @returns {Array} 文件块数组
*/
function parseDiffToFileBlocks(diffText) {
const lines = diffText.split("\n");
const fileBlocks = [];
let currentFile = null;
let inFileHeader = false;
let currentHunk = null;
let currentHunkContent = [];
lines.forEach((line) => {
if (line.startsWith("diff --git ")) {
if (currentFile) {
if (currentHunk) {
currentFile.hunks.push({
header: currentHunk,
content: currentHunkContent,
});
}
fileBlocks.push(currentFile);
}
currentFile = {
header: line,
path: extractFilePath(line),
hunks: [],
};
inFileHeader = true;
currentHunk = null;
currentHunkContent = [];
} else if (
inFileHeader &&
(line.startsWith("--- ") || line.startsWith("+++ "))
) {
if (currentFile) {
currentFile.header += "\n" + line;
}
} else if (line.startsWith("@@ ")) {
inFileHeader = false;
if (currentHunk && currentFile) {
currentFile.hunks.push({
header: currentHunk,
content: currentHunkContent,
});
}
currentHunk = line;
currentHunkContent = [];
} else if (currentFile) {
if (inFileHeader) {
currentFile.header += "\n" + line;
} else if (currentHunk) {
currentHunkContent.push(line);
}
}
});
if (currentFile && currentHunk) {
currentFile.hunks.push({
header: currentHunk,
content: currentHunkContent,
});
fileBlocks.push(currentFile);
}
return fileBlocks;
}
/**
* 创建GitHub风格的差异HTML
* @param {Object} fileBlock - 文件块对象
* @returns {string} HTML字符串
*/
function createGitHubStyleDiffHTML(fileBlock) {
const filePath = fileBlock.path;
let html = `
<div style="margin-bottom: 16px;">
<div style="background-color: #f6f8fa; border: 1px solid #e1e4e8; border-bottom: none; border-top-left-radius: 6px; border-top-right-radius: 6px; padding: 8px 16px; font-weight: 600;">
${escapeHtml(filePath)}
</div>
`;
fileBlock.hunks.forEach((hunk, index) => {
const isLast = index === fileBlock.hunks.length - 1;
html += `
<div style="background-color: #f8fafd; color: #586069; padding: 4px 10px; border: 1px solid #e1e4e8; border-bottom: none; font-size: 12px;">
${escapeHtml(hunk.header)}
</div>
<div style="border: 1px solid #e1e4e8; border-bottom-left-radius: ${
isLast ? "6px" : "0"
}; border-bottom-right-radius: ${
isLast ? "6px" : "0"
}; overflow: hidden;">
<table style="border-collapse: collapse; width: 100%; tab-size: 4;">
<tbody>
`;
hunk.content.forEach((line) => {
let lineClass = "";
let lineStyle = "";
let prefix = "";
if (line.startsWith("+")) {
lineClass = "addition";
lineStyle = "background-color: #e6ffed;";
prefix = "+";
} else if (line.startsWith("-")) {
lineClass = "deletion";
lineStyle = "background-color: #ffeef0;";
prefix = "-";
} else {
lineClass = "context";
lineStyle = "";
prefix = " ";
}
const lineContent = line.substring(1);
html += `
<tr class="${lineClass}" style="${lineStyle}">
<td style="width: 1%; padding: 0 10px; text-align: right; color: #bbb; border-right: 1px solid #e1e4e8; user-select: none;">${prefix}</td>
<td style="padding: 0 10px; white-space: pre-wrap; word-break: break-all;">${escapeHtml(
lineContent
)}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
});
html += `</div>`;
return html;
}
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* 节流函数
* @param {Function} func - 要节流的函数
* @param {number} limit - 时间限制(毫秒)
* @returns {Function} 节流后的函数
*/
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}