const view = document.getElementById("view"); const breadcrumbs = document.getElementById("breadcrumbs"); const repositoryForm = document.getElementById("repository-form"); const repositoryInput = document.getElementById("repository-input"); const repositoryCombobox = document.getElementById("repository-combobox"); const repositoryMenuButton = document.getElementById("repository-menu-button"); const repositoryPanel = document.getElementById("repository-panel"); const repositoryResults = document.getElementById("repository-results"); const accountButton = document.getElementById("account-button"); const accountAvatar = document.getElementById("account-avatar"); const accountFallback = document.getElementById("account-fallback"); const cacheLabel = document.getElementById("cache-label"); const toastRegion = document.getElementById("toast-region"); const capabilityToken = document.querySelector('meta[name="pr-artifact-explorer-token"]')?.content ?? ""; let bootstrapState = null; let currentArtifact = null; let currentPullContext = null; let currentPlayer = null; let currentPlayerIsPlaying = false; let artifactFilters = { context: null, query: "", status: "all", run: "latest", }; const artifactDownloadProgress = new Map(); const LARGE_ARTIFACT_PROGRESS_BYTES = 2 * 1024 * 1024; const MAX_VISIBLE_ARTIFACT_FILES = 1_000; const EXPAND_ALL_FILE_TREE_LIMIT = 200; const PULL_PAYLOAD_CACHE_FRESH_MS = 30 * 1_000; const PULL_PAYLOAD_CACHE_STALE_MS = 10 * 60 * 1_000; const PULL_PAYLOAD_CACHE_LIMIT = 30; const PULL_QUERY_QUALIFIERS = [ { name: "in", description: "Search within a field", values: [ ["title", "Pull request titles"], ["body", "Pull request descriptions"], ["comments", "Pull request comments"], ], }, { name: "is", description: "Filter by state or type", values: [ ["open", "Open pull requests"], ["closed", "Closed pull requests"], ["merged", "Merged pull requests"], ["unmerged", "Pull requests that are not merged"], ["draft", "Draft pull requests"], ["pr", "Pull requests only"], ], }, { name: "author", description: "Opened by a user", dynamicValues: "authors", }, { name: "assignee", description: "Assigned to a user", dynamicValues: "authors", }, { name: "label", description: "Has a label", dynamicValues: "labels", }, { name: "review", description: "Filter by review state", values: [ ["none", "No reviews"], ["required", "Review required"], ["approved", "Approved"], ["changes_requested", "Changes requested"], ], }, { name: "reviewed-by", description: "Reviewed by a user", dynamicValues: "authors", }, { name: "review-requested", description: "Review requested from a user", dynamicValues: "authors", }, { name: "draft", description: "Filter by draft state", values: [ ["true", "Draft pull requests"], ["false", "Ready for review"], ], }, { name: "status", description: "Filter by commit status", values: [ ["success", "Checks succeeded"], ["failure", "Checks failed"], ["pending", "Checks are pending"], ], }, { name: "no", description: "Exclude missing metadata", values: [ ["label", "No labels"], ["assignee", "No assignee"], ["milestone", "No milestone"], ["project", "No project"], ], }, { name: "sort", description: "Choose result order", values: [ ["updated-desc", "Recently updated first"], ["updated-asc", "Least recently updated first"], ["created-desc", "Newest first"], ["created-asc", "Oldest first"], ["comments-desc", "Most commented first"], ["comments-asc", "Least commented first"], ], }, { name: "base", description: "Targets a base branch" }, { name: "head", description: "Comes from a head branch" }, { name: "milestone", description: "Belongs to a milestone" }, { name: "mentions", description: "Mentions a user" }, { name: "commenter", description: "Commented on by a user" }, { name: "involves", description: "Involves a user" }, { name: "created", description: "Created on a date or range" }, { name: "updated", description: "Updated on a date or range" }, { name: "closed", description: "Closed on a date or range" }, { name: "merged", description: "Merged on a date or range" }, ]; const MANIFEST_FILE_NAMES = new Set([ "bun.lock", "bun.lockb", "cargo.lock", "cargo.toml", "composer.json", "composer.lock", "compose.yaml", "compose.yml", "deno.json", "deno.jsonc", "directory.build.props", "directory.build.targets", "dockerfile", "gemfile", "gemfile.lock", "global.json", "go.mod", "go.sum", "mix.exs", "mix.lock", "npm-shrinkwrap.json", "nuget.config", "package-lock.json", "package.json", "pipfile", "pipfile.lock", "pnpm-lock.yaml", "poetry.lock", "pubspec.yaml", "pyproject.toml", "yarn.lock", ]); const VENDORED_PATH_SEGMENTS = new Set([ ".pnpm", ".venv", "__pycache__", "bower_components", "node_modules", "site-packages", "third-party", "third_party", "vendor", "vendors", ]); const FILE_PATH_COLLATOR = new Intl.Collator("en", { numeric: true, sensitivity: "base", }); const pullPayloadCache = new Map(); let renderSequence = 0; let repositorySearchTimer = null; let repositorySearchController = null; let repositorySearchSequence = 0; let repositoryRemoteResults = []; let repositorySearchState = "idle"; let repositorySearchError = null; let repositoryActiveIndex = -1; let repositoryPickerQuery = ""; let pullAuthors = []; let pullAuthorsRepository = null; let pullAuthorsState = "idle"; let pullAuthorsError = null; let pullAuthorsLoadSequence = 0; let pullQueryLabels = []; let pullQuerySuggestionIndex = -1; let pullQueryPendingCommit = null; let pullTableRefreshSequence = 0; let pullFilterStorageWarningShown = false; const PULL_FILTER_STORAGE_KEY = "pr-artifact-explorer:pull-filters:v1"; const MAX_LOCAL_PULL_FILTER_REPOSITORIES = 50; const previewThemeMedia = window.matchMedia("(prefers-color-scheme: dark)"); function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function titleHtml(value) { const source = String(value ?? ""); let result = ""; let cursor = 0; while (cursor < source.length) { const opening = source.indexOf("`", cursor); if (opening === -1) { result += escapeHtml(source.slice(cursor)); break; } result += escapeHtml(source.slice(cursor, opening)); let runLength = 1; while (source[opening + runLength] === "`") runLength++; const delimiter = "`".repeat(runLength); let closing = source.indexOf(delimiter, opening + runLength); while ( closing !== -1 && (source[closing - 1] === "`" || source[closing + runLength] === "`") ) { closing = source.indexOf(delimiter, closing + runLength); } if (closing === -1) { result += escapeHtml(source.slice(opening)); break; } let code = source.slice(opening + runLength, closing).replace(/\s+/g, " "); if (code.startsWith(" ") && code.endsWith(" ") && code.trim()) { code = code.slice(1, -1); } result += `${escapeHtml(code)}`; cursor = closing + runLength; } return result; } function highlightXmlTag(token) { const opening = token.startsWith("") ? "/>" : ">"; const nameStart = opening.length; const name = token.slice(nameStart).match(/^[^\s/>]+/)?.[0]; if (!name) { return `${escapeHtml(token)}`; } const attributesStart = nameStart + name.length; const attributesEnd = token.length - ending.length; const attributes = token.slice(attributesStart, attributesEnd); const matcher = /([^\s=/>]+)(\s*=\s*)("(?:[^"]*)"|'(?:[^']*)'|[^\s>]+)/g; let highlightedAttributes = ""; let cursor = 0; for (const match of attributes.matchAll(matcher)) { highlightedAttributes += escapeHtml(attributes.slice(cursor, match.index)); highlightedAttributes += `${escapeHtml(match[1])}`; highlightedAttributes += `${escapeHtml(match[2])}`; highlightedAttributes += `${escapeHtml(match[3])}`; cursor = match.index + match[0].length; } highlightedAttributes += escapeHtml(attributes.slice(cursor)); return [ `${escapeHtml(opening)}`, `${escapeHtml(name)}`, highlightedAttributes, `${escapeHtml(ending)}`, ].join(""); } function highlightXml(value) { const source = String(value ?? ""); let highlighted = ""; let cursor = 0; while (cursor < source.length) { const opening = source.indexOf("<", cursor); if (opening === -1) { highlighted += escapeHtml(source.slice(cursor)); break; } highlighted += escapeHtml(source.slice(cursor, opening)); let closing; if (source.startsWith("", opening + 4); closing = index === -1 ? source.length : index + 3; } else if (source.startsWith("", opening + 9); closing = index === -1 ? source.length : index + 3; } else { let quote = null; let subsetDepth = 0; let foundClosing = false; closing = source.length; for (let index = opening + 1; index < source.length; index++) { const character = source[index]; if (quote) { if (character === quote) quote = null; continue; } if (character === '"' || character === "'") { quote = character; } else if (character === "[") { subsetDepth++; } else if (character === "]" && subsetDepth > 0) { subsetDepth--; } else if (character === ">" && subsetDepth === 0) { closing = index + 1; foundClosing = true; break; } } if (!foundClosing) { highlighted += escapeHtml(source.slice(opening)); break; } } const token = source.slice(opening, closing); if (token.startsWith("