git-sync-web 自动提交 2026-09-09 15:06:45
This commit is contained in:
+146
-7
@@ -126,6 +126,8 @@ const Editor = {
|
||||
onSave: null, // 由 cm-boot.js 的快捷键触发
|
||||
onChange: null, // 由 cm-boot.js 的 change 事件触发(草稿缓存)
|
||||
};
|
||||
// 顶层 const 不会挂到 window,显式暴露给 cm-boot.js 使用
|
||||
window.Editor = Editor;
|
||||
|
||||
// ---------- 渲染 ----------
|
||||
function render() {
|
||||
@@ -329,6 +331,7 @@ function openDetail(name) {
|
||||
$('#m-title').textContent = name;
|
||||
$('#f-name').disabled = true;
|
||||
$('#btn-delete').hidden = false;
|
||||
$('#btn-clone').hidden = false;
|
||||
fillForm(r);
|
||||
showTab('log');
|
||||
$('#modal').hidden = false;
|
||||
@@ -340,6 +343,7 @@ function openCreate() {
|
||||
$('#m-title').textContent = '新建配置';
|
||||
$('#f-name').disabled = false;
|
||||
$('#btn-delete').hidden = true;
|
||||
$('#btn-clone').hidden = true;
|
||||
fillForm(null);
|
||||
showTab('config');
|
||||
$('#modal').hidden = false;
|
||||
@@ -445,12 +449,86 @@ function loadConflicts(name) {
|
||||
list.innerHTML = '<p style="color:var(--muted)">当前没有冲突。</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = files.map(f =>
|
||||
`<div class="conflict-file"><h4>${esc(f.file)}</h4><pre>${esc(f.diff) || '(无差异内容)'}</pre></div>`
|
||||
).join('');
|
||||
list.innerHTML = files.map(f => `
|
||||
<div class="conflict-file" data-path="${esc(f.file)}">
|
||||
<div class="cf-head">
|
||||
<h4 title="${esc(f.file)}">${esc(f.file)}</h4>
|
||||
<button class="btn btn-sm" data-cf="edit">编辑解决</button>
|
||||
<button class="btn btn-sm" data-cf="close" hidden>收起</button>
|
||||
</div>
|
||||
<div class="cf-edit" hidden>
|
||||
<textarea class="cf-ta" spellcheck="false" placeholder="加载中…"></textarea>
|
||||
<div class="cf-actions">
|
||||
<input class="file-msg cf-msg" placeholder="提交说明(可选)">
|
||||
<button class="btn btn-sm" data-cf="save">仅保存</button>
|
||||
<button class="btn btn-sm btn-primary" data-cf="resolve">保存并标记已解决</button>
|
||||
</div>
|
||||
<div class="cf-hint">「仅保存」只写回磁盘;「保存并标记已解决」会暂存该文件,并在已无其它冲突文件时结束合并/变基。</div>
|
||||
</div>
|
||||
<pre>${esc(f.diff) || '(无差异内容)'}</pre>
|
||||
</div>`).join('');
|
||||
}).catch(err => { $('#conflict-list').innerHTML = `<p style="color:var(--red)">${esc(err.message)}</p>`; });
|
||||
}
|
||||
|
||||
// 冲突文件内联编辑:展开编辑区 → 读取原文 → 保存 / 保存并标记已解决
|
||||
$('#conflict-list').addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button[data-cf]');
|
||||
if (!btn || !current) return;
|
||||
const item = btn.closest('.conflict-file');
|
||||
const path = item.dataset.path;
|
||||
const act = btn.dataset.cf;
|
||||
const wrap = item.querySelector('.cf-edit');
|
||||
const ta = item.querySelector('.cf-ta');
|
||||
const editBtn = item.querySelector('[data-cf="edit"]');
|
||||
const closeBtn = item.querySelector('[data-cf="close"]');
|
||||
|
||||
if (act === 'edit') {
|
||||
wrap.hidden = false;
|
||||
editBtn.hidden = true;
|
||||
closeBtn.hidden = false;
|
||||
ta.disabled = false;
|
||||
ta.value = '加载中…';
|
||||
try {
|
||||
const d = await api(`/api/repos/${current}/file?path=${encodeURIComponent(path)}`);
|
||||
if (d.binary) {
|
||||
ta.value = `(二进制文件,大小 ${d.size} 字节,无法在网页编辑)`;
|
||||
ta.disabled = true;
|
||||
} else {
|
||||
ta.value = d.content || '';
|
||||
}
|
||||
} catch (err) {
|
||||
ta.value = '读取失败:' + err.message;
|
||||
ta.disabled = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (act === 'close') {
|
||||
wrap.hidden = true;
|
||||
editBtn.hidden = false;
|
||||
closeBtn.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// save / resolve
|
||||
if (ta.disabled) { toast('该文件当前不可编辑', 'error'); return; }
|
||||
try {
|
||||
await api(`/api/repos/${current}/resolve-file`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path,
|
||||
content: ta.value,
|
||||
finish: act === 'resolve',
|
||||
})
|
||||
});
|
||||
toast(act === 'resolve' ? '已保存并结束合并/变基' : '已保存到磁盘(尚未标记解决)', 'success');
|
||||
loadConflicts(current);
|
||||
updateConflictActions();
|
||||
refresh();
|
||||
} catch (err) { toast(err.message, 'error'); }
|
||||
});
|
||||
|
||||
async function doAction(action, okMsg) {
|
||||
try {
|
||||
await api(`/api/repos/${current}/action`, {
|
||||
@@ -562,6 +640,7 @@ function parseUnifiedDiff(text) {
|
||||
if (line.startsWith('@@')) {
|
||||
const m = /@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
||||
if (m) { oldNo = parseInt(m[1], 10); newNo = parseInt(m[2], 10); }
|
||||
inHunk = true;
|
||||
cur.rows.push({ type: 'hunk', text: line });
|
||||
continue;
|
||||
}
|
||||
@@ -573,9 +652,15 @@ function parseUnifiedDiff(text) {
|
||||
}
|
||||
if (line.startsWith('+')) { cur.rows.push({ type: 'add', text: line.slice(1), no: newNo++ }); continue; }
|
||||
if (line.startsWith('-')) { cur.rows.push({ type: 'del', text: line.slice(1), no: oldNo++ }); continue; }
|
||||
if (line.startsWith(' ')) { cur.rows.push({ type: 'ctx', text: line.slice(1), no: oldNo++ }); newNo++; continue; }
|
||||
if (line === '') { cur.rows.push({ type: 'ctx', text: '', no: oldNo++ }); newNo++; continue; }
|
||||
cur.rows.push({ type: 'meta', text: line }); // \ No newline 等
|
||||
if (line.startsWith(' ')) { cur.rows.push({ type: 'ctx', text: line.slice(1), no: oldNo++, nNo: newNo++ }); continue; }
|
||||
if (line === '') { cur.rows.push({ type: 'ctx', text: '', no: oldNo++, nNo: newNo++ }); continue; }
|
||||
// 词级模式(--word-diff=plain)下 git 的上下文行不带前导空格,
|
||||
// 因此 hunk 内未识别的行一律按上下文处理;hunk 外的(如 "\ No newline")按元信息处理。
|
||||
if (inHunk && !line.startsWith('\\')) {
|
||||
cur.rows.push({ type: 'ctx', text: line, no: oldNo++, nNo: newNo++ });
|
||||
continue;
|
||||
}
|
||||
cur.rows.push({ type: 'meta', text: line });
|
||||
}
|
||||
return files;
|
||||
}
|
||||
@@ -605,7 +690,12 @@ function buildSplitRows(rows) {
|
||||
while (i < rows.length) {
|
||||
const r = rows[i];
|
||||
if (r.type === 'hunk' || r.type === 'meta') { out.push({ kind: r.type, text: r.text }); i++; continue; }
|
||||
if (r.type === 'ctx') { out.push({ kind: 'ctx', left: r, right: r }); i++; continue; }
|
||||
if (r.type === 'ctx') {
|
||||
// 左右列行号分别取旧/新行号,故右侧用副本
|
||||
out.push({ kind: 'ctx', left: r, right: { text: r.text, no: r.nNo == null ? r.no : r.nNo } });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const dels = [], adds = [];
|
||||
while (i < rows.length && rows[i].type === 'del') dels.push(rows[i++]);
|
||||
while (i < rows.length && rows[i].type === 'add') adds.push(rows[i++]);
|
||||
@@ -817,6 +907,9 @@ async function saveFile(commit) {
|
||||
}
|
||||
|
||||
$('#f-diff-scope').addEventListener('change', () => { if (current && !$('#tab-diff').hidden) loadDiff(current); });
|
||||
// 并排/统一纯前端重渲染(同一份 diff 原文);词级需后端重新生成,故重新拉取
|
||||
$('#f-diff-view').addEventListener('change', () => { renderDiff($('#diffbox'), diffText || '(无差异)'); });
|
||||
$('#f-diff-word').addEventListener('change', () => { if (current && !$('#tab-diff').hidden) loadDiff(current); });
|
||||
$('#btn-diff-refresh').addEventListener('click', () => { if (current) loadDiff(current); });
|
||||
|
||||
// 差异:复制 / 下载 .patch
|
||||
@@ -993,19 +1086,65 @@ $('#btn-delete').addEventListener('click', async () => {
|
||||
} catch (err) { toast(err.message, 'error'); }
|
||||
});
|
||||
|
||||
// ---------- 配置「克隆」按钮(多仓库同构时省得重填) ----------
|
||||
// 复用 readForm 取当前表单值 → 切到新建态:名称自动加 -copy(去重),仓库路径清空
|
||||
$('#btn-clone').addEventListener('click', () => {
|
||||
if (!current) return;
|
||||
const base = readForm();
|
||||
const oldName = base.name || 'repo';
|
||||
creating = true;
|
||||
current = null;
|
||||
const stem = oldName.replace(/-copy\d*$/i, '');
|
||||
let name = stem + '-copy';
|
||||
const taken = new Set(repos.map(r => r.name));
|
||||
let i = 2;
|
||||
while (taken.has(name)) name = stem + '-copy' + (i++);
|
||||
$('#m-title').textContent = `克隆配置:${oldName}`;
|
||||
$('#f-name').disabled = false;
|
||||
$('#btn-delete').hidden = true;
|
||||
$('#btn-clone').hidden = true;
|
||||
fillForm(base);
|
||||
$('#f-name').value = name;
|
||||
$('#f-path').value = ''; // 路径必须换,否则会与源仓库指向同一目录
|
||||
showTab('config');
|
||||
toast('已载入原配置内容:请修改「配置名称」与「仓库路径」后保存(仓库路径已清空,需指向新目录)', 'info');
|
||||
$('#f-name').focus();
|
||||
});
|
||||
|
||||
// ---------- 顶部按钮 ----------
|
||||
$('#btn-new').addEventListener('click', openCreate);
|
||||
$('#btn-empty-new').addEventListener('click', openCreate);
|
||||
$('#btn-sync-all').addEventListener('click', () => {
|
||||
repos.filter(r => r.running).forEach(r => post(`/api/repos/${r.name}/sync`).catch(() => {}));
|
||||
toast(`已向 ${repos.filter(r => r.running).length} 个运行中的仓库发起同步`);
|
||||
});
|
||||
|
||||
// 批量启停:并发调用,汇总成功/失败
|
||||
async function batchToggle(act) {
|
||||
const isStart = act === 'start';
|
||||
const targets = repos.filter(r => isStart ? !r.running : r.running);
|
||||
if (!targets.length) {
|
||||
toast(isStart ? '没有处于暂停状态的仓库' : '没有正在运行的仓库');
|
||||
return;
|
||||
}
|
||||
let ok = 0, fail = 0;
|
||||
await Promise.all(targets.map(async (r) => {
|
||||
try { await post(`/api/repos/${r.name}/${act}`); ok++; }
|
||||
catch (_) { fail++; }
|
||||
}));
|
||||
toast(`${isStart ? '启动' : '暂停'}完成:成功 ${ok}${fail ? `,失败 ${fail}` : ''}`, fail ? 'warn' : 'success');
|
||||
refresh();
|
||||
}
|
||||
$('#btn-start-all').addEventListener('click', () => batchToggle('start'));
|
||||
$('#btn-stop-all').addEventListener('click', () => batchToggle('stop'));
|
||||
|
||||
// ---------- 暗色模式 ----------
|
||||
const THEME_KEY = 'gitautosync:theme';
|
||||
function applyTheme(dark) {
|
||||
document.documentElement.classList.toggle('dark', !!dark);
|
||||
const btn = $('#btn-theme');
|
||||
if (btn) btn.textContent = dark ? '☀️' : '🌙';
|
||||
Editor.setTheme(!!dark); // CodeMirror 主题跟随(未启用 CM 时为空操作)
|
||||
}
|
||||
function initTheme() {
|
||||
applyTheme(localStorage.getItem(THEME_KEY) === 'dark');
|
||||
|
||||
Reference in New Issue
Block a user