File size: 11,068 Bytes
bd0f095 58c4e76 bd0f095 58c4e76 bd0f095 58c4e76 bd0f095 37bc184 bd0f095 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | /* Shared unified diff renderer. Patch content is always inserted as text. */
(() => {
function parse(patch) {
const files = [];
let file, oldLine = null, newLine = null;
for (const line of patch.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n")) {
if (line.startsWith("diff --git ")) {
file = {name: line.slice(11), rows: [], additions: 0, deletions: 0};
files.push(file);
oldLine = newLine = null;
}
if (!file) {
file = {name: "Patch", rows: [], additions: 0, deletions: 0};
files.push(file);
}
const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
let kind = "meta", old = null, next = null;
if ((line.startsWith("+++ ") || line.startsWith("--- ")) && oldLine === null) {
if (line.slice(4) !== "/dev/null") file.name = line.slice(4).replace(/^[ab]\//, "");
}
if (hunk) {
oldLine = Number(hunk[1]); newLine = Number(hunk[2]); kind = "hunk";
} else if (oldLine !== null) {
if (line.startsWith("+")) {
kind = "add"; next = newLine++; file.additions++;
} else if (line.startsWith("-")) {
kind = "del"; old = oldLine++; file.deletions++;
} else if (line.startsWith(" ")) {
kind = "context"; old = oldLine++; next = newLine++;
}
}
file.rows.push({text: line, kind, old, next});
}
return files;
}
function el(tag, cls, value) {
const node = document.createElement(tag);
node.className = cls;
if (value != null) node.textContent = value;
return node;
}
function isTestFile(path) {
return /(^|\/)(tests?|__tests__)(\/|$)|(^|\/)test_[^/]+$|(^|\/)[^/]+_test\.[^/]+$|\.(test|spec)\.[^/]+$/.test(path);
}
function isGeneratedFile(file, files) {
if (file.rows.some(row => row.kind !== 'del' && /This file was automatically generated from/.test(row.text))) return true;
const dir = file.name.slice(0, file.name.lastIndexOf('/') + 1);
return (file.name.slice(dir.length) === '__init__.py' || /^(configuration|modeling|processing|image_processing(?:_pil|_fast)?|video_processing|tokenization(?:_fast)?|feature_extraction)_/.test(file.name.slice(dir.length))) && files.some(f => f.name.startsWith(dir + 'modular_') && !f.name.slice(dir.length).includes('/'));
}
function lineContext(file, row) {
const index = file.rows.indexOf(row);
const nearby = file.rows.slice(Math.max(0, index - 6), index + 7).filter(r => ['add', 'del', 'context'].includes(r.kind));
const old = nearby.filter(r => r.old != null), next = nearby.filter(r => r.next != null);
return `@@ -${old[0]?.old || 0},${old.length} +${next[0]?.next || 0},${next.length} @@\n` + nearby.map(r => r.text).join('\n');
}
function createRow(row, file = null) {
const tr = el('tr', `patch-line ${row.kind}`);
if (row.old != null) tr.dataset.oldLine = row.old;
if (row.next != null) tr.dataset.newLine = row.next;
for (const [line, side] of [[row.old, 'LEFT'], [row.next, 'RIGHT']]) {
const cell = el('td', 'patch-number');
if (line != null && file) {
const button = el('button', 'line-comment-button', line);
button.type = 'button'; button.setAttribute('aria-label', `Add comment on ${side === 'LEFT' ? 'old' : 'new'} line ${line}`);
button.title = 'Add inline comment';
button.onclick = () => button.dispatchEvent(new CustomEvent('patch-comment', {bubbles:true, detail:{path:file.name, line, side, diff_hunk:lineContext(file, row)}}));
cell.append(button);
} else if (line != null) cell.textContent = line;
tr.append(cell);
}
tr.append(el('td', 'patch-code', row.text));
return tr;
}
function isCommentLine(file, row) {
return file.name.endsWith('.py') && ['add', 'del', 'context'].includes(row.kind) && /^\s*#/.test(row.text.slice(1));
}
function moduleHeaderRows(file) {
const hidden = new Set();
const markdown = /\.mdx?$/.test(file.name);
if (!file.name.endsWith('.py') && !markdown) return hidden;
for (const coordinate of ['old', 'next']) {
const source = file.rows.filter(row => row[coordinate] != null);
if (!source.length || source[0][coordinate] !== 1) continue;
const header = [];
if (markdown) {
let started = false, closed = false;
for (const row of source) {
const content = row.text.slice(1).trim();
if (!started && content && !content.startsWith('<!--')) break;
header.push(row);
if (content.startsWith('<!--')) started = true;
if (started && content.includes('-->')) { closed = true; break; }
}
if (closed && header.some(row => /copyright/i.test(row.text))) header.forEach(row => hidden.add(row));
continue;
}
for (const row of source) {
const content = row.text.slice(1).trim();
if (content && !content.startsWith('#')) break;
header.push(row);
}
if (header.some(row => /copyright|licensed under|SPDX-License-Identifier|automatically generated/i.test(row.text))) header.forEach(row => hidden.add(row));
}
return hidden;
}
function render(container, patch, options = {}) {
const allFiles = parse(patch);
const files = allFiles.filter(file => (!options.hideTests || !isTestFile(file.name)) && (!options.onlyPython || file.name.endsWith('.py')) && (!options.onlyModels || file.name.startsWith('src/transformers/models/')) && (!(options.hideGenerated || options.onlyModels) || !isGeneratedFile(file, allFiles)));
const wrapper = el("div", "patch-view");
const toolbar = el("div", "patch-toolbar");
toolbar.append(el("span", "", `${files.length === allFiles.length ? files.length : files.length + ' of ' + allFiles.length} file${files.length === 1 ? "" : "s"} · +${files.reduce((n, f) => n + f.additions, 0)} −${files.reduce((n, f) => n + f.deletions, 0)}`));
const download = el("a", "secondary nav-link", "Download full patch");
const url = URL.createObjectURL(new Blob([patch], {type: "text/plain"}));
const previous = container.dataset.patchUrl;
if (previous) URL.revokeObjectURL(previous);
container.dataset.patchUrl = url;
download.href = url; download.download = "pr.diff";
toolbar.append(download);
wrapper.append(toolbar);
if (!files.length) wrapper.append(el('p', 'comments-status', 'No files match these filters.'));
for (const file of files) {
const details = el("details", "patch-file"); details.open = true;
details.dataset.path = file.name;
const summary = el("summary", "patch-file-header");
summary.append(el("span", "patch-filename", file.name), el("span", "patch-counts", `+${file.additions} −${file.deletions}`));
const scroll = el("div", "patch-scroll");
const table = el("table", "patch-table");
table.setAttribute("aria-label", `Unified diff for ${file.name}`);
const body = document.createElement("tbody");
const headerRows = options.hideModuleHeaders ? moduleHeaderRows(file) : new Set();
for (const row of file.rows) {
if (headerRows.has(row)) continue;
if (options.hideComments && isCommentLine(file, row)) continue;
body.append(createRow(row, file));
}
table.append(body); scroll.append(table); details.append(summary, scroll); wrapper.append(details);
}
const raw = el("details", "patch-raw");
raw.append(el("summary", "", "Raw full patch"), el("pre", "", patch));
wrapper.append(raw);
container.replaceChildren(wrapper);
}
function showLines(container, comment) {
container.querySelectorAll('.line-highlight').forEach(row => row.classList.remove('line-highlight'));
container.querySelectorAll('.comment-context').forEach(context => context.remove());
const file = [...container.querySelectorAll('.patch-file')].find(f => f.dataset.path === comment.path);
const key = comment.side === 'LEFT' ? 'oldLine' : 'newLine';
const end = Number(comment.outdated ? comment.original_line || comment.line : comment.line);
const start = Number((comment.outdated ? comment.original_start_line : comment.start_line) || end);
let rows = file && !comment.outdated ? [...file.querySelectorAll('.patch-line')].filter(row => row.dataset[key] != null && Number(row.dataset[key]) >= start && Number(row.dataset[key]) <= end) : [];
if (!rows.length && comment.diff_hunk) {
const parsed = parse(comment.diff_hunk)[0];
const coordinate = key === 'oldLine' ? 'old' : 'next';
const nearby = parsed.rows.filter(row => row.kind === 'hunk' || (row[coordinate] != null && row[coordinate] >= start - 4 && row[coordinate] <= end + 4));
const context = el('section', 'comment-context');
context.append(el('p', '', `${comment.path} · ${comment.outdated ? 'Comment context from an earlier revision' : 'Comment context'}`));
const scroll = el('div', 'patch-scroll'), table = el('table', 'patch-table'), body = document.createElement('tbody');
nearby.forEach(row => body.append(createRow(row))); table.append(body); scroll.append(table); context.append(scroll);
if (file) file.append(context); else container.prepend(context);
rows = [...context.querySelectorAll('.patch-line')].filter(row => row.dataset[key] != null && Number(row.dataset[key]) >= start && Number(row.dataset[key]) <= end);
}
if (!rows.length) return false;
if (file) file.open = true;
rows.forEach(row => row.classList.add('line-highlight'));
rows[0].tabIndex = -1; rows[0].focus({preventScroll:true}); rows[0].scrollIntoView({behavior:'smooth', block:'center', inline:'nearest'});
return true;
}
async function fetchText(url, progress, status, label) {
const response = await fetch(url, {cache:'no-store'});
if (!response.ok) throw Error(await response.text());
if (!response.body) return response.text();
// Fetch streams decoded bytes; compressed Content-Length is not comparable.
const length = response.headers.get('Content-Encoding') ? 0 : Number(response.headers.get('Content-Length'));
const total = Number.isFinite(length) && length > 0 ? length : 0;
const reader = response.body.getReader(), decoder = new TextDecoder(), chunks = [];
let loaded = 0, lastUpdate = 0;
while (true) {
const {done, value} = await reader.read();
if (done) break;
loaded += value.byteLength;
chunks.push(decoder.decode(value, {stream:true}));
if (total) { progress.max = total; progress.value = Math.min(loaded, total); }
if (performance.now() - lastUpdate > 200) {
status.textContent = 'Loading ' + label + '… ' + (total ? Math.min(100, Math.floor(loaded / total * 100)) + '%' : Math.ceil(loaded / 1024).toLocaleString() + ' KB');
lastUpdate = performance.now();
}
}
chunks.push(decoder.decode());
return chunks.join('');
}
globalThis.TCIPatch = {parse, render, isTestFile, isGeneratedFile, showLines, isCommentLine, moduleHeaderRows, fetchText};
})();
|