// 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
Commit
`;
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 };