diff --git a/src/content/github/common/fetchers.ts b/src/content/github/common/fetchers.ts index a61ace9..60b8e97 100644 --- a/src/content/github/common/fetchers.ts +++ b/src/content/github/common/fetchers.ts @@ -5,25 +5,6 @@ import { MessageType, } from "src/types"; -export async function getMetadata(url: string): Promise { - const response = await fetch(url, { - headers: { - "Accept": "application/json", - }, - }).then((response) => response.json()); - let branch = undefined; - if (response.payload.refInfo.refType === "branch") { - branch = response.payload.refInfo.name; - } - return { - owner: response.payload.repo.ownerLogin, - repo: response.payload.repo.name, - path: response.payload.path, - commit: response.payload.refInfo.currentOid, - branch: branch, - }; -} - export async function getFlags(metadata: FileMetadata): Promise { const payload = { service: "github", @@ -65,6 +46,11 @@ export async function getCommitReport( flag: string | undefined, component_id: string | undefined ): Promise { + // metadata.commit must be defined, check it before calling + if (!metadata.commit) { + throw new Error("getCommitReport called without commit sha"); + } + const payload = { service: "github", owner: metadata.owner, diff --git a/src/content/github/file/main.tsx b/src/content/github/file/main.tsx index 3857b6d..9d98fa1 100644 --- a/src/content/github/file/main.tsx +++ b/src/content/github/file/main.tsx @@ -1,4 +1,3 @@ -import React from "dom-chef"; import browser from "webextension-polyfill"; import alpha from "color-alpha"; import Drop from "tether-drop"; @@ -27,15 +26,14 @@ import { import { colors } from "../common/constants"; import { createDropdown } from "./utils/dropdown"; import { - getMetadata, getComponents, getCommitReport, getFlags, getBranchReport, } from "../common/fetchers"; import { print } from "src/utils"; +import Sentry from "../../common/sentry"; import { isFileUrl } from "../common/utils"; -import Sentry from '../../common/sentry'; const globals: { coverageReport?: FileCoverageReport; @@ -47,7 +45,7 @@ const globals: { prompt?: HTMLElement; } = {}; -init() +init(); function init(): Promise { // this event discovered by "reverse-engineering GitHub" @@ -63,21 +61,60 @@ function init(): Promise { async function main(): Promise { try { - if (!isFileUrl(document.URL)) { + const urlMetadata = getMetadataFromURL(); + if (!urlMetadata) { print("file not detected at current URL"); return; } + globals.coverageButton = createCoverageButton(); + process(urlMetadata); + } catch (e) { + Sentry.captureException(e); + throw e; + } +} - let metadata: FileMetadata; - metadata = await getMetadata(document.URL); +function getMetadataFromURL(): FileMetadata | null { + const regexp = + /\/(?.+?)\/(?.+?)\/blob\/(?.+?)\/(?.+?)$/; + const matches = regexp.exec(window.location.pathname); + const groups = matches?.groups; + if (!groups) { + return null; + } - globals.coverageButton = createCoverageButton(); + const branch = groups.branch; + const commitMatch = branch.match(/[\da-f]+/); - process(metadata) - } catch (e) { - Sentry.captureException(e) - throw e + // branch could be a commit sha + if ( + commitMatch && + commitMatch[0].length == branch.length && + (groups.branch.length === 40 || branch.length === 7) + ) { + // branch is actually a commit sha + let commit = branch; + + // if it's a short sha, we need to get the full sha + if (commit.length === 7) { + const commitLink = document.querySelector( + `[href^="/${groups.owner}/${groups.repo}/tree/${commit}"]` + ); + if (!commitLink) + throw new Error("Could not find commit link from short sha"); + const longSha = commitLink + .getAttribute("href") + ?.match(/[\da-f]{40}/)?.[0]; + if (!longSha) throw new Error("Could not get long sha from commit link"); + commit = longSha; + } + + return { + ...groups, + commit, + }; } + return groups; } async function process(metadata: FileMetadata): Promise { @@ -111,17 +148,16 @@ async function process(metadata: FileMetadata): Promise { previousElement: globals.coverageButton!, selectedOptions: selectedFlags, onClick: handleFlagClick, - }) - .then(({ button, list }) => { - globals.flagsButton = button; - globals.flagsDrop = new Drop({ - target: button, - content: list, - classes: "drop-theme-arrows codecov-z1 codecov-bg-white", - position: "bottom right", - openOn: "click", - }); - }) + }).then(({ button, list }) => { + globals.flagsButton = button; + globals.flagsDrop = new Drop({ + target: button, + content: list, + classes: "drop-theme-arrows codecov-z1 codecov-bg-white", + position: "bottom right", + openOn: "click", + }); + }); } const components = await getComponents(metadata); @@ -134,7 +170,7 @@ async function process(metadata: FileMetadata): Promise { return []; }); - // TODO: allow setting selected flags for different files at the same time + // TODO: allow setting selected components for different files at the same time if ( selectedComponents.length > 0 && _.intersection(components, selectedComponents).length === 0 @@ -151,35 +187,45 @@ async function process(metadata: FileMetadata): Promise { previousElement: globals.coverageButton!, onClick: handleComponentClick, selectedOptions: selectedComponents, - }) - .then(({ button, list }) => { - globals.componentsButton = button; - globals.componentsDrop = new Drop({ - target: button, - content: list, - classes: "drop-theme-arrows codecov-z1 codecov-bg-white", - position: "bottom right", - openOn: "click", - }); - }) + }).then(({ button, list }) => { + globals.componentsButton = button; + globals.componentsDrop = new Drop({ + target: button, + content: list, + classes: "drop-theme-arrows codecov-z1 codecov-bg-white", + position: "bottom right", + openOn: "click", + }); + }); } + // If commit sha is defined use that, otherwise just branch name + const getReportFn = metadata.commit ? getCommitReport : getBranchReport; + let coverageReportResponses: Array; try { - if (selectedFlags?.length > 0) { + if (selectedFlags?.length > 0 && selectedComponents?.length > 0) { + coverageReportResponses = await Promise.all( + selectedFlags.flatMap((flag) => + selectedComponents.map((component) => + getReportFn(metadata, flag, component) + ) + ) + ); + } else if (selectedFlags?.length > 0) { coverageReportResponses = await Promise.all( - selectedFlags.map((flag) => getCommitReport(metadata, flag, undefined)) + selectedFlags.map((flag) => getReportFn(metadata, flag, undefined)) ); } else if (selectedComponents?.length > 0) { coverageReportResponses = await Promise.all( selectedComponents.map((component) => - getCommitReport(metadata, undefined, component) + getReportFn(metadata, undefined, component) ) ); } else { - coverageReportResponses = await Promise.all([ - await getCommitReport(metadata, undefined, undefined), - ]); + coverageReportResponses = [ + await getReportFn(metadata, undefined, undefined), + ]; } } catch (e) { updateButton(`Coverage: ⚠`); @@ -220,7 +266,6 @@ async function process(metadata: FileMetadata): Promise { if (_.isEmpty(coverageReport)) { updateButton(`Coverage: N/A`); globals.coverageReport = {}; - await promptPastReport(metadata); return; } @@ -232,40 +277,6 @@ async function process(metadata: FileMetadata): Promise { animateAndAnnotateLines(noVirtLineSelector, annotateLine); } -async function promptPastReport(metadata: FileMetadata): Promise { - if (!metadata.branch) { - return; - } - const response = await getBranchReport(metadata); - const regexp = /app.codecov.io\/github\/.*\/.*\/commit\/(?.*)\/blob/; - const matches = regexp.exec(response.commit_file_url); - const commit = matches?.groups?.commit; - if (!commit) { - throw new Error("Could not parse commit hash from response for past coverage report") - } - const link = document.URL.replace( - `blob/${metadata.branch}`, - `blob/${commit}` - ); - globals.prompt = createPrompt( - - Coverage report not available for branch HEAD ( - {metadata.commit.substr(0, 7)}), most recent coverage report for this - branch available at commit {commit.substr(0, 7)} - - ); -} - -function createPrompt(child: any) { - const ref = document.querySelector('[data-testid="latest-commit"]') - ?.parentElement?.parentElement; - if (!ref) { - throw new Error("Could not find reference element to render prompt") - } - const prompt =
{child}
; - return ref.insertAdjacentElement("afterend", prompt) as HTMLElement; -} - function createCoverageButton() { const rawButton = document.querySelector('[data-testid="raw-button"]'); if (!rawButton) { diff --git a/src/content/github/file/utils/dropdown.tsx b/src/content/github/file/utils/dropdown.tsx index eb67632..8d03a6e 100644 --- a/src/content/github/file/utils/dropdown.tsx +++ b/src/content/github/file/utils/dropdown.tsx @@ -17,15 +17,34 @@ export async function createDropdown({ previousElement: HTMLElement; selectedOptions: string[]; }) { - const editButton = document - .querySelector('[data-testid="more-edit-button"]')! - .closest("div")!; - const dropdownButton = editButton.cloneNode(true) as HTMLElement; - const textNode: HTMLElement = dropdownButton.querySelector('[data-component="IconButton"]')!; + // Build the button out of the Raw/copy/download button group + const rawButton = document + .querySelector('[data-testid="download-raw-button"]')! + .closest("div"); + if (!rawButton) throw new Error("Could not find raw button group"); + const dropdownButton = rawButton.cloneNode(true) as HTMLElement; + // Remove copy button + const copyButton = dropdownButton.querySelector( + '[data-testid="copy-raw-button"]' + ); + if (!copyButton) throw new Error("Could not find copy button"); + dropdownButton.removeChild(copyButton); + // Replace download button with dropdown button + const downloadButton = dropdownButton.querySelector( + '[data-testid="download-raw-button"]' + ); + if (!downloadButton || !downloadButton.firstChild) + throw new Error("Could not find download button or it is missing children"); + const triangleDownSvg = document.querySelector(".octicon-triangle-down"); + if (!triangleDownSvg) throw new Error("Could not find triangle down svg"); + downloadButton.replaceChild(triangleDownSvg, downloadButton.firstChild); + + const textNode = dropdownButton.querySelector('[data-testid="raw-button"]'); + if (!textNode || !textNode.parentElement) + throw new Error("Could not find textNode"); textNode.innerHTML = ""; textNode.ariaDisabled = "false"; - textNode.parentElement!.ariaLabel = tooltip; - textNode.style.padding = `0 ${title.length * 5}px`; + textNode.parentElement.ariaLabel = tooltip; textNode.appendChild({title}); previousElement.insertAdjacentElement("afterend", dropdownButton); diff --git a/src/types.ts b/src/types.ts index 769435e..c008e4f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,5 @@ -export type FileMetadata = { - owner: string; - repo: string; - path: string; - commit: string; - branch: string | undefined; -}; +export type FileMetadata = { [key: string]: string }; +export type PRMetadata = { [key: string]: string }; export enum CoverageStatus { COVERED,