Enable version-control-aware links in messages.

This commit is contained in:
Jeff King 2021-12-14 11:25:25 -08:00
Родитель cee4e07681
Коммит bfbb757edd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 5E5F5F7EAAF929E4
3 изменённых файлов: 70 добавлений и 25 удалений

Просмотреть файл

@ -22,6 +22,7 @@ import {ExpandableTreeCell, ITreeColumn} from 'azure-devops-ui/TreeEx'
import {ITreeItemEx, ITreeItem} from 'azure-devops-ui/Utilities/TreeItemProvider'
import {Icon, IconSize} from 'azure-devops-ui/Icon'
import { renderPathCell } from './RunCard.renderPathCell'
import { getRepoUri } from './getRepoUri'
const colspan = 99 // No easy way to parameterize this, however extra does not hurt, so using an arbitrarily large value.
@ -160,13 +161,20 @@ function renderMessageWithEmbeddedLinks(result: Result, message: string) {
.map((item, i) => {
if (i % 2 === 0) return item
const [_, text, id] = item.match(rxLink)
const href = isNaN(id as any)
? id
// RelatedLocations is typically [{ id: 1, ...}, { id: 2, ...}]
// Consider using [].find inside of assuming the index correlates to the id.
: result.relatedLocations[+id - 1].physicalLocation.artifactLocation.uri
+ tryOr(() => `#L${result.locations[0].physicalLocation.region.startLine}`, '')
return <a key={i} href={href} target="_blank">{text}</a>
const href = (() => {
if (isNaN(id as any)) return id // `id` is a URI string
// Else `id` is a number
// TODO: search other location coolections
// RelatedLocations is typically [{ id: 1, ...}, { id: 2, ...}]
const location = result.relatedLocations?.find(location => location.id === +id)
return getRepoUri(location?.physicalLocation?.artifactLocation?.uri, result.run)
})()
return href
? <a key={i} href={href} target="_blank">{text}</a>
: text
})
: message
}

Просмотреть файл

@ -4,6 +4,7 @@
import { Tooltip } from 'azure-devops-ui/TooltipEx'
import * as React from 'react'
import { Result } from 'sarif'
import { getRepoUri } from './getRepoUri'
import { Hi } from './Hi'
import './RunCard.renderCell.scss'
import { TooltipSpan } from './TooltipSpan'
@ -42,23 +43,6 @@ function openInNewTab(fileName: string, text: string, region: SimpleRegion | und
setTimeout(() => document.body.querySelector('mark').scrollIntoView({ block: 'center' }))
}
function getRepoUri(uri: string | undefined, repositoryUri: string | undefined): string | undefined {
if (!uri) return undefined
function getHostname(url: string | undefined): string | undefined {
if (!url) return undefined
try {
return new URL(url).hostname
} catch (_) {
return undefined
}
}
const hostname = getHostname(repositoryUri)
if (!(hostname?.endsWith('azure.com') || hostname?.endsWith('visualstudio.com'))) return undefined // We currently only support Azure DevOps.
return `${repositoryUri}?path=${encodeURIComponent(uri)}`
}
// TODO:
// Unify runArt vs resultArt.
// Distinguish uri and text.
@ -95,7 +79,7 @@ export function renderPathCell(result: Result) {
const href = resArtLoc?.properties?.['href']
const runArtContentsText = runArt?.contents?.text
const repoUri = getRepoUri(uri, result.run.versionControlProvenance?.[0]?.repositoryUri) ?? uri
const repoUri = getRepoUri(uri, result.run, ploc?.region) ?? uri
const getHref = () => {
if (uri?.endsWith('.dll')) return undefined

53
components/getRepoUri.ts Normal file
Просмотреть файл

@ -0,0 +1,53 @@
import { Region, Run } from 'sarif'
function getHostname(url: string | undefined): string | undefined {
if (!url) return undefined
try {
return new URL(url).hostname
} catch (_) {
return undefined
}
}
// TODO: Account for URI joins (normalizing slashes).
// TODO: Handle regions beyond `startLine`.
export function getRepoUri(uri: string | undefined, run: Run, region?: Region | undefined): string | undefined {
if (!uri) return undefined
const versionControlDetails = run.versionControlProvenance?.[0]
if (!versionControlDetails) return undefined // Required.
const { repositoryUri, revisionId } = versionControlDetails
const hostname = getHostname(repositoryUri)
if (!hostname) return undefined // Required.
if (hostname.endsWith('azure.com') || hostname?.endsWith('visualstudio.com')) {
// Examples:
// https://dev.azure.com/microsoft/sarif-web-component/_git/sarif-web-component?path=%2F.gitignore
// https://dev.azure.com/microsoft/sarif-web-component/_git/sarif-web-component?path=%2F.gitignore&version=GCd14c42f18766159a7ef6fbb8858ab5ad4f0b532a
let repoUri = revisionId
? `${repositoryUri}?path=${encodeURIComponent(uri)}&version=GC${revisionId}`
: `${repositoryUri}?path=${encodeURIComponent(uri)}`
if (region?.startLine) { // `startLine` is 1-based.
// All three params required just to highlight a single line.
repoUri += `&line=${region!.startLine}`
repoUri += `&lineEnd=${region!.startLine + 1}`
repoUri += `&lineStartColumn=1`
}
return repoUri
}
if (hostname.endsWith('github.com')) {
// Examples:
// https://github.com/microsoft/sarif-web-component/blob/main/.gitignore
// https://github.com/microsoft/sarif-web-component/blob/d14c42f18766159a7ef6fbb8858ab5ad4f0b532a/.gitignore
// https://github.com/microsoft/sarif-web-component/blob/d14c42f18766159a7ef6fbb8858ab5ad4f0b532a/.gitignore#L1
let repoUri = `${repositoryUri}/blob/${revisionId ?? 'main'}${uri}`
if (region?.startLine) { // `startLine` is 1-based.
repoUri += `#L${region!.startLine}`
}
return repoUri
}
return undefined // Unsupported host.
}