mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
One appeared on load (a deprecated meta tag). One appeared when adding an Identity (Passthrough) node. One appeared when exposing the the Weight parameter of the Stroke node. This also fixes the intended behavior of the bisect script, which was involved when finding the prevenance of these errors.
619 lines
20 KiB
JavaScript
619 lines
20 KiB
JavaScript
document.addEventListener("DOMContentLoaded", () => {
|
|
const REPO = "GraphiteEditor/Graphite";
|
|
const API = "https://api.github.com";
|
|
|
|
// =========
|
|
// API LAYER
|
|
// =========
|
|
|
|
const cache = new Map();
|
|
let rateLimitRemaining = -1;
|
|
let rateLimitReset = 0;
|
|
|
|
async function fetchJSON(/** @type {string} */ url) {
|
|
if (cache.has(url)) return cache.get(url);
|
|
|
|
const response = await fetch(url);
|
|
|
|
// Track rate limit
|
|
const remaining = response.headers.get("X-RateLimit-Remaining");
|
|
const reset = response.headers.get("X-RateLimit-Reset");
|
|
if (remaining) rateLimitRemaining = parseInt(remaining);
|
|
if (reset) rateLimitReset = parseInt(reset);
|
|
updateRateLimitWarning();
|
|
|
|
if (response.status === 404) {
|
|
cache.set(url, undefined);
|
|
return undefined;
|
|
}
|
|
|
|
if (response.status === 403) {
|
|
const resetTime = rateLimitReset ? new Date(rateLimitReset * 1000).toLocaleTimeString() : undefined;
|
|
const suffix = resetTime ? ` Resets at ${resetTime}.` : "";
|
|
throw new Error(`GitHub API rate limit exceeded.${suffix}`);
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => undefined);
|
|
throw new Error(body?.message || `GitHub API error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
cache.set(url, data);
|
|
return data;
|
|
}
|
|
|
|
async function fetchCommitList(/** @type {string | undefined} */ since, /** @type {string | undefined} */ until, /** @type {number | undefined} */ page) {
|
|
let url = `${API}/repos/${REPO}/commits?sha=master&per_page=100`;
|
|
if (since) url += `&since=${since}`;
|
|
if (until) url += `&until=${until}`;
|
|
if (page && page > 1) url += `&page=${page}`;
|
|
return fetchJSON(url);
|
|
}
|
|
|
|
async function fetchDeployUrl(/** @type {string} */ sha) {
|
|
const comments = await fetchJSON(`${API}/repos/${REPO}/commits/${sha}/comments`);
|
|
if (!comments || !Array.isArray(comments)) return undefined;
|
|
|
|
// Find bot comments, use the last one
|
|
const botComments = comments.filter((c) => c.user && c.user.login === "github-actions[bot]");
|
|
if (botComments.length === 0) return undefined;
|
|
|
|
const lastComment = botComments[botComments.length - 1];
|
|
const match = lastComment.body.match(/\|\s*(https:\/\/[^\s|]+)\s*\|/);
|
|
return match ? match[1] : undefined;
|
|
}
|
|
|
|
// ==============
|
|
// DOM REFERENCES
|
|
// ==============
|
|
|
|
const tool = document.querySelector(".bisect-tool");
|
|
if (!tool) return;
|
|
|
|
const phases = {
|
|
// eslint-disable-next-line quotes
|
|
setup: tool.querySelector('[data-phase="setup"]'),
|
|
// eslint-disable-next-line quotes
|
|
bisect: tool.querySelector('[data-phase="bisect"]'),
|
|
};
|
|
|
|
const elements = {
|
|
messageBox: tool.querySelector("[data-message-box]"),
|
|
|
|
hashInput: tool.querySelector("[data-input='hash']"),
|
|
dateInput: tool.querySelector("[data-input='date']"),
|
|
commitHash: tool.querySelector("[data-commit-hash]"),
|
|
commitDate: tool.querySelector("[data-commit-date]"),
|
|
startButton: tool.querySelector("[data-start-button]"),
|
|
|
|
stepLabel: tool.querySelector("[data-step-label]"),
|
|
commitInfo: tool.querySelector("[data-commit-info]"),
|
|
progressInfo: tool.querySelector("[data-progress-info]"),
|
|
testBuildButton: tool.querySelector("[data-test-build-button]"),
|
|
issuePresentButton: tool.querySelector("[data-issue-present-button]"),
|
|
issueAbsentButton: tool.querySelector("[data-issue-absent-button]"),
|
|
goBackButton: tool.querySelector("[data-go-back-button]"),
|
|
findings: tool.querySelector(".findings"),
|
|
bisectActions: tool.querySelector(".bisect-actions"),
|
|
};
|
|
|
|
// =====
|
|
// STATE
|
|
// =====
|
|
|
|
/**
|
|
* @typedef {{ sha: string, date: Date, message: string }} Commit
|
|
* @typedef {{ goodIndex: number, badIndex: number, currentIndex: number, stepCount: number, bisectPhase: string, boundaryOffset: number, boundarySearching: boolean }} HistorySnapshot
|
|
*/
|
|
|
|
let mode = "regression"; // "regression" or "feature"
|
|
let /** @type {Commit[]} */ commits = []; // Ordered oldest-first
|
|
let goodIndex = -1; // Index where issue is absent (older side)
|
|
let badIndex = -1; // Index where issue is present (newer side)
|
|
let currentIndex = -1;
|
|
let /** @type {string | undefined} */ currentDeployUrl;
|
|
let stepCount = 0;
|
|
let /** @type {HistorySnapshot[]} */ history = []; // Snapshots for undo
|
|
let bisectPhase = "boundary"; // "boundary" or "binary"
|
|
let boundaryOffset = 1; // For exponential boundary search
|
|
let boundarySearching = false; // Whether we're in exponential backward search
|
|
let startIndex = -1; // Where user started
|
|
|
|
// =======
|
|
// HELPERS
|
|
// =======
|
|
|
|
function commitToHtml(/** @type {Commit} */ commit) {
|
|
const shortHash = (/** @type {string} */ sha) => sha.slice(0, 7);
|
|
const commitUrl = (/** @type {string} */ sha) => `https://github.com/${REPO}/commit/${sha}`;
|
|
|
|
const hash = `<a href="${commitUrl(commit.sha)}" target="_blank" rel="noopener">${shortHash(commit.sha)}</a>`;
|
|
const date = commit.date.toISOString().slice(0, 10);
|
|
const message = messageToHtml(commit.message);
|
|
return `<strong>${hash}</strong> (${date}): ${message}`;
|
|
}
|
|
|
|
function messageToHtml(/** @type {string} */ message) {
|
|
if (!message) return "";
|
|
const escaped = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
const prMatch = message.match(/\(#(\d+)\)$/);
|
|
if (prMatch) return escaped.replace(`(#${prMatch[1]})`, `(<a href="https://github.com/${REPO}/pull/${prMatch[1]}" target="_blank" rel="noopener">#${prMatch[1]}</a>)`);
|
|
return escaped;
|
|
}
|
|
|
|
function parseCommits(/** @type {any[]} */ apiCommits) {
|
|
return apiCommits.map((/** @type {any} */ c) => ({
|
|
sha: c.sha,
|
|
date: new Date(c.commit.committer.date),
|
|
message: c.commit.message.split("\n")[0],
|
|
}));
|
|
}
|
|
|
|
function setDisabled(/** @type {Element | null} */ element, /** @type {boolean} */ disabled) {
|
|
element?.classList.toggle("disabled", disabled);
|
|
}
|
|
|
|
function isDisabled(/** @type {Element | null} */ element) {
|
|
return element?.classList.contains("disabled") ?? false;
|
|
}
|
|
|
|
function showPhase(/** @type {string} */ name) {
|
|
Object.entries(phases).forEach(([key, phase]) => {
|
|
phase?.classList.toggle("active", key === name);
|
|
});
|
|
}
|
|
|
|
function showMessage(/** @type {string} */ html) {
|
|
if (elements.messageBox) elements.messageBox.innerHTML = html;
|
|
elements.messageBox?.classList.add("visible");
|
|
}
|
|
|
|
function hideMessage() {
|
|
elements.messageBox?.classList.remove("visible");
|
|
}
|
|
|
|
function updateRateLimitWarning() {
|
|
if (rateLimitRemaining >= 0 && rateLimitRemaining < 15) {
|
|
const resetTime = rateLimitReset ? new Date(rateLimitReset * 1000).toLocaleTimeString() : "unknown";
|
|
const plural = rateLimitRemaining === 1 ? "" : "s";
|
|
showMessage(`<strong>API rate limit:</strong> ${rateLimitRemaining} request${plural} remaining. Resets at ${resetTime}.`);
|
|
}
|
|
}
|
|
|
|
// ======================
|
|
// COMMIT LIST MANAGEMENT
|
|
// ======================
|
|
|
|
async function loadCommitsAroundDate(/** @type {Date} */ targetDate) {
|
|
const windowDays = 30;
|
|
const since = new Date(targetDate.getTime() - windowDays * 24 * 60 * 60 * 1000).toISOString();
|
|
const until = new Date(targetDate.getTime() + windowDays * 24 * 60 * 60 * 1000).toISOString();
|
|
|
|
// Paginate to load all commits in the window (API returns max 100 per page)
|
|
|
|
let /** @type {any[]} */ allRaw = [];
|
|
let page = 1;
|
|
|
|
while (true) {
|
|
const raw = await fetchCommitList(since, until, page);
|
|
if (!raw || raw.length === 0) break;
|
|
allRaw = allRaw.concat(raw);
|
|
if (raw.length < 100) break;
|
|
page++;
|
|
}
|
|
|
|
if (allRaw.length === 0) {
|
|
throw new Error("No commits found near that date. Try a different date.");
|
|
}
|
|
|
|
// GitHub returns newest-first, reverse to oldest-first
|
|
const fetched = parseCommits(allRaw);
|
|
fetched.reverse();
|
|
|
|
commits = fetched;
|
|
}
|
|
|
|
async function extendCommitsForward() {
|
|
if (commits.length === 0) return false;
|
|
|
|
const newest = commits[commits.length - 1];
|
|
const since = new Date(newest.date.getTime() + 1000).toISOString();
|
|
|
|
let /** @type {any[]} */ allRaw = [];
|
|
let page = 1;
|
|
|
|
while (true) {
|
|
const raw = await fetchCommitList(since, undefined, page);
|
|
if (!raw || raw.length === 0) break;
|
|
allRaw = allRaw.concat(raw);
|
|
if (raw.length < 100) break;
|
|
page++;
|
|
}
|
|
|
|
if (allRaw.length === 0) return false;
|
|
|
|
let fetched = parseCommits(allRaw);
|
|
fetched.reverse();
|
|
|
|
const existingShas = new Set(commits.map((c) => c.sha));
|
|
fetched = fetched.filter((c) => !existingShas.has(c.sha));
|
|
if (fetched.length === 0) return false;
|
|
|
|
commits = [...commits, ...fetched];
|
|
return true;
|
|
}
|
|
|
|
async function extendCommitsBackward() {
|
|
if (commits.length === 0) return false;
|
|
|
|
const oldest = commits[0];
|
|
const until = new Date(oldest.date.getTime() - 1000).toISOString();
|
|
const raw = await fetchCommitList(undefined, until, undefined);
|
|
if (!raw || raw.length === 0) return false;
|
|
|
|
let fetched = parseCommits(raw);
|
|
fetched.reverse();
|
|
|
|
const existingShas = new Set(commits.map((c) => c.sha));
|
|
fetched = fetched.filter((c) => !existingShas.has(c.sha));
|
|
if (fetched.length === 0) return false;
|
|
|
|
commits = [...fetched, ...commits];
|
|
|
|
// Adjust indices to account for prepended commits
|
|
const shift = fetched.length;
|
|
if (goodIndex >= 0) goodIndex += shift;
|
|
if (badIndex >= 0) badIndex += shift;
|
|
if (currentIndex >= 0) currentIndex += shift;
|
|
if (startIndex >= 0) startIndex += shift;
|
|
|
|
return true;
|
|
}
|
|
|
|
function findCommitIndex(/** @type {string} */ sha) {
|
|
return commits.findIndex((c) => c.sha.startsWith(sha) || sha.startsWith(c.sha));
|
|
}
|
|
|
|
// ============
|
|
// BISECT LOGIC
|
|
// ============
|
|
|
|
function pushHistory() {
|
|
history.push({
|
|
goodIndex,
|
|
badIndex,
|
|
currentIndex,
|
|
stepCount,
|
|
bisectPhase,
|
|
boundaryOffset,
|
|
boundarySearching,
|
|
});
|
|
elements.goBackButton?.classList.remove("hidden");
|
|
}
|
|
|
|
function popHistory() {
|
|
const snap = history.pop();
|
|
if (!snap) return;
|
|
goodIndex = snap.goodIndex;
|
|
badIndex = snap.badIndex;
|
|
currentIndex = snap.currentIndex;
|
|
stepCount = snap.stepCount;
|
|
bisectPhase = snap.bisectPhase;
|
|
boundaryOffset = snap.boundaryOffset;
|
|
boundarySearching = snap.boundarySearching;
|
|
elements.goBackButton?.classList.remove("hidden");
|
|
}
|
|
|
|
async function presentCommit(/** @type {number} */ index) {
|
|
currentIndex = index;
|
|
const commit = commits[index];
|
|
const deployUrl = await fetchDeployUrl(commit.sha);
|
|
currentDeployUrl = deployUrl;
|
|
|
|
if (elements.stepLabel) elements.stepLabel.innerHTML = `<strong>Bisect step ${stepCount + 1}</strong>`;
|
|
if (elements.commitInfo) {
|
|
elements.commitInfo.innerHTML = commitToHtml(commit);
|
|
}
|
|
|
|
if (goodIndex >= 0 && badIndex >= 0) {
|
|
const remaining = badIndex - goodIndex;
|
|
const stepsLeft = Math.max(1, Math.ceil(Math.log2(remaining)));
|
|
if (elements.progressInfo) {
|
|
elements.progressInfo.innerHTML = `<em>${remaining} commit${remaining === 1 ? "" : "s"} in range, ~${stepsLeft} step${stepsLeft === 1 ? "" : "s"} remaining</em>`;
|
|
}
|
|
} else {
|
|
if (elements.progressInfo) elements.progressInfo.innerHTML = "<em>Locating starting point</em>";
|
|
}
|
|
|
|
if (deployUrl) {
|
|
setDisabled(elements.testBuildButton, false);
|
|
if (elements.testBuildButton) elements.testBuildButton.textContent = "Test this build";
|
|
} else {
|
|
setDisabled(elements.testBuildButton, true);
|
|
if (elements.testBuildButton) elements.testBuildButton.textContent = "No build available";
|
|
}
|
|
|
|
// Set mode-specific button labels
|
|
if (mode === "regression") {
|
|
if (elements.issuePresentButton) elements.issuePresentButton.textContent = "Regression is present";
|
|
if (elements.issueAbsentButton) elements.issueAbsentButton.textContent = "Regression is absent";
|
|
} else {
|
|
if (elements.issuePresentButton) elements.issuePresentButton.textContent = "Feature is present";
|
|
if (elements.issueAbsentButton) elements.issueAbsentButton.textContent = "Feature is absent";
|
|
}
|
|
}
|
|
|
|
async function handleUserResponse(/** @type {boolean} */ issuePresent) {
|
|
pushHistory();
|
|
stepCount++;
|
|
|
|
if (bisectPhase === "boundary") {
|
|
await handleBoundaryResponse(issuePresent);
|
|
return;
|
|
}
|
|
|
|
// Binary search: narrow the range
|
|
if (issuePresent) badIndex = currentIndex;
|
|
else goodIndex = currentIndex;
|
|
|
|
if (badIndex - goodIndex <= 1) showResult();
|
|
else await doBinaryStep();
|
|
}
|
|
|
|
async function handleBoundaryResponse(/** @type {boolean} */ issuePresent) {
|
|
// "present" means the feature/regression exists at this commit (bad/newer side)
|
|
if (!boundarySearching) {
|
|
// First step: user tested the starting commit
|
|
if (issuePresent) {
|
|
// Exists at starting commit, so it was introduced earlier. Search backward (doubling).
|
|
badIndex = currentIndex;
|
|
boundarySearching = true;
|
|
boundaryOffset = Math.max(1, commits.length - 1 - startIndex);
|
|
} else {
|
|
// Absent at starting commit, so the issue was introduced more recently. Extend the commit list forward (towards HEAD) before narrowing.
|
|
goodIndex = currentIndex;
|
|
await extendCommitsForward();
|
|
badIndex = commits.length - 1;
|
|
bisectPhase = "binary";
|
|
}
|
|
} else if (issuePresent) {
|
|
badIndex = currentIndex;
|
|
boundaryOffset *= 2;
|
|
} else {
|
|
goodIndex = currentIndex;
|
|
bisectPhase = "binary";
|
|
}
|
|
|
|
if (bisectPhase === "binary") {
|
|
await doBinaryStep();
|
|
return;
|
|
}
|
|
|
|
// Continue boundary search backward
|
|
await doBoundaryStep();
|
|
}
|
|
|
|
async function doBoundaryStep() {
|
|
let targetIndex = startIndex - boundaryOffset;
|
|
while (targetIndex < 0) {
|
|
const extended = await extendCommitsBackward();
|
|
if (!extended) {
|
|
targetIndex = 0;
|
|
break;
|
|
}
|
|
targetIndex = startIndex - boundaryOffset;
|
|
}
|
|
|
|
// If we've hit the oldest commit and it's still marked bad, we've exhausted history
|
|
if (targetIndex <= 0 && badIndex === 0) {
|
|
showResult();
|
|
// Override the result message — we never confirmed a good baseline, so we can't pinpoint the introducing commit
|
|
if (elements.progressInfo) {
|
|
const label = mode === "regression" ? "regression" : "feature";
|
|
elements.progressInfo.innerHTML = `<em>The ${label} was already present in the oldest available commit</em>`;
|
|
}
|
|
return;
|
|
}
|
|
|
|
await presentCommit(targetIndex);
|
|
}
|
|
|
|
async function doBinaryStep() {
|
|
const mid = Math.floor((goodIndex + badIndex) / 2);
|
|
|
|
// Try to find a testable commit near the midpoint
|
|
let testIndex = mid;
|
|
let offset = 0;
|
|
while (testIndex > goodIndex && testIndex < badIndex) {
|
|
const url = await fetchDeployUrl(commits[testIndex].sha);
|
|
if (url) break;
|
|
// Try alternating sides
|
|
offset++;
|
|
if (offset % 2 === 1) testIndex = mid + Math.ceil(offset / 2);
|
|
else testIndex = mid - Math.ceil(offset / 2);
|
|
}
|
|
|
|
// If no testable commit found in range, show result as a range
|
|
if (testIndex <= goodIndex || testIndex >= badIndex) {
|
|
showResult();
|
|
return;
|
|
}
|
|
|
|
await presentCommit(testIndex);
|
|
}
|
|
|
|
function showResult() {
|
|
const heading = "Bisect complete";
|
|
|
|
// Hide interactive elements, keep the bisect phase visible
|
|
if (elements.progressInfo) elements.progressInfo.innerHTML = "";
|
|
setDisabled(elements.testBuildButton, true);
|
|
if (elements.testBuildButton instanceof HTMLElement) elements.testBuildButton.style.display = "none";
|
|
if (elements.findings instanceof HTMLElement) elements.findings.style.display = "none";
|
|
if (elements.bisectActions instanceof HTMLElement) elements.bisectActions.style.display = "none";
|
|
if (history.length > 0) elements.goBackButton?.classList.remove("hidden");
|
|
|
|
const label = mode === "regression" ? "regression" : "feature";
|
|
const single = badIndex - goodIndex <= 1;
|
|
|
|
if (elements.stepLabel) elements.stepLabel.innerHTML = `<strong>${heading}</strong>`;
|
|
if (elements.progressInfo) {
|
|
elements.progressInfo.innerHTML = single
|
|
? `<em>The ${label} was introduced in the following commit</em>`
|
|
: `<em>The ${label} was introduced in one of the following commits (not all have build links)</em>`;
|
|
}
|
|
|
|
const start = single ? badIndex : goodIndex + 1;
|
|
let html = "";
|
|
for (let i = start; i <= badIndex; i++) {
|
|
const c = commits[i];
|
|
html += single ? `${commitToHtml(c)}` : `<div>${commitToHtml(c)}</div>`;
|
|
}
|
|
if (elements.commitInfo) elements.commitInfo.innerHTML = html;
|
|
}
|
|
|
|
// ==============
|
|
// EVENT HANDLERS
|
|
// ==============
|
|
|
|
// Toggle start input visibility
|
|
function syncStartInputVisibility() {
|
|
// eslint-disable-next-line quotes
|
|
const selected = tool?.querySelector('input[name="start-method"]:checked');
|
|
const method = selected instanceof HTMLInputElement ? selected.value : "date";
|
|
elements.hashInput?.classList.toggle("hidden", method !== "hash");
|
|
elements.dateInput?.classList.toggle("hidden", method !== "date");
|
|
}
|
|
syncStartInputVisibility();
|
|
// eslint-disable-next-line quotes
|
|
tool.querySelectorAll('input[name="start-method"]').forEach((radio) => {
|
|
radio.addEventListener("change", syncStartInputVisibility);
|
|
});
|
|
|
|
// Start bisect
|
|
elements.startButton?.addEventListener("click", async () => {
|
|
if (isDisabled(elements.startButton)) return;
|
|
hideMessage();
|
|
// eslint-disable-next-line quotes
|
|
const modeInput = tool.querySelector('input[name="bisect-mode"]:checked');
|
|
// eslint-disable-next-line quotes
|
|
const methodInput = tool.querySelector('input[name="start-method"]:checked');
|
|
if (!(modeInput instanceof HTMLInputElement) || !(methodInput instanceof HTMLInputElement)) return;
|
|
mode = modeInput.value;
|
|
const method = methodInput.value;
|
|
|
|
try {
|
|
setDisabled(elements.startButton, true);
|
|
|
|
if (method === "hash") {
|
|
const hash = elements.commitHash instanceof HTMLInputElement ? elements.commitHash.value.trim() : "";
|
|
if (!/^[0-9a-fA-F]{7,40}$/.test(hash)) {
|
|
throw new Error("Please enter a valid commit hash (7-40 hex characters).");
|
|
}
|
|
|
|
// Fetch the commit to get its date
|
|
const commitData = await fetchJSON(`${API}/repos/${REPO}/commits/${hash}`);
|
|
if (!commitData) {
|
|
throw new Error("Commit not found. Check the hash and try again.");
|
|
}
|
|
|
|
const commitDate = new Date(commitData.commit.committer.date);
|
|
await loadCommitsAroundDate(commitDate);
|
|
|
|
startIndex = findCommitIndex(hash);
|
|
if (startIndex < 0) {
|
|
throw new Error("Commit not found in the master branch history.");
|
|
}
|
|
} else {
|
|
const dateStr = elements.commitDate instanceof HTMLInputElement ? elements.commitDate.value : "";
|
|
if (!dateStr) {
|
|
throw new Error("Please select a date.");
|
|
}
|
|
|
|
const date = new Date(dateStr + "T12:00:00Z");
|
|
if (date > new Date()) {
|
|
throw new Error("Date cannot be in the future.");
|
|
}
|
|
|
|
await loadCommitsAroundDate(date);
|
|
|
|
// Find the commit closest to the selected date
|
|
let closestIndex = 0;
|
|
let closestDiff = Infinity;
|
|
for (let i = 0; i < commits.length; i++) {
|
|
const diff = Math.abs(commits[i].date.getTime() - date.getTime());
|
|
if (diff < closestDiff) {
|
|
closestDiff = diff;
|
|
closestIndex = i;
|
|
}
|
|
}
|
|
startIndex = closestIndex;
|
|
}
|
|
|
|
// Reset state
|
|
goodIndex = -1;
|
|
badIndex = -1;
|
|
currentIndex = -1;
|
|
currentDeployUrl = undefined;
|
|
stepCount = 0;
|
|
history = [];
|
|
bisectPhase = "boundary";
|
|
boundaryOffset = 0;
|
|
boundarySearching = false;
|
|
elements.goBackButton?.classList.remove("hidden");
|
|
if (elements.testBuildButton instanceof HTMLElement) elements.testBuildButton.style.display = "";
|
|
if (elements.findings instanceof HTMLElement) elements.findings.style.display = "";
|
|
if (elements.bisectActions instanceof HTMLElement) elements.bisectActions.style.display = "";
|
|
|
|
// Show bisect phase and present the starting commit
|
|
showPhase("bisect");
|
|
await presentCommit(startIndex);
|
|
} catch (err) {
|
|
if (err instanceof Error) showMessage(err.message);
|
|
} finally {
|
|
setDisabled(elements.startButton, false);
|
|
}
|
|
});
|
|
|
|
// Test build button
|
|
elements.testBuildButton?.addEventListener("click", () => {
|
|
if (isDisabled(elements.testBuildButton)) return;
|
|
if (currentDeployUrl) {
|
|
window.open(currentDeployUrl, "_blank", "noopener");
|
|
}
|
|
});
|
|
|
|
// Issue response buttons
|
|
function onIssueResponse(/** @type {Element | null} */ button, /** @type {boolean} */ issuePresent) {
|
|
button?.addEventListener("click", async () => {
|
|
if (isDisabled(button)) return;
|
|
hideMessage();
|
|
try {
|
|
await handleUserResponse(issuePresent);
|
|
} catch (err) {
|
|
if (err instanceof Error) showMessage(err.message);
|
|
}
|
|
});
|
|
}
|
|
onIssueResponse(elements.issuePresentButton, true);
|
|
onIssueResponse(elements.issueAbsentButton, false);
|
|
|
|
// Go back
|
|
elements.goBackButton?.querySelector("a")?.addEventListener("click", async () => {
|
|
hideMessage();
|
|
|
|
if (history.length === 0) {
|
|
showPhase("setup");
|
|
return;
|
|
}
|
|
|
|
// Restore interactive elements that may have been hidden by showResult
|
|
if (elements.testBuildButton instanceof HTMLElement) elements.testBuildButton.style.display = "";
|
|
if (elements.findings instanceof HTMLElement) elements.findings.style.display = "";
|
|
if (elements.bisectActions instanceof HTMLElement) elements.bisectActions.style.display = "";
|
|
popHistory();
|
|
await presentCommit(currentIndex);
|
|
});
|
|
});
|