// Escape repo-derived strings (branch/file names, commit messages, git stderr) before // interpolating them into innerHTML. Quotes are included so attribute contexts // (data-file, data-branch) cannot be broken out of. function escapeHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function renderGitPanel( container, { gitStatus, gitDiff, gitStage, gitCommit, gitLog, gitBranches, gitCheckout, gitPush, gitPull } ) { container.innerHTML = `

Branches

Loading...

Changes

Loading...

Commit

Recent Commits

`; loadGitStatus(); loadGitBranches(); async function loadGitStatus() { const status = await gitStatus(); const changesEl = document.getElementById('git-changes'); if (!status || !changesEl) return; if (status.error) { changesEl.innerHTML = `

${escapeHtml(status.error)}

`; return; } const files = [ ...status.modified.map((f) => ({ file: f, status: 'M', color: '#f59e0b' })), ...status.not_added.map((f) => ({ file: f, status: '?', color: '#6b7280' })), ...status.created.map((f) => ({ file: f, status: 'A', color: '#10b981' })), ...status.deleted.map((f) => ({ file: f, status: 'D', color: '#ef4444' })), ...status.staged.map((f) => ({ file: f, status: 'S', color: '#3b82f6' })), ]; if (files.length === 0) { changesEl.innerHTML = '

No changes

'; } else { changesEl.innerHTML = files .map( (f) => `
${f.status} ${escapeHtml(f.file)}
` ) .join(''); changesEl.querySelectorAll('.git-stage-btn').forEach((btn) => { btn.addEventListener('click', async (e) => { e.stopPropagation(); await gitStage([btn.dataset.file]); loadGitStatus(); }); }); changesEl.querySelectorAll('.git-diff-btn').forEach((btn) => { btn.addEventListener('click', async (e) => { e.stopPropagation(); await showDiff(btn.dataset.file); }); }); } // Load log const log = await gitLog(); const logEl = document.getElementById('git-log'); if (log && logEl) { logEl.innerHTML = (log.all || []) .slice(0, 10) .map( (entry) => `
${escapeHtml(entry.message)}
${entry.date?.substring(0, 10) || ''} · ${escapeHtml(entry.author_name || '')}
` ) .join('') || '

No commits

'; } } async function showDiff(file) { const diffView = document.getElementById('git-diff-view'); if (!diffView || !gitDiff) return; const result = await gitDiff(file); diffView.textContent = result && result.error ? result.error : result && result.length ? result : 'No changes'; diffView.style.display = 'block'; } async function loadGitBranches() { const branchesEl = document.getElementById('git-branches'); if (!gitBranches || !branchesEl) return; const result = await gitBranches(); if (!result) return; if (result.error) { branchesEl.innerHTML = `

${escapeHtml(result.error)}

`; return; } const names = result.all || []; const current = result.current; if (names.length === 0) { branchesEl.innerHTML = '

No branches

'; return; } branchesEl.innerHTML = names .map( (name) => `
${name === current ? '● ' : ''}${escapeHtml(name)} ${name === current ? '' : ``}
` ) .join(''); branchesEl.querySelectorAll('.git-checkout-btn').forEach((btn) => { btn.addEventListener('click', async (e) => { e.stopPropagation(); const result = await gitCheckout(btn.dataset.branch, false); if (result && result.error) { const statusEl = document.getElementById('git-remote-status'); if (statusEl) statusEl.textContent = `Checkout failed: ${result.error}`; } loadGitBranches(); loadGitStatus(); }); }); } document.getElementById('git-commit-btn')?.addEventListener('click', async () => { const msg = document.getElementById('git-commit-msg')?.value?.trim(); if (!msg) return; await gitCommit(msg); document.getElementById('git-commit-msg').value = ''; loadGitStatus(); }); document.getElementById('git-branch-create-btn')?.addEventListener('click', async () => { const input = document.getElementById('git-branch-input'); const name = input?.value?.trim(); if (!name || !gitCheckout) return; const result = await gitCheckout(name, true); const statusEl = document.getElementById('git-remote-status'); if (result && result.error) { if (statusEl) statusEl.textContent = `Create branch failed: ${result.error}`; } else { input.value = ''; if (statusEl) statusEl.textContent = ''; } loadGitBranches(); loadGitStatus(); }); document.getElementById('git-push-btn')?.addEventListener('click', async () => { const statusEl = document.getElementById('git-remote-status'); if (!gitPush) return; const result = await gitPush(); if (statusEl) { statusEl.textContent = result && result.error ? `Push failed: ${result.error}` : 'Push complete'; } }); document.getElementById('git-pull-btn')?.addEventListener('click', async () => { const statusEl = document.getElementById('git-remote-status'); if (!gitPull) return; const result = await gitPull(); if (statusEl) { statusEl.textContent = result && result.error ? `Pull failed: ${result.error}` : 'Pull complete'; } loadGitStatus(); loadGitBranches(); }); } module.exports = { renderGitPanel };