#!/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)))