formatting
This commit is contained in:
parent
249c88da83
commit
d8918f8b36
2 changed files with 225 additions and 168 deletions
172
server.js
172
server.js
|
|
@ -1,35 +1,44 @@
|
||||||
import express from 'express';
|
import express from "express";
|
||||||
import { execSync, spawn } from 'child_process';
|
import { execSync, spawn } from "child_process";
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from "path";
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from "os";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from "url";
|
||||||
import { parsePatchFiles, DEFAULT_THEMES } from '@pierre/diffs';
|
import { parsePatchFiles, DEFAULT_THEMES } from "@pierre/diffs";
|
||||||
import { preloadFileDiff } from '@pierre/diffs/ssr';
|
import { preloadFileDiff } from "@pierre/diffs/ssr";
|
||||||
|
|
||||||
const __dirname = typeof __filename !== 'undefined'
|
const __dirname =
|
||||||
|
typeof __filename !== "undefined"
|
||||||
? dirname(__filename)
|
? dirname(__filename)
|
||||||
: dirname(fileURLToPath(import.meta.url));
|
: dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// --- arg parsing ---
|
// --- arg parsing ---
|
||||||
|
|
||||||
const outIdx = process.argv.indexOf('--out');
|
const outIdx = process.argv.indexOf("--out");
|
||||||
const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null;
|
const outFile = outIdx !== -1 ? process.argv[outIdx + 1] : null;
|
||||||
const isStatic = process.argv.includes('--static');
|
const isStatic = process.argv.includes("--static");
|
||||||
|
|
||||||
const positional = [];
|
const positional = [];
|
||||||
let skipNext = false;
|
let skipNext = false;
|
||||||
for (const arg of process.argv.slice(2)) {
|
for (const arg of process.argv.slice(2)) {
|
||||||
if (skipNext) { skipNext = false; continue; }
|
if (skipNext) {
|
||||||
if (arg === '--static') continue;
|
skipNext = false;
|
||||||
if (arg === '--out') { skipNext = true; continue; }
|
continue;
|
||||||
|
}
|
||||||
|
if (arg === "--static") continue;
|
||||||
|
if (arg === "--out") {
|
||||||
|
skipNext = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
positional.push(arg);
|
positional.push(arg);
|
||||||
}
|
}
|
||||||
const [projectDir, mainBranch, featureBranch, portArg] = positional;
|
const [projectDir, mainBranch, featureBranch, portArg] = positional;
|
||||||
const port = parseInt(portArg, 10) || 3000;
|
const port = parseInt(portArg, 10) || 3000;
|
||||||
|
|
||||||
if (!projectDir || !mainBranch || !featureBranch) {
|
if (!projectDir || !mainBranch || !featureBranch) {
|
||||||
console.error('Usage: difftool.sh [--static] [--out <file>] <git-project-dir> <main branch> <feature branch> [port]');
|
console.error(
|
||||||
|
"Usage: difftool.sh [--static] [--out <file>] <git-project-dir> <main branch> <feature branch> [port]",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,30 +46,35 @@ if (!projectDir || !mainBranch || !featureBranch) {
|
||||||
|
|
||||||
function run(cmd) {
|
function run(cmd) {
|
||||||
try {
|
try {
|
||||||
return execSync(cmd, { encoding: 'utf-8', cwd: projectDir }).trim();
|
return execSync(cmd, {
|
||||||
|
encoding: "utf-8",
|
||||||
|
cwd: projectDir,
|
||||||
|
}).trim();
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchDiff = run(`git diff ${mainBranch} ${featureBranch} --no-color`);
|
const branchDiff = run(`git diff ${mainBranch} ${featureBranch} --no-color`);
|
||||||
const stagedDiff = run('git diff --cached --no-color');
|
const stagedDiff = run("git diff --cached --no-color");
|
||||||
const unstagedDiff = run('git diff --no-color');
|
const unstagedDiff = run("git diff --no-color");
|
||||||
|
|
||||||
const untrackedFiles = run('git ls-files --others --exclude-standard')
|
const untrackedFiles = run("git ls-files --others --exclude-standard")
|
||||||
.split('\n')
|
.split("\n")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
let untrackedPatch = '';
|
let untrackedPatch = "";
|
||||||
for (const file of untrackedFiles) {
|
for (const file of untrackedFiles) {
|
||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
content = readFileSync(join(projectDir, file), 'utf-8');
|
content = readFileSync(join(projectDir, file), "utf-8");
|
||||||
} catch {
|
} catch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const lines = content.split('\n');
|
const lines = content.split("\n");
|
||||||
const lineCount = content.endsWith('\n') ? lines.length - 1 : lines.length;
|
const lineCount = content.endsWith("\n")
|
||||||
|
? lines.length - 1
|
||||||
|
: lines.length;
|
||||||
untrackedPatch += `diff --git a/${file} b/${file}\n`;
|
untrackedPatch += `diff --git a/${file} b/${file}\n`;
|
||||||
untrackedPatch += `new file mode 100644\n`;
|
untrackedPatch += `new file mode 100644\n`;
|
||||||
untrackedPatch += `--- /dev/null\n`;
|
untrackedPatch += `--- /dev/null\n`;
|
||||||
|
|
@ -73,18 +87,18 @@ for (const file of untrackedFiles) {
|
||||||
|
|
||||||
const patch = [branchDiff, stagedDiff, unstagedDiff, untrackedPatch]
|
const patch = [branchDiff, stagedDiff, unstagedDiff, untrackedPatch]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n');
|
.join("\n");
|
||||||
|
|
||||||
if (!patch.trim()) {
|
if (!patch.trim()) {
|
||||||
console.log('No differences found');
|
console.log("No differences found");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedEntries = parsePatchFiles(patch);
|
const parsedEntries = parsePatchFiles(patch);
|
||||||
const files = parsedEntries.flatMap(e => e.files ?? []);
|
const files = parsedEntries.flatMap((e) => e.files ?? []);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
console.log('No file diffs found in patch');
|
console.log("No file diffs found in patch");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,23 +107,28 @@ if (files.length === 0) {
|
||||||
(async () => {
|
(async () => {
|
||||||
const ssrOptions = {
|
const ssrOptions = {
|
||||||
theme: DEFAULT_THEMES,
|
theme: DEFAULT_THEMES,
|
||||||
themeType: 'system',
|
themeType: "system",
|
||||||
diffStyle: 'unified',
|
diffStyle: "unified",
|
||||||
useTokenTransformer: true,
|
useTokenTransformer: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderedFiles = [];
|
const renderedFiles = [];
|
||||||
for (const fileDiff of files) {
|
for (const fileDiff of files) {
|
||||||
try {
|
try {
|
||||||
const result = await preloadFileDiff({ fileDiff, options: ssrOptions });
|
const result = await preloadFileDiff({
|
||||||
|
fileDiff,
|
||||||
|
options: ssrOptions,
|
||||||
|
});
|
||||||
renderedFiles.push({
|
renderedFiles.push({
|
||||||
name: fileDiff.name || 'unknown',
|
name: fileDiff.name || "unknown",
|
||||||
prerenderedHTML: result.prerenderedHTML,
|
prerenderedHTML: result.prerenderedHTML,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`Failed to SSR-render ${fileDiff.name || 'unknown'}: ${err.message}`);
|
console.warn(
|
||||||
|
`Failed to SSR-render ${fileDiff.name || "unknown"}: ${err.message}`,
|
||||||
|
);
|
||||||
renderedFiles.push({
|
renderedFiles.push({
|
||||||
name: fileDiff.name || 'unknown',
|
name: fileDiff.name || "unknown",
|
||||||
prerenderedHTML: null,
|
prerenderedHTML: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -118,17 +137,22 @@ if (files.length === 0) {
|
||||||
const finalPatch = patch;
|
const finalPatch = patch;
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
return str
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileId(name) {
|
function fileId(name) {
|
||||||
return 'DF-' + name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
return "DF-" + name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pageHtml(opts) {
|
function pageHtml(opts) {
|
||||||
const { mode, sidebarHtml, extraScript } = opts;
|
const { mode, sidebarHtml, extraScript } = opts;
|
||||||
|
|
||||||
const fileSections = renderedFiles.map((f) => {
|
const fileSections = renderedFiles
|
||||||
|
.map((f) => {
|
||||||
const escapedName = escapeHtml(f.name);
|
const escapedName = escapeHtml(f.name);
|
||||||
const id = fileId(f.name);
|
const id = fileId(f.name);
|
||||||
if (f.prerenderedHTML !== null) {
|
if (f.prerenderedHTML !== null) {
|
||||||
|
|
@ -145,16 +169,18 @@ if (files.length === 0) {
|
||||||
<div class="diff-file-header">${escapedName}</div>
|
<div class="diff-file-header">${escapedName}</div>
|
||||||
<div class="diff-fallback"><a href="/patch">View raw patch</a></div>
|
<div class="diff-fallback"><a href="/patch">View raw patch</a></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n');
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
const hasLocal = stagedDiff || unstagedDiff || untrackedPatch;
|
const hasLocal = stagedDiff || unstagedDiff || untrackedPatch;
|
||||||
const subtitle = hasLocal
|
const subtitle = hasLocal
|
||||||
? `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)} (includes local changes)`
|
? `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)} (includes local changes)`
|
||||||
: `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
|
: `${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
|
||||||
const title = `Diff: ${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
|
const title = `Diff: ${escapeHtml(mainBranch)} \u2192 ${escapeHtml(featureBranch)}`;
|
||||||
const rawLink = mode === 'server'
|
const rawLink =
|
||||||
|
mode === "server"
|
||||||
? '<a class="raw-link" href="/patch">View raw patch \u2192</a>'
|
? '<a class="raw-link" href="/patch">View raw patch \u2192</a>'
|
||||||
: '';
|
: "";
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
@ -269,7 +295,7 @@ ${extraScript}
|
||||||
const sidebarHtml = `
|
const sidebarHtml = `
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<div class="file-list">
|
<div class="file-list">
|
||||||
${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)}">${escapeHtml(f.name)}</a>`).join('\n')}
|
${renderedFiles.map((f) => ` <a class="file-item" href="#${fileId(f.name)}">${escapeHtml(f.name)}</a>`).join("\n")}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
|
@ -292,18 +318,24 @@ ${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)
|
||||||
});
|
});
|
||||||
</script>`;
|
</script>`;
|
||||||
|
|
||||||
return pageHtml({ mode: 'static', sidebarHtml, extraScript })
|
return pageHtml({
|
||||||
.replace('</body>', ` <details style="padding:0 24px 16px">
|
mode: "static",
|
||||||
|
sidebarHtml,
|
||||||
|
extraScript,
|
||||||
|
}).replace(
|
||||||
|
"</body>",
|
||||||
|
` <details style="padding:0 24px 16px">
|
||||||
<summary id="patch-toggle" style="cursor:pointer;font-size:0.8125rem;color:#2563eb">View raw patch</summary>
|
<summary id="patch-toggle" style="cursor:pointer;font-size:0.8125rem;color:#2563eb">View raw patch</summary>
|
||||||
<pre id="patch-content" class="raw-patch">${escapedPatch}</pre>
|
<pre id="patch-content" class="raw-patch">${escapedPatch}</pre>
|
||||||
</details>\n</body>`);
|
</details>\n</body>`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- mode dispatch ---
|
// --- mode dispatch ---
|
||||||
|
|
||||||
if (outFile) {
|
if (outFile) {
|
||||||
const html = buildStaticHtml();
|
const html = buildStaticHtml();
|
||||||
writeFileSync(outFile, html, 'utf-8');
|
writeFileSync(outFile, html, "utf-8");
|
||||||
console.log(`Written to ${outFile}`);
|
console.log(`Written to ${outFile}`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
@ -311,42 +343,58 @@ ${renderedFiles.map(f => ` <a class="file-item" href="#${fileId(f.name)
|
||||||
if (isStatic) {
|
if (isStatic) {
|
||||||
const html = buildStaticHtml();
|
const html = buildStaticHtml();
|
||||||
const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`);
|
const tmpFile = join(tmpdir(), `difftool-${Date.now()}.html`);
|
||||||
writeFileSync(tmpFile, html, 'utf-8');
|
writeFileSync(tmpFile, html, "utf-8");
|
||||||
|
|
||||||
const openCmd = process.platform === 'darwin' ? 'open'
|
const openCmd =
|
||||||
: process.platform === 'win32' ? 'start'
|
process.platform === "darwin"
|
||||||
: 'xdg-open';
|
? "open"
|
||||||
|
: process.platform === "win32"
|
||||||
|
? "start"
|
||||||
|
: "xdg-open";
|
||||||
console.log(`Opening ${tmpFile}`);
|
console.log(`Opening ${tmpFile}`);
|
||||||
try {
|
try {
|
||||||
spawn(openCmd, [tmpFile], { detached: true, stdio: 'ignore' }).unref();
|
spawn(openCmd, [tmpFile], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
}).unref();
|
||||||
} catch {
|
} catch {
|
||||||
console.log(`HTML file saved to ${tmpFile}`);
|
console.log(`HTML file saved to ${tmpFile}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.static(join(__dirname, 'public')));
|
app.use(express.static(join(__dirname, "public")));
|
||||||
|
|
||||||
app.get('/patch', (req, res) => {
|
app.get("/patch", (req, res) => {
|
||||||
res.type('text/plain').send(finalPatch);
|
res.type("text/plain").send(finalPatch);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/', (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
const fileDataJson = JSON.stringify(renderedFiles.map(f => ({ name: f.name })))
|
const fileDataJson = JSON.stringify(
|
||||||
.replace(/<\/script>/gi, '<\\/script>');
|
renderedFiles.map((f) => ({ name: f.name })),
|
||||||
|
).replace(/<\/script>/gi, "<\\/script>");
|
||||||
|
|
||||||
const sidebarHtml = '<div class="sidebar"><div id="file-tree"></div></div>';
|
const sidebarHtml =
|
||||||
|
'<div class="sidebar"><div id="file-tree"></div></div>';
|
||||||
|
|
||||||
const extraScript = `
|
const extraScript = `
|
||||||
<script id="diff-files-data" type="application/json">${fileDataJson}</script>
|
<script id="diff-files-data" type="application/json">${fileDataJson}</script>
|
||||||
<script type="module" src="/client.js"></script>`;
|
<script type="module" src="/client.js"></script>`;
|
||||||
|
|
||||||
const html = pageHtml({ mode: 'server', sidebarHtml, extraScript });
|
const html = pageHtml({
|
||||||
res.type('html').send(html);
|
mode: "server",
|
||||||
|
sidebarHtml,
|
||||||
|
extraScript,
|
||||||
|
});
|
||||||
|
res.type("html").send(html);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Difftool running at http://localhost:${port}`);
|
console.log(
|
||||||
console.log(`Patch endpoint: http://localhost:${port}/patch`);
|
`Difftool running at http://localhost:${port}`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Patch endpoint: http://localhost:${port}/patch`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
import { FileTree } from '@pierre/trees';
|
import { FileTree } from "@pierre/trees";
|
||||||
|
|
||||||
const dataEl = document.getElementById('diff-files-data');
|
const dataEl = document.getElementById("diff-files-data");
|
||||||
const files = JSON.parse(dataEl.textContent);
|
const files = JSON.parse(dataEl.textContent);
|
||||||
const paths = files.map(f => f.name);
|
const paths = files.map((f) => f.name);
|
||||||
|
|
||||||
const mount = document.getElementById('file-tree');
|
const mount = document.getElementById("file-tree");
|
||||||
mount.style.height = (window.innerHeight - mount.getBoundingClientRect().top) + 'px';
|
mount.style.height =
|
||||||
|
window.innerHeight - mount.getBoundingClientRect().top + "px";
|
||||||
|
|
||||||
const tree = new FileTree({
|
const tree = new FileTree({
|
||||||
paths,
|
paths,
|
||||||
initialExpansion: 'open',
|
initialExpansion: "open",
|
||||||
onSelectionChange: (selectedPaths) => {
|
onSelectionChange: (selectedPaths) => {
|
||||||
if (selectedPaths.length !== 1) return;
|
if (selectedPaths.length !== 1) return;
|
||||||
const id = 'DF-' + selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, '_');
|
const id =
|
||||||
|
"DF-" +
|
||||||
|
selectedPaths[0].replace(/[^a-zA-Z0-9_-]/g, "_");
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
el.scrollIntoView({
|
||||||
el.classList.add('diff-highlight');
|
behavior: "smooth",
|
||||||
setTimeout(() => el.classList.remove('diff-highlight'), 1500);
|
block: "start",
|
||||||
|
});
|
||||||
|
el.classList.add("diff-highlight");
|
||||||
|
setTimeout(
|
||||||
|
() => el.classList.remove("diff-highlight"),
|
||||||
|
1500,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue