├── icon.png ├── mocha.opts ├── screenshot.png ├── .gitignore ├── .vscode ├── extensions.json ├── settings.json └── tasks.json ├── .prettierignore ├── .eslintrc.json ├── .prettierrc ├── tsconfig.dist.json ├── src ├── colors.ts ├── uri.ts ├── decoration.ts ├── uri.test.ts ├── settings.test.ts ├── model.ts ├── settings.ts ├── insights.ts ├── api.ts └── extension.ts ├── .editorconfig ├── .travis.yml ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecov/sourcegraph-codecov/HEAD/icon.png -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --watch-extensions ts 3 | --timeout 200 4 | src/**/*.test.ts -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codecov/sourcegraph-codecov/HEAD/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | dist/ 3 | lib/ 4 | node_modules/ 5 | /.nyc_output/ 6 | yarn.lock 7 | /coverage/ 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "EditorConfig.EditorConfig"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | coverage/ 4 | .cache/ 5 | lib/ 6 | dist/ 7 | package.json 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sourcegraph/eslint-config", 3 | "parserOptions": { 4 | "project": "tsconfig.json" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "useTabs": false, 6 | "printWidth": 120, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".cache/": true, 4 | "dist/": true, 5 | "lib/": true 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false 5 | }, 6 | "include": ["src/**/*"], 7 | "exclude": ["src/**/*.test.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/colors.ts: -------------------------------------------------------------------------------- 1 | export function hsl(hue: number | string, saturation: number, lightness: number): string { 2 | const sat = Math.floor(saturation * 100) 3 | const light = Math.floor(lightness * 100) 4 | return `hsl(${hue}, ${sat}%, ${light}%)` 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | insert_final_newline = true 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.{json,js,yml}] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | sudo: false 4 | 5 | node_js: 6 | - '12' 7 | 8 | script: 9 | - npm run typecheck 10 | - npm run eslint 11 | - npm run prettier:check 12 | - npm run cover 13 | - nyc report --reporter json 14 | - 'bash <(curl -s https://codecov.io/bash)' 15 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch:typecheck", 7 | "problemMatcher": ["$tsc-watch"], 8 | "isBackground": true 9 | }, 10 | { 11 | "type": "npm", 12 | "script": "serve", 13 | "problemMatcher": [] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": ["es2020", "webworker"], 7 | "sourceMap": true, 8 | "allowUnreachableCode": false, 9 | "esModuleInterop": true, 10 | "allowUnusedLabels": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "noImplicitAny": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": false, 17 | "strictFunctionTypes": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": true, 20 | "noErrorTruncation": true, 21 | "skipLibCheck": true, 22 | "declaration": false, 23 | "outDir": "lib", 24 | "noEmit": true, 25 | "plugins": [ 26 | { 27 | "name": "tslint-language-service" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Sourcegraph Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/uri.ts: -------------------------------------------------------------------------------- 1 | import { APIOptions, CommitSpec, RepoSpec } from './api' 2 | import { resolveEndpoint, Settings } from './settings' 3 | 4 | /** 5 | * A resolved URI without an identified path. 6 | */ 7 | export interface ResolvedRootURI { 8 | repo: string 9 | rev: string 10 | } 11 | 12 | /** 13 | * A resolved URI with an identified path in a repository at a specific revision. 14 | */ 15 | export interface ResolvedDocumentURI extends ResolvedRootURI { 16 | path: string 17 | } 18 | 19 | /** 20 | * Resolve a URI of the form git://github.com/owner/repo?rev to an absolute reference. 21 | */ 22 | export function resolveRootURI(uri: string): ResolvedRootURI { 23 | const url = new URL(uri) 24 | if (url.protocol !== 'git:') { 25 | throw new Error(`Unsupported protocol: ${url.protocol}`) 26 | } 27 | const repo = (url.host + url.pathname).replace(/^\/*/, '') 28 | const rev = url.search.slice(1) 29 | if (!rev) { 30 | throw new Error('Could not determine revision') 31 | } 32 | return { repo, rev } 33 | } 34 | 35 | /** 36 | * Resolve a URI of the form git://github.com/owner/repo?rev#path to an absolute reference. 37 | */ 38 | export function resolveDocumentURI(uri: string): ResolvedDocumentURI { 39 | return { 40 | ...resolveRootURI(uri), 41 | path: new URL(uri).hash.slice(1), 42 | } 43 | } 44 | 45 | const knownHosts = [ 46 | { name: 'github.com', service: 'gh' }, 47 | { name: 'gitlab.com', service: 'gl' }, 48 | { name: 'bitbucket.org', service: 'bb' }, 49 | ] as const 50 | 51 | /** 52 | * Returns the URL parameters used to access the Codecov API for the URI's repository. 53 | * 54 | * Currently only GitHub.com repositories are supported. 55 | */ 56 | export function codecovParamsForRepositoryCommit( 57 | uri: Pick, 58 | sourcegraph: typeof import('sourcegraph') 59 | ): RepoSpec & CommitSpec & APIOptions { 60 | const endpoint = resolveEndpoint(sourcegraph.configuration.get().get('codecov.endpoints')) 61 | 62 | const knownHost = knownHosts.find(knownHost => uri.repo.includes(knownHost.name)) 63 | 64 | const service = knownHost?.service || endpoint.service || 'gh' 65 | 66 | const [, owner, repo] = uri.repo.split('/', 4) 67 | 68 | return { 69 | baseURL: endpoint.url, 70 | service, 71 | owner, 72 | repo, 73 | sha: uri.rev, 74 | token: endpoint.token, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/decoration.ts: -------------------------------------------------------------------------------- 1 | import { Range, TextDocumentDecoration } from 'sourcegraph' 2 | import { hsl } from './colors' 3 | import { FileLineCoverage, LineCoverage } from './model' 4 | import { Settings } from './settings' 5 | 6 | export function codecovToDecorations( 7 | settings: Pick, 8 | data: FileLineCoverage 9 | ): TextDocumentDecoration[] { 10 | if (!data) { 11 | return [] 12 | } 13 | const decorations: TextDocumentDecoration[] = [] 14 | for (const [lineStr, coverage] of Object.entries(data)) { 15 | if (coverage === null) { 16 | continue 17 | } 18 | const line = parseInt(lineStr, 10) - 1 // 0-indexed line 19 | const decoration: TextDocumentDecoration = { 20 | range: new Range(line, 0, line, 0), 21 | isWholeLine: true, 22 | } 23 | if (settings['codecov.decorations.lineCoverage']) { 24 | decoration.light = { 25 | backgroundColor: lineColor(coverage, 0.8), 26 | } 27 | decoration.dark = { 28 | backgroundColor: lineColor(coverage, 0.2), 29 | } 30 | } 31 | if (settings['codecov.decorations.lineHitCounts']) { 32 | decoration.after = { 33 | ...lineText(coverage), 34 | backgroundColor: lineColor(coverage, 0.4), 35 | color: 'white', 36 | } 37 | } 38 | decorations.push(decoration) 39 | } 40 | return decorations 41 | } 42 | 43 | function lineColor(coverage: LineCoverage, lightness: number): string { 44 | if (coverage === 0 || coverage === null) { 45 | return hsl(0, 1, lightness) // red 46 | } 47 | if (typeof coverage === 'number' || coverage.hits === coverage.branches) { 48 | return hsl(120, 0.64, lightness) // green 49 | } 50 | return hsl(62, 0.97, lightness) // partially covered, yellow 51 | } 52 | 53 | function lineText(coverage: LineCoverage): { contentText?: string; hoverMessage?: string } { 54 | if (coverage === null) { 55 | return {} 56 | } 57 | if (typeof coverage === 'number') { 58 | if (coverage >= 1) { 59 | return { 60 | contentText: ` ${coverage} `, 61 | hoverMessage: `${coverage} hit${coverage === 1 ? '' : 's'} (CodeCov)`, 62 | } 63 | } 64 | return { 65 | contentText: ' 0 ', 66 | hoverMessage: 'not covered by test (CodeCov)', 67 | } 68 | } 69 | return { 70 | contentText: ` ${coverage.hits}/${coverage.branches} `, 71 | hoverMessage: `${coverage.hits}/${coverage.branches} branch${ 72 | coverage.branches === 1 ? '' : 'es' 73 | } hit (CodeCov)`, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/uri.test.ts: -------------------------------------------------------------------------------- 1 | import { createStubSourcegraphAPI } from '@sourcegraph/extension-api-stubs' 2 | import mock from 'mock-require' 3 | mock('sourcegraph', createStubSourcegraphAPI()) 4 | 5 | import * as assert from 'assert' 6 | import { codecovParamsForRepositoryCommit, resolveDocumentURI } from './uri' 7 | 8 | describe('resolveDocumentURI', () => { 9 | const UNSUPPORTED_SCHEMES = ['file:', 'http:', 'https:'] 10 | 11 | for (const p of UNSUPPORTED_SCHEMES) { 12 | it(`throws for ${p} uris`, () => { 13 | assert.throws( 14 | () => resolveDocumentURI('git://github.com/sourcegraph/sourcegraph'), 15 | `Invalid protocol: ${p}` 16 | ) 17 | }) 18 | } 19 | 20 | it('throws if url.search is falsy', () => { 21 | assert.throws(() => resolveDocumentURI('git://github.com/sourcegraph/sourcegraph')) 22 | }) 23 | 24 | it('throws if url.hash is falsy', () => { 25 | assert.throws(() => resolveDocumentURI('git://github.com/sourcegraph/sourcegraph')) 26 | }) 27 | 28 | it('resolves git: URIs', () => { 29 | assert.deepStrictEqual( 30 | resolveDocumentURI( 31 | 'git://github.com/sourcegraph/sourcegraph?a8215fe4bd9571b43d7a03277069445adca85b2a#pkg/extsvc/github/codehost.go' 32 | ), 33 | { 34 | path: 'pkg/extsvc/github/codehost.go', 35 | repo: 'github.com/sourcegraph/sourcegraph', 36 | rev: 'a8215fe4bd9571b43d7a03277069445adca85b2a', 37 | } 38 | ) 39 | }) 40 | }) 41 | 42 | describe('codecovParamsForRepo', () => { 43 | const sourcegraph = createStubSourcegraphAPI() 44 | 45 | it('handles valid GitHub.com repositories', () => 46 | assert.deepStrictEqual( 47 | codecovParamsForRepositoryCommit( 48 | { 49 | repo: 'github.com/owner/repo', 50 | rev: 'v', 51 | }, 52 | sourcegraph 53 | ), 54 | { 55 | baseURL: 'https://codecov.io', 56 | service: 'gh', 57 | owner: 'owner', 58 | repo: 'repo', 59 | sha: 'v', 60 | token: undefined, 61 | } 62 | )) 63 | 64 | it('defaults to gh when the service cannot be determined', () => 65 | assert.deepStrictEqual( 66 | codecovParamsForRepositoryCommit( 67 | { 68 | repo: 'example.com/owner/repo', 69 | rev: 'v', 70 | }, 71 | sourcegraph 72 | ), 73 | { 74 | baseURL: 'https://codecov.io', 75 | owner: 'owner', 76 | repo: 'repo', 77 | service: 'gh', 78 | sha: 'v', 79 | token: undefined, 80 | } 81 | )) 82 | }) 83 | -------------------------------------------------------------------------------- /src/settings.test.ts: -------------------------------------------------------------------------------- 1 | import { createStubSourcegraphAPI } from '@sourcegraph/extension-api-stubs' 2 | import mock from 'mock-require' 3 | mock('sourcegraph', createStubSourcegraphAPI()) 4 | 5 | import * as assert from 'assert' 6 | import { resolveSettings, Settings } from './settings' 7 | 8 | describe('Settings', () => { 9 | describe('decorations', () => { 10 | it('applies defaults when not set', () => { 11 | assert.deepStrictEqual(resolveSettings({})['codecov.decorations.lineCoverage'], false) 12 | }) 13 | 14 | it('respects the hide property', () => { 15 | assert.deepStrictEqual( 16 | resolveSettings({ 17 | 'codecov.showCoverage': true, 18 | 'codecov.decorations.lineCoverage': true, 19 | 'codecov.decorations.lineHitCounts': true, 20 | })['codecov.showCoverage'], 21 | true 22 | ) 23 | }) 24 | 25 | it('respects the other properties', () => { 26 | const settings: Settings = { 27 | 'codecov.insight.sunburst': false, 28 | 'codecov.insight.icicle': false, 29 | 'codecov.insight.tree': false, 30 | 'codecov.insight.pie': false, 31 | 'codecov.decorations.lineCoverage': false, 32 | 'codecov.decorations.lineHitCounts': true, 33 | 'codecov.showCoverage': true, 34 | 'codecov.endpoints': [ 35 | { 36 | url: 'https://codecov.io', 37 | }, 38 | ], 39 | 'codecov.fileDecorations.low': 70, 40 | 'codecov.fileDecorations.high': 85, 41 | 'codecov.fileDecorations.optimum': 100, 42 | 'codecov.fileDecorations.show': false, 43 | } 44 | assert.deepStrictEqual( 45 | resolveSettings({ 46 | 'codecov.decorations.lineCoverage': false, 47 | 'codecov.decorations.lineHitCounts': true, 48 | }), 49 | settings 50 | ) 51 | }) 52 | 53 | it('applies defaults for the other properties', () => { 54 | const settings: Settings = { 55 | 'codecov.insight.tree': false, 56 | 'codecov.insight.icicle': false, 57 | 'codecov.insight.sunburst': false, 58 | 'codecov.insight.pie': false, 59 | 'codecov.decorations.lineCoverage': false, 60 | 'codecov.decorations.lineHitCounts': false, 61 | 'codecov.showCoverage': true, 62 | 'codecov.endpoints': [ 63 | { 64 | url: 'https://codecov.io', 65 | }, 66 | ], 67 | 'codecov.fileDecorations.low': 70, 68 | 'codecov.fileDecorations.high': 85, 69 | 'codecov.fileDecorations.optimum': 100, 70 | 'codecov.fileDecorations.show': false, 71 | } 72 | assert.deepStrictEqual(resolveSettings({}), settings) 73 | }) 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { CodecovCommitData, getCommitCoverage } from './api' 2 | import { Endpoint } from './settings' 3 | import { codecovParamsForRepositoryCommit, ResolvedDocumentURI, ResolvedRootURI } from './uri' 4 | 5 | export interface FileLineCoverage { 6 | [line: string]: LineCoverage 7 | } 8 | 9 | export type LineCoverage = number | { hits: number; branches: number } | null 10 | 11 | /** Gets the coverage ratio for a commit. */ 12 | export async function getCommitCoverageRatio( 13 | { repo, rev }: Pick, 14 | endpoint: Endpoint, 15 | sourcegraph: typeof import('sourcegraph') 16 | ): Promise { 17 | const data = await getCommitCoverage({ 18 | ...codecovParamsForRepositoryCommit({ repo, rev }, sourcegraph), 19 | baseURL: endpoint.url, 20 | token: endpoint.token, 21 | }) 22 | if (data === null) { 23 | return null 24 | } 25 | return data.commit.totals.coverage 26 | } 27 | 28 | /** Gets line coverage data for a file at a given commit in a repository. */ 29 | export async function getFileLineCoverage( 30 | { repo, rev, path }: ResolvedDocumentURI, 31 | endpoint: Endpoint, 32 | sourcegraph: typeof import('sourcegraph') 33 | ): Promise { 34 | const data = await getCommitCoverage({ 35 | ...codecovParamsForRepositoryCommit({ repo, rev }, sourcegraph), 36 | baseURL: endpoint.url, 37 | token: endpoint.token, 38 | }) 39 | if (data === null) { 40 | return null 41 | } 42 | return toLineCoverage(data, path) 43 | } 44 | 45 | /** Gets the file coverage ratios for all files at a given commit in a repository. */ 46 | export async function getFileCoverageRatios( 47 | { repo, rev }: Pick, 48 | endpoint: Endpoint, 49 | sourcegraph: typeof import('sourcegraph') 50 | ): Promise<{ [path: string]: number } | null> { 51 | const data = await getCommitCoverage({ 52 | ...codecovParamsForRepositoryCommit({ repo, rev }, sourcegraph), 53 | baseURL: endpoint.url, 54 | token: endpoint.token, 55 | }) 56 | if (data === null) { 57 | return null 58 | } 59 | const ratios: { [path: string]: number } = {} 60 | for (const [path, fileData] of Object.entries(data.commit.report.files)) { 61 | const ratio = toCoverageRatio(fileData) 62 | if (ratio !== undefined) { 63 | ratios[path] = ratio 64 | } 65 | } 66 | return ratios 67 | } 68 | 69 | function toLineCoverage(data: CodecovCommitData, path: string): FileLineCoverage { 70 | const result: FileLineCoverage = {} 71 | const fileData = data.commit.report.files[path] 72 | if (fileData) { 73 | for (const [lineStr, value] of Object.entries(fileData.l)) { 74 | const line = parseInt(lineStr, 10) 75 | if (typeof value === 'number' || value === null) { 76 | result[line] = value 77 | } else if (typeof value === 'string') { 78 | const [hits, branches] = value.split('/', 2).map(v => parseInt(v, 10)) 79 | result[line] = { hits, branches } 80 | } 81 | } 82 | } 83 | return result 84 | } 85 | 86 | function toCoverageRatio(fileData: CodecovCommitData['commit']['report']['files'][string]): number | undefined { 87 | const ratioStr = fileData?.t.c 88 | if (!ratioStr) { 89 | return undefined 90 | } 91 | const ratio = parseFloat(ratioStr) 92 | if (Number.isNaN(ratio)) { 93 | return undefined 94 | } 95 | return ratio 96 | } 97 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { map } from 'rxjs/operators' 3 | import * as sourcegraph from 'sourcegraph' 4 | import { Service } from './api' 5 | 6 | export interface InsightSettings { 7 | ['codecov.insight.icicle']: boolean 8 | ['codecov.insight.tree']: boolean 9 | ['codecov.insight.sunburst']: boolean 10 | ['codecov.insight.pie']: boolean 11 | } 12 | 13 | export interface FileDecorationSettings { 14 | ['codecov.fileDecorations.low']: number 15 | ['codecov.fileDecorations.high']: number 16 | ['codecov.fileDecorations.optimum']: number 17 | ['codecov.fileDecorations.show']: boolean 18 | } 19 | 20 | /** 21 | * The resolved and normalized settings for this extension, the result of calling resolveSettings on a raw settings 22 | * value. 23 | * 24 | * See the configuration JSON Schema in extension.json for the canonical documentation on these properties. 25 | */ 26 | export interface Settings extends InsightSettings, FileDecorationSettings { 27 | ['codecov.showCoverage']: boolean 28 | ['codecov.decorations.lineCoverage']: boolean 29 | ['codecov.decorations.lineHitCounts']: boolean 30 | ['codecov.endpoints']: Endpoint[] 31 | } 32 | 33 | /** Returns a copy of the extension settings with values normalized and defaults applied. */ 34 | export function resolveSettings(raw: Partial): Settings { 35 | return { 36 | ['codecov.insight.icicle']: raw['codecov.insight.icicle'] || false, 37 | ['codecov.insight.tree']: raw['codecov.insight.tree'] || false, 38 | ['codecov.insight.sunburst']: raw['codecov.insight.sunburst'] || false, 39 | ['codecov.insight.pie']: raw['codecov.insight.pie'] || false, 40 | ['codecov.showCoverage']: raw['codecov.showCoverage'] !== false, 41 | ['codecov.decorations.lineCoverage']: !!raw['codecov.decorations.lineCoverage'], 42 | ['codecov.decorations.lineHitCounts']: !!raw['codecov.decorations.lineHitCounts'], 43 | ['codecov.endpoints']: [resolveEndpoint(raw['codecov.endpoints'])], 44 | ['codecov.fileDecorations.low']: 45 | typeof raw['codecov.fileDecorations.low'] === 'number' ? raw['codecov.fileDecorations.low'] : 70, 46 | ['codecov.fileDecorations.high']: 47 | typeof raw['codecov.fileDecorations.high'] === 'number' ? raw['codecov.fileDecorations.high'] : 85, 48 | ['codecov.fileDecorations.optimum']: 49 | typeof raw['codecov.fileDecorations.optimum'] === 'number' ? raw['codecov.fileDecorations.optimum'] : 100, 50 | ['codecov.fileDecorations.show']: raw['codecov.fileDecorations.show'] || false, 51 | } 52 | } 53 | 54 | export interface Endpoint { 55 | url: string 56 | token?: string 57 | service?: Service 58 | } 59 | 60 | export const CODECOV_IO_URL = 'https://codecov.io' 61 | 62 | /** 63 | * Returns the configured endpoint with values normalized and defaults applied. 64 | * 65 | * @todo support more than 1 endpoint 66 | */ 67 | export function resolveEndpoint(endpoints?: Readonly): Readonly { 68 | if (!endpoints || endpoints.length === 0) { 69 | return { url: CODECOV_IO_URL } 70 | } 71 | return { 72 | url: endpoints[0].url ? urlWithOnlyProtocolAndHost(endpoints[0].url) : CODECOV_IO_URL, 73 | token: endpoints[0].token || undefined, 74 | service: endpoints[0].service || undefined, 75 | } 76 | } 77 | 78 | function urlWithOnlyProtocolAndHost(urlStr: string): string { 79 | const url = new URL(urlStr) 80 | return `${url.protocol}//${url.host}` 81 | } 82 | 83 | /** 84 | * The extension's resolved Settings. 85 | */ 86 | export const configurationChanges: Observable = new Observable(observer => 87 | sourcegraph.configuration.subscribe(observer.next.bind(observer)) 88 | ).pipe(map(() => resolveSettings(sourcegraph.configuration.get>().value))) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codecov Sourcegraph extension 2 | 3 | [![Build Status](https://travis-ci.org/codecov/sourcegraph-codecov.svg?branch=master)](https://travis-ci.org/codecov/sourcegraph-codecov) 4 | [![codecov](https://codecov.io/gh/codecov/sourcegraph-codecov/branch/master/graph/badge.svg)](https://codecov.io/gh/codecov/sourcegraph-codecov) 5 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcodecov%2Fsourcegraph-codecov.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcodecov%2Fsourcegraph-codecov?ref=badge_shield) 6 | 7 | ## ⚠️ Deprecation notice 8 | 9 | **Sourcegraph extensions have been deprecated with the September 2022 Sourcegraph 10 | release. [Learn more](https://docs.sourcegraph.com/extensions/deprecation).** 11 | 12 | The repo and the docs below are kept to support older Sourcegraph versions. 13 | 14 | ## Description 15 | 16 | A [Sourcegraph extension](https://docs.sourcegraph.com/extensions) for showing code coverage information from [Codecov](https://codecov.io) on GitHub, Sourcegraph, and other tools. 17 | 18 | [**🗃️ Source code**](https://github.com/codecov/sourcegraph-codecov) 19 | 20 | [**➕ Add to Sourcegraph**](https://sourcegraph.com/extensions/sourcegraph/codecov) 21 | 22 | ## Features 23 | 24 | - Support for GitHub.com and Sourcegraph.com 25 | - Line coverage overlays on files (with green/yellow/red background colors) 26 | - Line branches/hits annotations on files 27 | - File coverage ratio indicator (`Coverage: N%`) and toggle button 28 | - Support for using a Codecov API token to see coverage for private repositories 29 | - File and directory coverage decorations on Sourcegraph 30 | 31 | ## Usage 32 | 33 | ### On GitHub using the Chrome extension 34 | 35 | 1. Install [Sourcegraph for Chrome](https://chrome.google.com/webstore/detail/sourcegraph/dgjhfomjieaadpoljlnidmbgkdffpack) 36 | 1. [Enable the Codecov extension on Sourcegraph](https://sourcegraph.com/extensions/sourcegraph/codecov) 37 | 1. Visit [tuf_store.go in theupdateframework/notary on GitHub](https://github.com/theupdateframework/notary/blob/master/server/storage/tuf_store.go) (or any other file in a public repository that has Codecov code coverage) 38 | 1. Click the `Coverage: N%` button to toggle Codecov test coverage background colors on the file (scroll down if they aren't immediately visible) 39 | 40 | ![Screenshot](https://user-images.githubusercontent.com/1976/45107396-53d56880-b0ee-11e8-96e9-ca83e991101c.png) 41 | 42 | #### With private GitHub.com repositories 43 | 44 | You can use the Codecov extension for private repositories on GitHub.com. Your code is never sent to Sourcegraph. 45 | 46 | 1. Follow the [Codecov extension usage instructions](https://github.com/codecov/sourcegraph-codecov#usage) above to install Sourcegraph for Chrome 47 | 2. Go to the command palette on GitHub (added by the Sourcegraph browser extension, see screenshot below) and choose "Codecov: Set API token for private repositories" 48 | 3. Enter your Codecov API token 49 | 4. Visit any file in a GitHub.com private repository that has Codecov coverage data 50 | 51 | ![image](https://user-images.githubusercontent.com/1976/45338265-04a19480-b541-11e8-9b35-517f3bbff530.png) 52 | 53 | Your code is never sent to Sourcegraph. The Codecov extension runs on the client side in a Web Worker and communicates with Codecov directly to retrieve code coverage data. 54 | 55 | #### With Codecov Enterprise and GitHub Enterprise 56 | 57 | You can use this extension to overlay coverage information from your Codecov Enterprise install into GitHub Enterprise. 58 | 59 | 1. Follow the [Codecov extension usage instructions](https://github.com/codecov/sourcegraph-codecov#usage) above to install Sourcegraph for Chrome 60 | 2. From the command palette (added by the Sourcegraph browser extension, see screenshot above) on GitHub Enterprise click, "Codecov: Setup up Codecov Enterprise" 61 | 3. From the pop up that appears, set your Version control type to: `ghe` 62 | 4. From the next pop up that appears, set your Codecov endpoint, this is just the root level of your Codecov Enterprise domain, e.g., `https://codecov.mycompany.com`. 63 | 5. Go to the command palette on GitHub and choose "Codecov: Set API token for private repositories" 64 | 6. Enter your Codecov Enterprise API token. 65 | 7. Visit any file in your Github Enterprise install with coverage data uploaded to Codecov Enterprise to see coverage data. 66 | 67 | Note: Additional documentation, if needed, can be found in [Codecov's official documentation](https://docs.codecov.io/docs/browser-extension#section-additional-steps-for-on-premises-codecov-customers). 68 | 69 | ### On Sourcegraph.com 70 | 71 | 1. Visit [tuf_store.go in theupdateframework/notary](https://sourcegraph.com/github.com/theupdateframework/notary@fb795b0bc868746ed2efa2cd7109346bc7ddf0a4/-/blob/server/storage/tuf_store.go) on Sourcegraph.com (or any other file that has Codecov code coverage) 72 | 2. Click the `Coverage: N%` button to toggle Codecov test coverage background colors on the file (sign-in required) 73 | 74 | > The Codecov extension is enabled by default on Sourcegraph.com, so you don't need to add it from its [extension registry listing](https://sourcegraph.com/extensions/sourcegraph/codecov). 75 | 76 | #### With a self-hosted Sourcegraph instance and the browser extension 77 | 78 | #### File decorations 79 | 80 | Enable file decorations in user, organization or global settings to see coverage status of files and directories in the file tree and on directory pages. 81 | 82 | ```jsonc 83 | { 84 | "codecov.fileDecorations.show": true 85 | } 86 | ``` 87 | 88 | ![File decorations](https://user-images.githubusercontent.com/37420160/101069758-6ad2c180-3568-11eb-9778-f20f59f46d6f.png) 89 | 90 | ## License 91 | 92 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fcodecov%2Fsourcegraph-codecov.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fcodecov%2Fsourcegraph-codecov?ref=badge_large) 93 | -------------------------------------------------------------------------------- /src/insights.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest, concat } from 'rxjs' 2 | import { map } from 'rxjs/operators' 3 | import * as sourcegraph from 'sourcegraph' 4 | import { getCommitCoverage, getGraphSVG, getTreeCoverage } from './api' 5 | import { codecovParamsForRepositoryCommit, resolveDocumentURI } from './uri' 6 | 7 | /** 8 | * Returns a view provider that shows a coverage graph on directory pages using the experimental view provider API. 9 | */ 10 | export const createGraphViewProvider = ( 11 | graphType: 'icicle' | 'sunburst' | 'tree' | 'pie' 12 | ): sourcegraph.DirectoryViewProvider => ({ 13 | where: 'directory', 14 | provideView: ({ workspace, viewer }) => { 15 | const { repo, rev, path } = resolveDocumentURI(viewer.directory.uri.href) 16 | const apiParams = codecovParamsForRepositoryCommit({ repo, rev }, sourcegraph) 17 | 18 | return combineLatest([ 19 | // coverage 20 | concat([null], path ? getTreeCoverage({ ...apiParams, path }) : getCommitCoverage(apiParams)), 21 | 22 | // svg 23 | concat( 24 | [null], 25 | (async () => { 26 | // Codecov native SVG graphs are not available for subdirectories 27 | if (path || (graphType !== 'icicle' && graphType !== 'sunburst' && graphType !== 'tree')) { 28 | return null 29 | } 30 | // Try to get graph SVG 31 | // Fallback to default branch if commit is not available 32 | // TODO: Extension API should expose the rev instead 33 | // https://github.com/sourcegraph/sourcegraph/issues/4278 34 | return ( 35 | (await getGraphSVG({ ...apiParams, graphType })) || 36 | (await getGraphSVG({ 37 | ...apiParams, 38 | sha: undefined, 39 | graphType, 40 | })) 41 | ) 42 | })() 43 | ), 44 | ]).pipe( 45 | map(([coverage, svg]) => { 46 | if (!svg && coverage === null) { 47 | // We don't have anything to show 48 | return null 49 | } 50 | 51 | if (svg) { 52 | const repoLink = new URL( 53 | `${workspace.uri.hostname}${workspace.uri.pathname}@${rev}`, 54 | sourcegraph.internal.sourcegraphURL 55 | ) 56 | svg = prepareSVG(svg, repoLink) 57 | } 58 | 59 | const coverageRatio = 60 | coverage && 61 | ('folder_totals' in coverage.commit 62 | ? parseFloat(coverage.commit.folder_totals.coverage) 63 | : coverage.commit.totals.coverage) 64 | 65 | let title = 'Test coverage' 66 | if (coverageRatio !== null && graphType !== 'pie') { 67 | title += `: ${coverageRatio.toFixed(0)}%` 68 | } 69 | 70 | const content: ( 71 | | sourcegraph.MarkupContent 72 | | sourcegraph.PieChartContent<{ name: string; value: number; fill: string }> 73 | )[] = 74 | graphType === 'pie' && coverageRatio !== null 75 | ? [ 76 | { 77 | chart: 'pie', 78 | pies: [ 79 | { 80 | data: [ 81 | { 82 | name: 'Not covered', 83 | value: 100 - coverageRatio, 84 | fill: 'var(--danger)', 85 | }, 86 | { 87 | name: 'Covered', 88 | value: coverageRatio, 89 | fill: 'var(--success)', 90 | }, 91 | ], 92 | dataKey: 'value', 93 | nameKey: 'name', 94 | fillKey: 'fill', 95 | }, 96 | ], 97 | }, 98 | ] 99 | : svg 100 | ? [{ kind: sourcegraph.MarkupKind.Markdown, value: svg }] 101 | : [] 102 | 103 | const subtitle = 104 | graphType === 'pie' && coverageRatio !== null 105 | ? 'Percentages of lines of code that are covered/not covered by tests.' 106 | : 'Distribution of test coverage across the codebase.' 107 | 108 | return { 109 | title, 110 | subtitle, 111 | content, 112 | } 113 | }) 114 | ) 115 | }, 116 | }) 117 | 118 | function prepareSVG(svg: string, repoLink: URL): string { 119 | // Always link to tree pages. 120 | // We cannot determine if the path is a file or directory, 121 | // but Sourcegraph will redirect if necessary. 122 | const baseLink = repoLink.href + '/-/tree' 123 | 124 | // Regex XML parsing is bad, but DOMParser is not available in Workers 125 | // and we know the structure of the SVG 126 | return ( 127 | svg 128 | // Make SVG responsive 129 | .replace(/width="\d+"/, 'width="100%" style="flex: 1 1 0"') 130 | .replace(/height="\d+"/, '') 131 | // Remove weird double-slashes 132 | .replace(/\/\//g, '/') 133 | // Remove and replace with data-tooltip used in Sourcegraph webapp 134 | .replace(/<title>[^<]*<\/title>/g, '') 135 | // Make borders react to dark theme 136 | .replace(/stroke="white"/g, 'stroke="var(--body-bg)"') 137 | // Link to directories 138 | .replace( 139 | /^<rect (.+)data-content="(\/[^"]*)"(.+)<\/rect>$/gm, 140 | `<a href="${baseLink}$2" data-tooltip="$2"><rect $1$3</rect></a>` 141 | ) 142 | // Make sure line breaks are not interpreted as markdown line breaks 143 | .replace(/\n/g, ' ') 144 | ) 145 | } 146 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | export type Service = 'gh' | 'ghe' | 'gl' | 'bb' 2 | 3 | export interface RepoSpec { 4 | /** The identifier for the service where the repository lives. */ 5 | service: Service 6 | 7 | /** The value for the :owner URL parameter (the repository's owner). */ 8 | owner: string 9 | 10 | /** The value for the :repo URL parameter (the repository's name). */ 11 | repo: string 12 | } 13 | 14 | export interface APIOptions { 15 | /** 16 | * The base URL of the Codecov instance. 17 | * 18 | * @example https://codecov.io 19 | */ 20 | baseURL: string 21 | 22 | /** The Codecov API token (required for private repositories). */ 23 | token?: string 24 | } 25 | 26 | /** 27 | * Codecov API parameters for a commit. 28 | */ 29 | export interface CommitSpec { 30 | /** The value for the :sha URL parameter (the Git commit SHA). */ 31 | sha: string 32 | } 33 | export interface PathSpec { 34 | path: string 35 | } 36 | 37 | export interface CodecovOwner { 38 | /** An identifier for the code host or other service where the repository lives. */ 39 | service: 'github' | 'gitlab' | 'bitbucket' 40 | 41 | /** For GitHub, the name of the repository's owner. */ 42 | username: string 43 | } 44 | 45 | export interface CodecovRepo { 46 | /** The repository name (without the owner). */ 47 | name: string 48 | } 49 | 50 | /** The response data from the Codecov API for a commit. */ 51 | export interface CodecovCommitData { 52 | owner: CodecovOwner 53 | repo: CodecovRepo 54 | commit: { 55 | commitid: string 56 | report: { 57 | files: { 58 | [path: string]: 59 | | undefined 60 | | { 61 | /** Line coverage data for this file at this commit.. */ 62 | l: { 63 | /** 64 | * The coverage for the line (1-indexed). 65 | * 66 | * @type {number} number of hits on a fully covered line 67 | * @type {string} "(partial hits)/branches" on a partially covered line 68 | * @type {null} skipped line 69 | */ 70 | [line: number]: number | string | null | undefined 71 | } 72 | 73 | /** Totals for this file at this commit. */ 74 | t: { 75 | /** The coverage ratio for this file, as a string (e.g., "62.5000000"). */ 76 | c: string 77 | } 78 | } 79 | } 80 | } 81 | totals: { 82 | /** The coverage ratio of the repository at this commit. */ 83 | coverage: number 84 | } 85 | } 86 | } 87 | 88 | export interface CodecovTreeData { 89 | owner: CodecovOwner 90 | repo: CodecovRepo 91 | commit: { 92 | folder_totals: { 93 | /** Coverage as a stringified float. */ 94 | coverage: string 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * Gets the Codecov coverage data for a single commit of a repository. 101 | * 102 | * See https://docs.codecov.io/v5.0.0/reference#section-get-a-single-commit. 103 | */ 104 | export const getCommitCoverage = memoizeAsync( 105 | async (args: RepoSpec & CommitSpec & APIOptions): Promise<CodecovCommitData | null> => { 106 | const response = await fetch(commitApiURL(args).href, { 107 | method: 'GET', 108 | mode: 'cors', 109 | }) 110 | if (response.status === 404) { 111 | console.warn(`No Codecov coverage found for ${args.owner}/${args.repo}@${args.sha}`) 112 | return null 113 | } 114 | if (!response.ok) { 115 | throw new Error(`Error response from Codecov API: ${response.status} ${response.statusText}`) 116 | } 117 | return (await response.json()) as CodecovCommitData 118 | }, 119 | options => commitApiURL(options).href 120 | ) 121 | 122 | /** 123 | * Constructs the URL for Codecov coverage data for a single commit of a repository. 124 | * 125 | * See https://docs.codecov.io/v5.0.0/reference#section-get-a-single-commit. 126 | */ 127 | function commitApiURL({ baseURL, service, owner, repo, sha, token }: RepoSpec & CommitSpec & APIOptions): URL { 128 | const url = new URL(`${baseURL}/api/${service}/${owner}/${repo}/commits/${sha}`) 129 | // Necessary to get the data for all files in the response. 130 | url.searchParams.set('src', 'extension') 131 | setAccessToken(url, token) 132 | return url 133 | } 134 | 135 | export const getTreeCoverage = memoizeAsync( 136 | async (args: RepoSpec & CommitSpec & PathSpec & APIOptions): Promise<CodecovTreeData | null> => { 137 | if (!args.path.replace(/\/+$/, '')) { 138 | throw new Error('Invalid path') 139 | } 140 | const response = await fetch(treeCoverageURL(args).href) 141 | if (response.status === 404) { 142 | console.warn(`No Codecov coverage found for ${args.owner}/${args.repo}@${args.sha}/${args.path}`) 143 | return null 144 | } 145 | if (!response.ok) { 146 | throw new Error(`Error response from Codecov API: ${response.status} ${response.statusText}`) 147 | } 148 | return (await response.json()) as CodecovTreeData 149 | }, 150 | options => treeCoverageURL(options).href 151 | ) 152 | 153 | /** 154 | * Adds the access token to a given URL if defined. 155 | */ 156 | export function setAccessToken(url: URL, token: string | undefined): void { 157 | if (token) { 158 | url.searchParams.set('access_token', token) 159 | } 160 | } 161 | 162 | /** 163 | * Constructs the URL for Codecov coverage data for a folder of a repository at a commit. 164 | */ 165 | function treeCoverageURL({ 166 | baseURL, 167 | service, 168 | owner, 169 | repo, 170 | sha, 171 | token, 172 | path, 173 | }: RepoSpec & CommitSpec & PathSpec & APIOptions): URL { 174 | const url = new URL(`${baseURL}/api/${service}/${owner}/${repo}/tree/${sha}/${path}`) 175 | setAccessToken(url, token) 176 | return url 177 | } 178 | 179 | interface GetGraphSVGOptions extends RepoSpec, Partial<CommitSpec>, APIOptions { 180 | graphType: 'icicle' | 'tree' | 'sunburst' 181 | } 182 | 183 | /** 184 | * Get a graph SVG from the API as text. 185 | */ 186 | export async function getGraphSVG({ 187 | baseURL, 188 | owner, 189 | repo, 190 | service, 191 | sha, 192 | graphType, 193 | token, 194 | }: GetGraphSVGOptions): Promise<string | null> { 195 | const url = new URL(`${baseURL}/api/${service}/${owner}/${repo}`) 196 | if (sha) { 197 | url.pathname += `/commit/${sha}` 198 | } 199 | url.pathname += `/graphs/${graphType}.svg` 200 | setAccessToken(url, token) 201 | 202 | const response = await fetch(url.href) 203 | if (response.status === 404) { 204 | return null 205 | } 206 | if (!response.ok) { 207 | throw new Error(`Could not fetch SVG: ${response.status} ${response.statusText}`) 208 | } 209 | 210 | return response.text() 211 | } 212 | 213 | /** 214 | * Creates a function that memoizes the async result of func. If the Promise is rejected, the result will not be 215 | * cached. 216 | * 217 | * @param func The function to memoize 218 | * @param toKey Determines the cache key for storing the result based on the first argument provided to the memoized 219 | * function 220 | */ 221 | function memoizeAsync<P, T>(func: (params: P) => Promise<T>, toKey: (params: P) => string): (params: P) => Promise<T> { 222 | const cache = new Map<string, Promise<T>>() 223 | return (params: P) => { 224 | const key = toKey(params) 225 | const hit = cache.get(key) 226 | if (hit) { 227 | return hit 228 | } 229 | const p = func(params) 230 | p.then(null, () => cache.delete(key)) 231 | cache.set(key, p) 232 | return p 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { combineLatest, from, Subscription } from 'rxjs' 2 | import { concatMap, filter, map, startWith, switchMap, distinctUntilChanged, debounceTime } from 'rxjs/operators' 3 | import * as sourcegraph from 'sourcegraph' 4 | import { getCommitCoverage, getTreeCoverage, Service } from './api' 5 | import { codecovToDecorations } from './decoration' 6 | import { createGraphViewProvider } from './insights' 7 | import { getCommitCoverageRatio, getFileCoverageRatios, getFileLineCoverage } from './model' 8 | import { configurationChanges, Endpoint, resolveEndpoint, resolveSettings, Settings, InsightSettings } from './settings' 9 | import { codecovParamsForRepositoryCommit, resolveDocumentURI, ResolvedRootURI, resolveRootURI } from './uri' 10 | 11 | const decorationType = sourcegraph.app.createDecorationType && sourcegraph.app.createDecorationType() 12 | 13 | // Check if the Sourcegraph instance supports status bar items (minimum Sourcegraph version 3.26) 14 | const supportsStatusBarAPI = !!sourcegraph.app.createStatusBarItemType as boolean 15 | const statusBarItemType = sourcegraph.app.createStatusBarItemType && sourcegraph.app.createStatusBarItemType() 16 | 17 | /** Entrypoint for the Codecov Sourcegraph extension. */ 18 | export function activate( 19 | context: sourcegraph.ExtensionContext = { 20 | subscriptions: new Subscription(), 21 | } 22 | ): void { 23 | /** 24 | * An Observable that emits the active window's visible view components 25 | * when the active window or its view components change. 26 | */ 27 | const editorsChanges = sourcegraph.app.activeWindowChanges 28 | ? from(sourcegraph.app.activeWindowChanges).pipe( 29 | filter( 30 | (activeWindow): activeWindow is Exclude<typeof activeWindow, undefined> => activeWindow !== undefined 31 | ), 32 | switchMap(activeWindow => 33 | from(activeWindow.activeViewComponentChanges).pipe(map(() => activeWindow.visibleViewComponents)) 34 | ) 35 | // Backcompat: rely on onDidOpenTextDocument if the extension host doesn't support activeWindowChanges / activeViewComponentChanges 36 | ) 37 | : from(sourcegraph.workspace.onDidOpenTextDocument).pipe( 38 | map(() => (sourcegraph.app.activeWindow && sourcegraph.app.activeWindow.visibleViewComponents) || []) 39 | ) 40 | 41 | // When the configuration or current file changes, publish new decorations. 42 | // 43 | // TODO: Unpublish decorations on previously (but not currently) open files when settings changes, to avoid a 44 | // brief flicker of the old state when the file is reopened. 45 | async function decorate(settings: Readonly<Settings>, editors: sourcegraph.ViewComponent[]): Promise<void> { 46 | const resolvedSettings = resolveSettings(settings) 47 | for (const editor of editors) { 48 | if (editor.type !== 'CodeEditor') { 49 | continue 50 | } 51 | const decorations = await getFileLineCoverage( 52 | resolveDocumentURI(editor.document.uri), 53 | resolvedSettings['codecov.endpoints'][0], 54 | sourcegraph 55 | ) 56 | if (!decorations) { 57 | continue 58 | } 59 | editor.setDecorations(decorationType, codecovToDecorations(settings, decorations)) 60 | } 61 | } 62 | 63 | context.subscriptions.add( 64 | combineLatest([configurationChanges, editorsChanges]) 65 | .pipe( 66 | concatMap(async ([settings, editors]) => { 67 | try { 68 | await decorate(settings, editors) 69 | } catch (err) { 70 | console.error('Codecov: decoration error', err) 71 | } 72 | }) 73 | ) 74 | .subscribe() 75 | ) 76 | 77 | // Set context values referenced in template expressions in the extension manifest (e.g., to interpolate "N" in 78 | // the "Coverage: N%" button label). 79 | // 80 | // The context only needs to be updated when the endpoints configuration changes. 81 | async function updateContext( 82 | endpoints: readonly Endpoint[] | undefined, 83 | roots: readonly sourcegraph.WorkspaceRoot[], 84 | editors: sourcegraph.ViewComponent[] 85 | ): Promise<void> { 86 | // Get the current repository. Sourcegraph 3.0-preview exposes sourcegraph.workspace.roots, but earlier 87 | // versions do not. 88 | let uri: string 89 | if (roots && roots.length > 0) { 90 | uri = roots[0].uri.toString() 91 | } else if (editors.length > 0) { 92 | const editor = editors[0] 93 | if (editor.type !== 'CodeEditor') { 94 | return 95 | } 96 | uri = editor.document.uri 97 | } else { 98 | return 99 | } 100 | const lastURI = resolveRootURI(uri) 101 | const endpoint = resolveEndpoint(endpoints) 102 | 103 | const context: { 104 | [key: string]: string | number | boolean | null 105 | } = {} 106 | 107 | const p = codecovParamsForRepositoryCommit(lastURI, sourcegraph) 108 | const repoURL = `${p.baseURL || 'https://codecov.io'}/${p.service}/${p.owner}/${p.repo}` 109 | context['codecov.repoURL'] = repoURL 110 | const baseFileURL = `${repoURL}/src/${p.sha}` 111 | context['codecov.commitURL'] = `${repoURL}/commit/${p.sha}` 112 | 113 | try { 114 | // Store overall commit coverage ratio. 115 | const commitCoverage = await getCommitCoverageRatio(lastURI, endpoint, sourcegraph) 116 | context['codecov.commitCoverage'] = commitCoverage ? commitCoverage.toFixed(1) : null 117 | 118 | // Store coverage ratio (and Codecov report URL) for each file at this commit so that 119 | // template strings in contributions can refer to these values. 120 | const fileRatios = await getFileCoverageRatios(lastURI, endpoint, sourcegraph) 121 | if (fileRatios) { 122 | for (const [path, ratio] of Object.entries(fileRatios)) { 123 | const uri = `git://${lastURI.repo}?${lastURI.rev}#${path}` 124 | context[`codecov.coverageRatio.${uri}`] = ratio.toFixed(0) 125 | context[`codecov.fileURL.${uri}`] = `${baseFileURL}/${path}` 126 | } 127 | } 128 | } catch (err) { 129 | console.error('Error loading Codecov file coverage:', err) 130 | } 131 | 132 | sourcegraph.internal.updateContext(context) 133 | } 134 | 135 | async function setStatusBars( 136 | endpoints: readonly Endpoint[] | undefined, 137 | editors: sourcegraph.ViewComponent[] 138 | ): Promise<void> { 139 | if (supportsStatusBarAPI) { 140 | /** 141 | * Keys: repo + revision separated, repo: "sourcegraph/sourcegraph" + revision: "1234" => "sourcegraph/sourcegraph1234" 142 | * Values: resolved root uri 143 | * Used to find all unique pairs of repo + revision across editors 144 | */ 145 | const resolvedRootURIs = new Map<string, ResolvedRootURI>() 146 | for (const editor of editors) { 147 | if (editor.type === 'CodeEditor') { 148 | const { repo, rev } = resolveRootURI(editor.document.uri) 149 | resolvedRootURIs.set(repo + rev, { repo, rev }) 150 | } 151 | } 152 | 153 | // Fetch data 154 | const endpoint = resolveEndpoint(endpoints) 155 | 156 | const fileRatioByURI = new Map<string, { fileRatio: string; fileURL: string }>() 157 | 158 | const results = await Promise.allSettled( 159 | [...resolvedRootURIs].map(async ([, resolvedRootURI]) => ({ 160 | resolvedRootURI, 161 | fileRatios: await getFileCoverageRatios(resolvedRootURI, endpoint, sourcegraph), 162 | })) 163 | ) 164 | 165 | for (const result of results) { 166 | if (result.status === 'fulfilled') { 167 | const { resolvedRootURI, fileRatios } = result.value 168 | 169 | const p = codecovParamsForRepositoryCommit(resolvedRootURI, sourcegraph) 170 | const repoURL = `${p.baseURL || 'https://codecov.io'}/${p.service}/${p.owner}/${p.repo}` 171 | 172 | if (fileRatios) { 173 | for (const [path, ratio] of Object.entries(fileRatios)) { 174 | // Convert relative paths to URIs 175 | const uri = `git://${resolvedRootURI.repo}?${resolvedRootURI.rev}#${path}` 176 | const baseFileURL = `${repoURL}/src/${p.sha}` 177 | 178 | fileRatioByURI.set(uri, { fileRatio: ratio.toFixed(0), fileURL: `${baseFileURL}/${path}` }) 179 | } 180 | } 181 | } 182 | } 183 | 184 | // Set status bars 185 | for (const editor of editors) { 186 | if (editor.type === 'CodeEditor') { 187 | const ratio = fileRatioByURI.get(editor.document.uri) 188 | if (ratio) { 189 | editor.setStatusBarItem(statusBarItemType, { 190 | text: `Code Coverage: ${ratio.fileRatio}%`, 191 | command: { id: 'open', args: [ratio.fileURL] }, 192 | }) 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | // Update the context when the configuration, workspace roots or active editors change. 200 | context.subscriptions.add( 201 | combineLatest([ 202 | configurationChanges.pipe(map(settings => settings['codecov.endpoints'])), 203 | from( 204 | // Backcompat: rely on onDidChangeRoots if the extension host doesn't support rootChanges. 205 | sourcegraph.workspace.rootChanges || sourcegraph.workspace.onDidChangeRoots 206 | ).pipe( 207 | map(() => sourcegraph.workspace.roots), 208 | startWith(sourcegraph.workspace.roots) 209 | ), 210 | editorsChanges, 211 | ]) 212 | .pipe( 213 | // Debounce to avoid doing duplicate work for intermediate states 214 | // for views with multiple editors added sequentially (e.g. commit page, PR) 215 | debounceTime(30), 216 | concatMap(async ([endpoints, roots, editors]) => { 217 | try { 218 | await setStatusBars(endpoints, editors) 219 | } catch (err) { 220 | console.error('Codecov: error setting status bars', err) 221 | } 222 | 223 | try { 224 | // The commit data used for `updateContext` should be cached if Status Bar API is supported 225 | await updateContext(endpoints, roots, editors) 226 | } catch (err) { 227 | console.error('Codecov: error updating context', err) 228 | } 229 | }) 230 | ) 231 | .subscribe() 232 | ) 233 | 234 | sourcegraph.commands.registerCommand('codecov.setupEnterprise', async () => { 235 | const endpoint = resolveEndpoint(sourcegraph.configuration.get<Settings>().get('codecov.endpoints')) 236 | if (!sourcegraph.app.activeWindow) { 237 | throw new Error('To set a Codecov Endpoint, navigate to a file and then re-run this command.') 238 | } 239 | 240 | const service = await sourcegraph.app.activeWindow.showInputBox({ 241 | prompt: 'Version control type (gh/ghe/bb/gl):', 242 | value: endpoint.service || '', 243 | }) 244 | 245 | const url = await sourcegraph.app.activeWindow.showInputBox({ 246 | prompt: 'Codecov endpoint:', 247 | value: endpoint.url || '', 248 | }) 249 | 250 | if (url !== undefined && service !== undefined) { 251 | // TODO: Only supports setting the token of the first API endpoint. 252 | return sourcegraph.configuration 253 | .get<Settings>() 254 | .update('codecov.endpoints', [{ ...endpoint, url, service: service as Service }]) 255 | } 256 | }) 257 | 258 | // Handle the "Set Codecov API token" command (show the user a prompt for their token, and save 259 | // their input to settings). 260 | sourcegraph.commands.registerCommand('codecov.setAPIToken', async () => { 261 | const endpoint = resolveEndpoint(sourcegraph.configuration.get<Settings>().get('codecov.endpoints')) 262 | 263 | if (!sourcegraph.app.activeWindow) { 264 | throw new Error('To set a Codecov API token, navigate to a file and then re-run this command.') 265 | } 266 | 267 | const token = await sourcegraph.app.activeWindow.showInputBox({ 268 | prompt: `Codecov API token (for ${endpoint.url}):`, 269 | value: endpoint.token || undefined, 270 | }) 271 | 272 | if (token !== undefined) { 273 | // TODO: Only supports setting the token of the first API endpoint. 274 | return sourcegraph.configuration.get<Settings>().update('codecov.endpoints', [{ ...endpoint, token }]) 275 | } 276 | }) 277 | 278 | // Experimental: Show graphs on repository pages 279 | if (sourcegraph.app.registerViewProvider) { 280 | const graphTypes: ['icicle', 'sunburst', 'tree', 'pie'] = ['icicle', 'sunburst', 'tree', 'pie'] 281 | for (const graphType of graphTypes) { 282 | let subscription: sourcegraph.Unsubscribable = new Subscription() 283 | context.subscriptions.add( 284 | configurationChanges 285 | .pipe( 286 | map((settings): boolean => settings[`codecov.insight.${graphType}` as keyof InsightSettings]), 287 | distinctUntilChanged() 288 | ) 289 | .subscribe(enabled => { 290 | if (enabled) { 291 | subscription = sourcegraph.app.registerViewProvider( 292 | `codecov.${graphType}`, 293 | createGraphViewProvider(graphType) 294 | ) 295 | context.subscriptions.add(subscription) 296 | } else { 297 | subscription.unsubscribe() 298 | } 299 | }) 300 | ) 301 | } 302 | } 303 | 304 | // Experimental: file decorations. Check if the Sourcegraph instance supports 305 | // file decoration providers (minimum Sourcegraph version 3.23) 306 | if (sourcegraph.app.registerFileDecorationProvider) { 307 | function createFileDecoration(uri: string, ratio: number, settings: Settings): sourcegraph.FileDecoration { 308 | const after = { 309 | contentText: `${ratio}%`, 310 | hoverMessage: `Codecov: ${ratio}% covered`, 311 | } 312 | 313 | return { 314 | uri, 315 | after, 316 | meter: { 317 | value: ratio, 318 | hoverMessage: `Codecov: ${ratio}% covered`, 319 | min: 0, 320 | max: 100, 321 | low: settings['codecov.fileDecorations.low'], 322 | high: settings['codecov.fileDecorations.high'], 323 | optimum: settings['codecov.fileDecorations.optimum'], 324 | }, 325 | } 326 | } 327 | 328 | context.subscriptions.add( 329 | sourcegraph.app.registerFileDecorationProvider({ 330 | provideFileDecorations: async ({ files, uri }) => { 331 | const settings = resolveSettings(sourcegraph.configuration.get<Partial<Settings>>().value) 332 | if (!settings['codecov.fileDecorations.show']) { 333 | return [] 334 | } 335 | 336 | const { repo, rev } = resolveDocumentURI(uri) 337 | const apiParams = codecovParamsForRepositoryCommit({ repo, rev }, sourcegraph) 338 | 339 | // Fetch commit coverage to get ratio for files, tree coverage to get ratio for directories 340 | const nonDirectoryFiles = files.filter(file => !file.isDirectory) 341 | const directories = files.filter(file => file.isDirectory) 342 | const [commitCoverage, ...treeCoverages] = await Promise.allSettled([ 343 | getCommitCoverage(apiParams), 344 | ...directories.map(async (dir, i) => { 345 | const treeCoverage = await getTreeCoverage({ ...apiParams, path: dir.path }) 346 | return { treeCoverage, directory: directories[i] } 347 | }), 348 | ]) 349 | 350 | // Iterate over files and get value from commit coverage to construct file decorations 351 | const fileDecorations: sourcegraph.FileDecoration[] = [] 352 | 353 | if (commitCoverage.status === 'fulfilled' && commitCoverage.value) { 354 | const { files: reportFiles } = commitCoverage.value.commit.report 355 | for (const file of nonDirectoryFiles) { 356 | const report = reportFiles[file.path] 357 | if (!report) { 358 | continue 359 | } 360 | 361 | const ratio = parseInt(report.t.c, 10) 362 | 363 | fileDecorations.push(createFileDecoration(file.uri, ratio, settings)) 364 | } 365 | } 366 | 367 | // Iterate over tree coverage results to construct directory decorations 368 | const directoryDecorations: sourcegraph.FileDecoration[] = [] 369 | 370 | for (const result of treeCoverages) { 371 | if (result.status === 'rejected') { 372 | continue 373 | } 374 | 375 | const { treeCoverage, directory } = result.value 376 | 377 | if (!treeCoverage) { 378 | continue 379 | } 380 | 381 | const ratio = parseInt(treeCoverage.commit.folder_totals.coverage, 10) 382 | 383 | directoryDecorations.push(createFileDecoration(directory.uri, ratio, settings)) 384 | } 385 | return [...fileDecorations, ...directoryDecorations] 386 | }, 387 | }) 388 | ) 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/sourcegraph/sourcegraph/main/client/shared/src/schema/extension.schema.json", 3 | "name": "codecov", 4 | "publisher": "sourcegraph", 5 | "description": "Shows Codecov code coverage", 6 | "icon": "data:image/png;base64,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", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/codecov/sourcegraph-codecov.git" 10 | }, 11 | "categories": [ 12 | "External services", 13 | "Reports and stats", 14 | "Code analysis", 15 | "Insights" 16 | ], 17 | "tags": [ 18 | "codecov", 19 | "coverage", 20 | "code coverage", 21 | "testing" 22 | ], 23 | "version": "0.0.0-development", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/codecov/sourcegraph-codecov/issues" 27 | }, 28 | "activationEvents": [ 29 | "*" 30 | ], 31 | "contributes": { 32 | "actions": [ 33 | { 34 | "id": "codecov.decorations.toggleCoverage", 35 | "command": "updateConfiguration", 36 | "commandArguments": [ 37 | [ 38 | "codecov.decorations.lineCoverage" 39 | ], 40 | "${!config.codecov.decorations.lineCoverage}", 41 | null, 42 | "json" 43 | ], 44 | "title": "${config.codecov.decorations.lineCoverage && \"Hide\" || \"Show\"} line coverage on file${get(context, `codecov.coverageRatio.${resource.uri}`) && ` (${get(context, `codecov.coverageRatio.${resource.uri}`)}% coverage)` || \"\"}", 45 | "category": "Codecov", 46 | "actionItem": { 47 | "label": "Coverage: ${get(context, `codecov.coverageRatio.${resource.uri}`)}%", 48 | "description": "${config.codecov.decorations.lineCoverage && \"Hide\" || \"Show\"} code coverage\nCmd/Ctrl+Click: View on Codecov", 49 | "pressed": "config.codecov.decorations.lineCoverage", 50 | "iconURL": "https://raw.githubusercontent.com/codecov/media/master/logos/codecov%20logo%20mark%20pink.svg?sanitize=true" 51 | } 52 | }, 53 | { 54 | "id": "codecov.decorations.toggleHits", 55 | "command": "updateConfiguration", 56 | "commandArguments": [ 57 | [ 58 | "codecov.decorations.lineHitCounts" 59 | ], 60 | "${!config.codecov.decorations.lineHitCounts}", 61 | null, 62 | "json" 63 | ], 64 | "title": "${config.codecov.decorations.lineHitCounts && \"Hide\" || \"Show\"} line hit/branch counts", 65 | "category": "Codecov" 66 | }, 67 | { 68 | "id": "codecov.button.toggle", 69 | "command": "updateConfiguration", 70 | "commandArguments": [ 71 | [ 72 | "codecov.hideCoverageButton" 73 | ], 74 | "${!config.codecov.hideCoverageButton}", 75 | null, 76 | "json" 77 | ], 78 | "title": "${config.codecov.hideCoverageButton && \"Show\" || \"Hide\"} coverage % button", 79 | "category": "Codecov" 80 | }, 81 | { 82 | "id": "codecov.link.file", 83 | "command": "open", 84 | "commandArguments": [ 85 | "${get(context, `codecov.fileURL.${resource.uri}`)}" 86 | ], 87 | "title": "View file coverage report", 88 | "category": "Codecov" 89 | }, 90 | { 91 | "id": "codecov.link.commit", 92 | "command": "open", 93 | "commandArguments": [ 94 | "${codecov.commitURL}" 95 | ], 96 | "title": "View commit report${codecov.commitCoverage && ` (${codecov.commitCoverage}% coverage)` || \"\"}", 97 | "category": "Codecov" 98 | }, 99 | { 100 | "id": "codecov.link.repository", 101 | "command": "open", 102 | "commandArguments": [ 103 | "${codecov.repoURL}" 104 | ], 105 | "title": "View repository coverage dashboard", 106 | "category": "Codecov" 107 | }, 108 | { 109 | "id": "codecov.setAPIToken", 110 | "command": "codecov.setAPIToken", 111 | "title": "Set API token for private repositories", 112 | "category": "Codecov" 113 | }, 114 | { 115 | "id": "codecov.setupEnterprise", 116 | "command": "codecov.setupEnterprise", 117 | "title": "Set up Codecov Enterprise", 118 | "category": "Codecov" 119 | }, 120 | { 121 | "id": "codecov.help", 122 | "command": "open", 123 | "commandArguments": [ 124 | "https://docs.codecov.io" 125 | ], 126 | "title": "Documentation and support", 127 | "category": "Codecov", 128 | "iconURL": "data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\"?><svg width=\"2278\" height=\"2500\" viewBox=\"0 0 256 281\" xmlns=\"http://www.w3.org/2000/svg\" preserveAspectRatio=\"xMidYMid\"><path d=\"M218.551 37.419C194.416 13.289 162.33 0 128.097 0 57.537.047.091 57.527.04 128.121L0 149.813l16.859-11.49c11.468-7.814 24.75-11.944 38.417-11.944 4.079 0 8.198.373 12.24 1.11 12.742 2.32 24.165 8.089 33.414 16.758 2.12-4.67 4.614-9.209 7.56-13.536a88.081 88.081 0 0 1 3.805-5.15c-11.652-9.84-25.649-16.463-40.926-19.245a90.35 90.35 0 0 0-16.12-1.459 88.377 88.377 0 0 0-32.29 6.07c8.36-51.222 52.85-89.37 105.23-89.408 28.392 0 55.078 11.053 75.149 31.117 16.011 16.01 26.254 36.033 29.788 58.117-10.329-4.035-21.212-6.1-32.403-6.144l-1.568-.007a90.957 90.957 0 0 0-3.401.111c-1.955.1-3.898.277-5.821.5-.574.063-1.139.153-1.707.231-1.378.186-2.75.395-4.109.639-.603.11-1.203.231-1.8.351a90.517 90.517 0 0 0-4.114.937c-.492.126-.983.243-1.47.374a90.183 90.183 0 0 0-5.09 1.538c-.1.035-.204.063-.304.096a87.532 87.532 0 0 0-11.057 4.649c-.097.05-.193.101-.293.151a86.7 86.7 0 0 0-4.912 2.701l-.398.238a86.09 86.09 0 0 0-22.302 19.253c-.262.318-.524.635-.784.958-1.376 1.725-2.718 3.49-3.976 5.336a91.412 91.412 0 0 0-3.672 5.913 90.235 90.235 0 0 0-2.496 4.638c-.044.09-.089.175-.133.265a88.786 88.786 0 0 0-4.637 11.272l-.002.009v.004a88.006 88.006 0 0 0-4.509 29.313c.005.397.005.794.019 1.192.021.777.06 1.557.104 2.338a98.66 98.66 0 0 0 .289 3.834c.078.804.174 1.606.275 2.41.063.512.119 1.026.195 1.534a90.11 90.11 0 0 0 .658 4.01c4.339 22.938 17.261 42.937 36.39 56.316l2.446 1.564.02-.048a88.572 88.572 0 0 0 36.232 13.45l1.746.236 12.974-20.822-4.664-.127c-35.898-.985-65.1-31.003-65.1-66.917 0-35.348 27.624-64.702 62.876-66.829l2.23-.085c14.292-.362 28.372 3.859 40.325 11.997l16.781 11.421.036-21.58c.027-34.219-13.272-66.379-37.449-90.554\" fill=\"%23E0225C\"/></svg>" 129 | } 130 | ], 131 | "menus": { 132 | "editor/title": [ 133 | { 134 | "action": "codecov.decorations.toggleCoverage", 135 | "alt": "codecov.link.file", 136 | "when": "resource && !config.codecov.hideCoverageButton && get(context, `codecov.coverageRatio.${resource.uri}`)" 137 | } 138 | ], 139 | "commandPalette": [ 140 | { 141 | "action": "codecov.decorations.toggleCoverage", 142 | "when": "resource" 143 | }, 144 | { 145 | "action": "codecov.decorations.toggleHits", 146 | "when": "resource" 147 | }, 148 | { 149 | "action": "codecov.button.toggle", 150 | "when": "resource && get(context, `codecov.coverageRatio.${resource.uri}`)" 151 | }, 152 | { 153 | "action": "codecov.link.file", 154 | "when": "resource && get(context, `codecov.fileURL.${resource.uri}`)" 155 | }, 156 | { 157 | "action": "codecov.link.commit", 158 | "when": "codecov.commitURL" 159 | }, 160 | { 161 | "action": "codecov.link.repository", 162 | "when": "codecov.repoURL" 163 | }, 164 | { 165 | "action": "codecov.setAPIToken" 166 | }, 167 | { 168 | "action": "codecov.setupEnterprise" 169 | }, 170 | { 171 | "action": "codecov.help" 172 | } 173 | ], 174 | "help": [ 175 | { 176 | "action": "codecov.help" 177 | } 178 | ] 179 | }, 180 | "views": [ 181 | { 182 | "id": "codecov.coverageGraph", 183 | "where": "directory" 184 | } 185 | ], 186 | "configuration": { 187 | "title": "Codecov settings", 188 | "properties": { 189 | "codecov.insight.icicle": { 190 | "description": "Show an interactive coverage icicle graph on repository pages.", 191 | "type": "boolean" 192 | }, 193 | "codecov.insight.tree": { 194 | "description": "Show a coverage tree map on repository pages.", 195 | "type": "boolean" 196 | }, 197 | "codecov.insight.sunburst": { 198 | "description": "Show a coverage sunburst graph on repository pages.", 199 | "type": "boolean" 200 | }, 201 | "codecov.insight.pie": { 202 | "description": "Show a coverage pie chart on repository and directory pages.", 203 | "type": "boolean" 204 | }, 205 | "codecov.decorations.lineCoverage": { 206 | "description": "Whether to decorate each line with a background color based on its coverage.", 207 | "type": "boolean", 208 | "default": true 209 | }, 210 | "codecov.fileDecorations.show": { 211 | "description": "Whether to decorate each file in the file tree with a meter representing its coverage", 212 | "type": "boolean", 213 | "default": false 214 | }, 215 | "codecov.fileDecorations.low": { 216 | "description": "The highest coverage ratio in the 'low' range.", 217 | "type": "number", 218 | "default": 70, 219 | "minimum": 0, 220 | "maximum": 100 221 | }, 222 | "codecov.fileDecorations.high": { 223 | "description": "The lowest coverage ratio in the 'high' range.", 224 | "type": "number", 225 | "default": 85, 226 | "minimum": 0, 227 | "maximum": 100 228 | }, 229 | "codecov.fileDecorations.optimum": { 230 | "description": "The optimal coverage ratio.", 231 | "type": "number", 232 | "default": 100, 233 | "minimum": 0, 234 | "maximum": 100 235 | }, 236 | "codecov.decorations.lineHitCounts": { 237 | "description": "Whether to decorate the end of each covered line with the hit/branch stats.", 238 | "type": "boolean", 239 | "default": false 240 | }, 241 | "codecov.hideCoverageButton": { 242 | "description": "Whether to hide the file coverage ratio item in the toolbar (in a button labeled \"Coverage: N%\").", 243 | "type": "boolean", 244 | "default": false 245 | }, 246 | "codecov.endpoints": { 247 | "description": "An array of Codecov endpoints that are contacted to retrieve coverage data. Currently only 1 endpoint is supported.\n\nIf empty or not set, https://codecov.io is used.", 248 | "type": "array", 249 | "maxLength": 1, 250 | "items": { 251 | "description": "A Codecov endpoint (either https://codecov.io or Codecov Enterprise).", 252 | "type": "object", 253 | "additionalProperties": false, 254 | "properties": { 255 | "url": { 256 | "description": "The URL for this Codecov endpoint.", 257 | "type": "string", 258 | "format": "uri", 259 | "default": "https://codecov.io", 260 | "examples": [ 261 | "https://codecov.io", 262 | "https://codecov.example.com" 263 | ] 264 | }, 265 | "token": { 266 | "description": "The Codecov API token for this endpoint (required for private repositories).", 267 | "type": "string" 268 | }, 269 | "type": { 270 | "description": "Define hosted version control type (gh|ghe|bb|gl):", 271 | "type": "string", 272 | "enum": [ 273 | "gh", 274 | "ghe", 275 | "bb", 276 | "gl" 277 | ] 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | }, 285 | "main": "dist/extension.js", 286 | "scripts": { 287 | "prettier": "prettier 'src/**/*.{js?(on),ts}' --write --list-different", 288 | "prettier:check": "npm run prettier -- --write=false", 289 | "eslint": "eslint './src/**/*.ts'", 290 | "eslint:fix": "npm run eslint -- --fix", 291 | "typecheck": "tsc -p tsconfig.json", 292 | "build": "parcel build --out-file extension.js src/extension.ts", 293 | "serve": "npm run symlink-package && parcel serve --no-hmr --no-source-maps --out-file extension.js src/extension.ts", 294 | "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require source-map-support/register --opts mocha.opts", 295 | "cover": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --require ts-node/register --require source-map-support/register --all mocha --opts mocha.opts --timeout 10000", 296 | "watch:typecheck": "tsc -p tsconfig.json -w", 297 | "watch:build": "tsc -p tsconfig.dist.json -w", 298 | "watch:test": "npm run test -- -w", 299 | "symlink-package": "mkdirp dist && lnfs ./package.json ./dist/package.json", 300 | "sourcegraph:prepublish": "npm run build" 301 | }, 302 | "nyc": { 303 | "extension": [ 304 | ".tsx", 305 | ".ts" 306 | ], 307 | "include": [ 308 | "src/**/*.ts?(x)" 309 | ], 310 | "exclude": [ 311 | "**/*.test.ts?(x)", 312 | "**/*.d.ts", 313 | "**/*.js" 314 | ] 315 | }, 316 | "browserslist": [ 317 | "last 1 Chrome versions", 318 | "last 1 Firefox versions", 319 | "last 1 Edge versions", 320 | "last 1 Safari versions" 321 | ], 322 | "devDependencies": { 323 | "@sourcegraph/eslint-config": "^0.16.2", 324 | "@sourcegraph/extension-api-stubs": "^1.4.0", 325 | "@types/mocha": "^5.2.7", 326 | "@types/mock-require": "^2.0.0", 327 | "@types/node": "^10.17.21", 328 | "@types/promise.allsettled": "^1.0.3", 329 | "eslint": "^7.16.0", 330 | "lnfs-cli": "^2.1.0", 331 | "mkdirp": "^0.5.5", 332 | "mocha": "^6.2.3", 333 | "mock-require": "^3.0.3", 334 | "nyc": "^15.0.0", 335 | "parcel-bundler": "^1.12.5", 336 | "prettier": "^2.0.5", 337 | "rxjs": "^6.5.5", 338 | "source-map-support": "^0.5.19", 339 | "sourcegraph": "^25.2.0", 340 | "ts-node": "^8.9.1", 341 | "typescript": "^3.8.3" 342 | } 343 | } 344 | --------------------------------------------------------------------------------