Enable version-control-aware links in messages.
This commit is contained in:
Родитель
cee4e07681
Коммит
bfbb757edd
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
}
|
Загрузка…
Ссылка в новой задаче