#!/usr/bin/env python3 # Generates the git history UI JavaScript and appends to app.js # Using chr() for quote characters to avoid shell/heredoc quoting issues sq = chr(39) # single quote dq = chr(34) # double quote nl = chr(10) # newline bs = chr(92) # backslash lines = [ '', '// --- Git 版本历史 UI ---', '', '/** 打开 Git 版本历史面板 */', 'async function showGitHistory() {', ' if (!_currentProjectId) return;', ' const overlay = document.createElement(' + sq + 'div' + sq + ');', ' overlay.className = ' + sq + 'modal-overlay' + sq + ';', ' overlay.id = ' + sq + 'gitHistoryOverlay' + sq + ';', ' overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };', ' overlay.innerHTML = `', ' `;', ' document.body.appendChild(overlay);', ' await _loadGitLog();', '}', '', '// 当前选中的 commit hash(用于回滚)', 'let _selectedCommitHash = null;', '', '/** 加载提交历史列表 */', 'async function _loadGitLog() {', ' const listEl = document.getElementById(' + sq + 'gitLogList' + sq + ');', ' if (!listEl) return;', ' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/log?id=' + sq + ' + _currentProjectId + ' + sq + '&limit=30' + sq + ');', ' if (status !== 200 || !data.commits) {', ' listEl.innerHTML = ' + sq + '

加载失败

' + sq + ';', ' return;', ' }', ' const commits = data.commits;', ' if (commits.length === 0) {', ' listEl.innerHTML = ' + sq + '

暂无提交记录

' + sq + ';', ' return;', ' }', ' listEl.innerHTML = commits.map((c, idx) => `', '
', '
', '
', '
${_escHtml(c.message)}
', '
', ' ${c.hash}', '  ${_fmtDate(c.date)} · ${_escHtml(c.author)}', '
', '
', ' ${idx === 0 ? ' + sq + '最新' + sq + ' : ' + sq + sq + '}', '
`).join(' + sq + sq + ');', '}', '', '/** 展示某次提交的 diff */', 'async function _showCommitDiff(hash, message) {', ' _selectedCommitHash = hash;', ' const diffArea = document.getElementById(' + sq + 'gitDiffArea' + sq + ');', ' const diffContent = document.getElementById(' + sq + 'gitDiffContent' + sq + ');', ' const diffTitle = document.getElementById(' + sq + 'gitDiffTitle' + sq + ');', ' if (!diffArea) return;', ' diffTitle.textContent = hash + ' + sq + ' ' + sq + ' + (message || ' + sq + sq + ');', ' diffContent.textContent = ' + sq + '加载中...' + sq + ';', ' diffArea.style.display = ' + sq + 'block' + sq + ';', ' // 高亮选中行', ' document.querySelectorAll(' + sq + '.git-commit-item' + sq + ').forEach(el => {', ' el.style.background = el.dataset.hash === hash ? ' + sq + 'var(--bg-3)' + sq + ' : ' + sq + 'var(--bg-2)' + sq + ';', ' });', ' const { status, data } = await api(' + sq + 'GET' + sq + ', ' + sq + '/v1/projects/git/diff?id=' + sq + ' + _currentProjectId + ' + sq + '&hash=' + sq + ' + encodeURIComponent(hash));', ' if (status !== 200) { diffContent.textContent = ' + sq + '获取 diff 失败' + sq + '; return; }', ' const rawText = (data.stat || ' + sq + sq + ') + ' + sq + bs + 'n' + bs + 'n' + sq + ' + (data.diff || ' + sq + '(无变更)' + sq + ');', ' // 简单着色: + / -', ' diffContent.innerHTML = rawText', ' .split(' + sq + bs + 'n' + sq + ')', ' .map(ln => {', ' const safe = _escHtml(ln);', ' if (ln.startsWith(' + sq + '+++' + sq + ') || ln.startsWith(' + sq + '---' + sq + ')) return ' + sq + '' + sq + ' + safe + ' + sq + '' + sq + ';', ' if (ln.startsWith(' + sq + '+' + sq + ')) return ' + sq + '' + sq + ' + safe + ' + sq + '' + sq + ';', ' if (ln.startsWith(' + sq + '-' + sq + ')) return ' + sq + '' + sq + ' + safe + ' + sq + '' + sq + ';', ' return safe;', ' })', ' .join(' + sq + bs + 'n' + sq + ');', '}', '', '/** 确认并执行回滚 */', 'async function confirmRevert() {', ' if (!_selectedCommitHash || !_currentProjectId) return;', ' const h = _selectedCommitHash;', ' if (!confirm(' + sq + '确定回滚到版本 ' + sq + ' + h + ' + sq + ' 吗?当前未保存的变更将被覆盖。此操作产生新提交,不破坏历史记录。' + sq + ')) return;', ' const btn = document.getElementById(' + sq + 'gitRevertBtn' + sq + ');', ' if (btn) { btn.disabled = true; btn.textContent = ' + sq + '回滚中...' + sq + '; }', ' const { status, data } = await api(' + sq + 'POST' + sq + ', ' + sq + '/v1/projects/git/revert' + sq + ', { projectId: _currentProjectId, hash: h });', ' if (btn) { btn.disabled = false; btn.textContent = ' + sq + '回滚到此版本' + sq + '; }', ' if (status === 200) {', ' toast(' + sq + '已回滚到版本 ' + sq + ' + h, ' + sq + 'success' + sq + ');', ' document.getElementById(' + sq + 'gitHistoryOverlay' + sq + ')?.remove();', ' if (typeof openProject === ' + sq + 'function' + sq + ') openProject(_currentProjectId);', ' } else {', ' toast((data && data.error) || ' + sq + '回滚失败' + sq + ', ' + sq + 'error' + sq + ');', ' }', '}', '', '/** HTML 转义辅助 */', 'function _escHtml(s) {', ' return String(s || ' + sq + sq + ')', ' .replace(/&/g, ' + sq + '&' + sq + ')', ' .replace(//g, ' + sq + '>' + sq + ')', ' .replace(/"/g, ' + sq + '"' + sq + ');', '}', '', '/** 格式化 ISO 日期为简短可读格式 */', 'function _fmtDate(iso) {', ' try {', ' const d = new Date(iso);', ' const now = new Date();', ' const diff = (now - d) / 1000;', ' if (diff < 60) return ' + sq + '刚刚' + sq + ';', ' if (diff < 3600) return Math.floor(diff / 60) + ' + sq + ' 分钟前' + sq + ';', ' if (diff < 86400) return Math.floor(diff / 3600) + ' + sq + ' 小时前' + sq + ';', ' if (diff < 2592000) return Math.floor(diff / 86400) + ' + sq + ' 天前' + sq + ';', ' return d.toLocaleDateString(' + sq + 'zh-CN' + sq + ', { year: ' + sq + 'numeric' + sq + ', month: ' + sq + '2-digit' + sq + ', day: ' + sq + '2-digit' + sq + ' });', ' } catch(e) { return iso || ' + sq + sq + '; }', '}', '', ] content = nl.join(lines) + nl import tempfile, os out_path = os.path.join(tempfile.gettempdir(), 'git_ui_append.js') with open(out_path, 'w', encoding='utf-8') as f: f.write(content) print('Generated %s (%d bytes, %d lines)' % (out_path, len(content), content.count(nl)))