mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:51:57 +00:00
web: Switch to absolute blame recency calculation (#61661)
In a nutshell: - We currently compute the recency color relative to either one year ago, three years ago, or the first commit of the repository. - That means the colors have different meaning across different repositories. - Fetching the date of the first commit can be expensive/slow. This commit changes the method to calculate the color to always use the same time frame. The age of the commit is mapped to a number between [0, 1) and d3-scale-chromatic is used to map that value to a color. We can control the sensibility of the colors for newer/older commits by changing how we do the mapping. In this specific version I chose the midpoint to be ~ 6 months in the past. I don't think there is "the right way" to perform the mapping but intuitively I would say that being able to better differentiate more recent commits is more useful than differentiating older commits.
This commit is contained in:
parent
fedc9a2eb1
commit
ba9e78eae4
@ -1776,6 +1776,7 @@ ts_project(
|
||||
"//:node_modules/@types/bloomfilter",
|
||||
"//:node_modules/@types/chrome", #keep
|
||||
"//:node_modules/@types/classnames",
|
||||
"//:node_modules/@types/d3-scale-chromatic",
|
||||
"//:node_modules/@types/d3-time-format",
|
||||
"//:node_modules/@types/history",
|
||||
"//:node_modules/@types/js-cookie",
|
||||
@ -1801,6 +1802,7 @@ ts_project(
|
||||
"//:node_modules/bloomfilter",
|
||||
"//:node_modules/classnames",
|
||||
"//:node_modules/copy-to-clipboard",
|
||||
"//:node_modules/d3-scale-chromatic",
|
||||
"//:node_modules/d3-time-format",
|
||||
"//:node_modules/date-fns",
|
||||
"//:node_modules/fast-json-stable-stringify",
|
||||
|
||||
@ -12,7 +12,7 @@ import { makeRepoGitURI } from '@sourcegraph/shared/src/util/url'
|
||||
import { useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
import { requestGraphQL } from '../../backend/graphql'
|
||||
import type { ExternalServiceKind, FirstCommitDateResult, FirstCommitDateVariables } from '../../graphql-operations'
|
||||
import type { ExternalRepoURLsResult, ExternalRepoURLsVariables, ExternalServiceKind } from '../../graphql-operations'
|
||||
|
||||
import { useBlameVisibility } from './useBlameVisibility'
|
||||
|
||||
@ -61,7 +61,6 @@ export interface BlameHunk {
|
||||
export interface BlameHunkData {
|
||||
current: BlameHunk[] | undefined
|
||||
externalURLs: { url: string; serviceKind: ExternalServiceKind | null }[] | undefined
|
||||
firstCommitDate: Date | undefined
|
||||
}
|
||||
|
||||
interface RawStreamHunk {
|
||||
@ -110,10 +109,8 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
revision: string
|
||||
filePath: string
|
||||
sourcegraphURL: string
|
||||
}): Observable<BlameHunkData | ErrorLike> =>
|
||||
new Observable<BlameHunkData | ErrorLike>(subscriber => {
|
||||
let didEmitFirstCommitDate = false
|
||||
let firstCommitDate: Date | undefined
|
||||
}): Observable<BlameHunkData> =>
|
||||
new Observable<BlameHunkData>(subscriber => {
|
||||
let externalURLs: BlameHunkData['externalURLs']
|
||||
|
||||
const assembledHunks: BlameHunk[] = []
|
||||
@ -121,7 +118,6 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
|
||||
Promise.all([
|
||||
fetchRepositoryData(repoName).then(res => {
|
||||
firstCommitDate = res.firstCommitDate
|
||||
externalURLs = res.externalURLs
|
||||
}),
|
||||
fetchEventSource(`/.api/blame${repoAndRevisionPath}/stream/${filePath}`, {
|
||||
@ -168,10 +164,7 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
}
|
||||
assembledHunks.push(addDisplayInfoForHunk(hunk, sourcegraphURL))
|
||||
}
|
||||
if (firstCommitDate !== undefined) {
|
||||
didEmitFirstCommitDate = true
|
||||
}
|
||||
subscriber.next({ current: assembledHunks, externalURLs, firstCommitDate })
|
||||
subscriber.next({ current: assembledHunks, externalURLs })
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
@ -182,11 +175,6 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
}),
|
||||
]).then(
|
||||
() => {
|
||||
// This case can happen when the event source yields before the commit date is resolved
|
||||
if (!didEmitFirstCommitDate) {
|
||||
subscriber.next({ current: assembledHunks, externalURLs, firstCommitDate })
|
||||
}
|
||||
|
||||
subscriber.complete()
|
||||
},
|
||||
error => subscriber.error(error)
|
||||
@ -202,15 +190,10 @@ const fetchBlameViaStreaming = memoizeObservable(
|
||||
|
||||
async function fetchRepositoryData(repoName: string): Promise<Omit<BlameHunkData, 'current'>> {
|
||||
return lastValueFrom(
|
||||
requestGraphQL<FirstCommitDateResult, FirstCommitDateVariables>(
|
||||
requestGraphQL<ExternalRepoURLsResult, ExternalRepoURLsVariables>(
|
||||
gql`
|
||||
query FirstCommitDate($repo: String!) {
|
||||
query ExternalRepoURLs($repo: String!) {
|
||||
repository(name: $repo) {
|
||||
firstEverCommit {
|
||||
author {
|
||||
date
|
||||
}
|
||||
}
|
||||
externalURLs {
|
||||
url
|
||||
serviceKind
|
||||
@ -221,13 +204,9 @@ async function fetchRepositoryData(repoName: string): Promise<Omit<BlameHunkData
|
||||
{ repo: repoName }
|
||||
).pipe(
|
||||
map(dataOrThrowErrors),
|
||||
map(({ repository }) => {
|
||||
const firstCommitDate = repository?.firstEverCommit?.author?.date
|
||||
return {
|
||||
externalURLs: repository?.externalURLs,
|
||||
firstCommitDate: firstCommitDate ? new Date(firstCommitDate) : undefined,
|
||||
}
|
||||
})
|
||||
map(({ repository }) => ({
|
||||
externalURLs: repository?.externalURLs,
|
||||
}))
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -286,12 +265,12 @@ export const useBlameHunks = (
|
||||
() =>
|
||||
shouldFetchBlame
|
||||
? fetchBlameViaStreaming({ revision, repoName, filePath, sourcegraphURL })
|
||||
: of({ current: undefined, externalURLs: undefined, firstCommitDate: undefined }),
|
||||
: of({ current: undefined, externalURLs: undefined }),
|
||||
[shouldFetchBlame, revision, repoName, filePath, sourcegraphURL]
|
||||
)
|
||||
)
|
||||
|
||||
return hunks || { current: undefined, externalURLs: undefined, firstCommitDate: undefined }
|
||||
return hunks || { current: undefined, externalURLs: undefined }
|
||||
}
|
||||
|
||||
const ONE_MONTH = 30 * 24 * 60 * 60 * 1000
|
||||
|
||||
@ -1,42 +1,44 @@
|
||||
import { subYears } from 'date-fns'
|
||||
import { interpolatePurples } from 'd3-scale-chromatic'
|
||||
|
||||
// We use an exponential scale to get more diverse colors for more recent changes.
|
||||
//
|
||||
// The values are sampled from the following function:
|
||||
// y=0.005*1.7^x
|
||||
const STEPS = [0.008, 0.0144, 0.0245, 0.0417, 0.0709, 0.1206, 0.2051, 0.3487, 0.5929, 1]
|
||||
// MIDPOINT is the duration for which the scale function returns the midpoint color (0.5)
|
||||
// The closer the midpoint is to "now", the more pronounced the color difference for
|
||||
// recent commits. I.e. more recent commits will be easier to distinguish from each other
|
||||
// than older commits.
|
||||
// Conversely, if the midpoint is further back in time, the color difference for recent
|
||||
// commits is less pronounced.
|
||||
// Another factor is the granularity of time. If the scale is in hours, the color difference
|
||||
// for commits from the last few days is more pronounced than if the scale is in months.
|
||||
|
||||
const COLORS = [
|
||||
'var(--blame-recency-0)',
|
||||
'var(--blame-recency-1)',
|
||||
'var(--blame-recency-2)',
|
||||
'var(--blame-recency-3)',
|
||||
'var(--blame-recency-4)',
|
||||
'var(--blame-recency-5)',
|
||||
'var(--blame-recency-6)',
|
||||
'var(--blame-recency-7)',
|
||||
'var(--blame-recency-8)',
|
||||
'var(--blame-recency-9)',
|
||||
]
|
||||
const ONE_YEAR_AGO = subYears(Date.now(), 1).getTime()
|
||||
const THREE_YEARS_AGO = subYears(Date.now(), 3).getTime()
|
||||
const MIDPOINT = 6 * 30 * 24 // 6 months in hours
|
||||
const MILLIS_IN_HOUR = 1000 * 60 * 60
|
||||
|
||||
export function getBlameRecencyColor(commit: Date | undefined, firstCommitDate: Date | undefined): string {
|
||||
if (!commit) {
|
||||
return COLORS[0]
|
||||
/**
|
||||
* Get the color for the recency of a commit. The color is interpolated between
|
||||
* light grey and purple, with grey being the oldest and purple being the most recent.
|
||||
* The "direction" of color can be reversed, so that the most recent commits are light
|
||||
* and the oldest are dark (e.g. for dark mode).
|
||||
*
|
||||
* @param commitDate The date of the commit
|
||||
* @param lightToDark If true, the most recent commits will be light and the oldest dark. Default is false.
|
||||
* @returns The color for the recency of the commit. It's an `rgb(...)` CSS color string.
|
||||
*/
|
||||
export function getBlameRecencyColor(commitDate: Date | undefined, lightToDark = false): string {
|
||||
if (!commitDate) {
|
||||
return 'var(--gray-04)'
|
||||
}
|
||||
|
||||
// We create a recency range depending on the repo creation date. If the
|
||||
// repo is newer than a year, we use the last year so that we don't have a
|
||||
// scale that is too sensible.
|
||||
const now = Date.now()
|
||||
const start = Math.min(firstCommitDate ? firstCommitDate.getTime() : THREE_YEARS_AGO, ONE_YEAR_AGO)
|
||||
const age = (Date.now() - commitDate.getTime()) / MILLIS_IN_HOUR
|
||||
|
||||
// Get a value between [0, 1] that represents the recency of the commit in a linear scale
|
||||
const recency = Math.min(Math.max((now - commit.getTime()) / (now - start), 0), 1)
|
||||
// Get a value between [0, 1) that represents the recency of the commit
|
||||
// (0 is most recent, 1 is least recent)
|
||||
let recency = age / (age + MIDPOINT)
|
||||
|
||||
// Map from the linear scale to the exponential scale
|
||||
const index = STEPS.findIndex(step => recency <= step)
|
||||
// The color scheme goes from light (0) to dark (1), but unless lightToDark is true,
|
||||
// we want the most recent commits to be dark and the oldest to be light. Therefore
|
||||
// we simply invert the recency value.
|
||||
if (!lightToDark) {
|
||||
recency = 1 - recency
|
||||
}
|
||||
|
||||
return COLORS[index]
|
||||
return interpolatePurples(recency)
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ import { getBlameRecencyColor } from '../blameRecency'
|
||||
* Unlike {@link BlameHunkData} which is a list of unordered hunks, this
|
||||
* structure provides a line number -> blame hunk index for fast access.
|
||||
*/
|
||||
interface IndexedBlameHunkData extends Pick<BlameHunkData, 'firstCommitDate' | 'externalURLs'> {
|
||||
interface IndexedBlameHunkData extends Pick<BlameHunkData, 'externalURLs'> {
|
||||
lines: BlameHunk[]
|
||||
}
|
||||
|
||||
@ -245,7 +245,6 @@ const blameDataFacet = Facet.define<BlameHunkData, IndexedBlameHunkData>({
|
||||
}
|
||||
return {
|
||||
lines,
|
||||
firstCommitDate: value.firstCommitDate,
|
||||
externalURLs: value.externalURLs,
|
||||
}
|
||||
},
|
||||
@ -253,7 +252,7 @@ const blameDataFacet = Facet.define<BlameHunkData, IndexedBlameHunkData>({
|
||||
|
||||
class RecencyMarker extends GutterMarker {
|
||||
// hunk can be undefined if when the data is not available yet
|
||||
constructor(private line: number, private hunk?: BlameHunk) {
|
||||
constructor(private line: number, private hunk?: BlameHunk, private darkTheme?: boolean | null | undefined) {
|
||||
super()
|
||||
}
|
||||
|
||||
@ -261,18 +260,17 @@ class RecencyMarker extends GutterMarker {
|
||||
// Only consider two markers with the same line equal if
|
||||
// hunk data is available. Otherwise the marker won't be
|
||||
// update/recreated as new data becomes available.
|
||||
return this.line === other.line && !!this.hunk && !!other.hunk
|
||||
return this.line === other.line && this.darkTheme === other.darkTheme && !!this.hunk && !!other.hunk
|
||||
}
|
||||
|
||||
public toDOM(view: EditorView): Node {
|
||||
public toDOM(_view: EditorView): Node {
|
||||
const dom = document.createElement('div')
|
||||
dom.className = 'sg-recency-marker'
|
||||
const { firstCommitDate } = view.state.facet(blameDataFacet)
|
||||
if (this.hunk) {
|
||||
if (this.hunk.startLine === this.line) {
|
||||
dom.classList.add('border-top')
|
||||
}
|
||||
dom.style.backgroundColor = getBlameRecencyColor(new Date(this.hunk.author.date), firstCommitDate)
|
||||
dom.style.backgroundColor = getBlameRecencyColor(new Date(this.hunk.author.date), !this.darkTheme)
|
||||
}
|
||||
return dom
|
||||
}
|
||||
@ -288,10 +286,13 @@ const blameGutter: Extension = [
|
||||
lineMarker(view, line) {
|
||||
const lineNumber = view.state.doc.lineAt(line.from).number
|
||||
const hunks = view.state.facet(blameDataFacet).lines
|
||||
return new RecencyMarker(lineNumber, hunks[lineNumber])
|
||||
return new RecencyMarker(lineNumber, hunks[lineNumber], view.state.facet(EditorView.darkTheme))
|
||||
},
|
||||
lineMarkerChange(update) {
|
||||
return update.state.facet(blameDataFacet) !== update.startState.facet(blameDataFacet)
|
||||
return (
|
||||
update.state.facet(blameDataFacet) !== update.startState.facet(blameDataFacet) ||
|
||||
update.state.facet(EditorView.darkTheme) !== update.startState.facet(EditorView.darkTheme)
|
||||
)
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@ -308,18 +308,6 @@ $violet-09: #5f3dc4;
|
||||
--chroma-generic-traceback-fg: #aa0000;
|
||||
--chroma-text-whitespace-fg: #bbbbbb;
|
||||
|
||||
// Blame colors
|
||||
--blame-recency-0: var(--oc-violet-9);
|
||||
--blame-recency-1: var(--oc-violet-8);
|
||||
--blame-recency-2: var(--oc-violet-7);
|
||||
--blame-recency-3: var(--oc-violet-6);
|
||||
--blame-recency-4: var(--oc-violet-5);
|
||||
--blame-recency-5: var(--oc-violet-4);
|
||||
--blame-recency-6: var(--oc-violet-3);
|
||||
--blame-recency-7: var(--oc-violet-2);
|
||||
--blame-recency-8: var(--oc-violet-1);
|
||||
--blame-recency-9: var(--oc-violet-0);
|
||||
|
||||
// Header colors
|
||||
--result-header-bg: #f5f8fa;
|
||||
|
||||
@ -494,18 +482,6 @@ $violet-09: #5f3dc4;
|
||||
--chroma-generic-traceback-fg: #ff7b72;
|
||||
--chroma-text-whitespace-fg: #6e7681;
|
||||
|
||||
// Blame colors
|
||||
--blame-recency-0: var(--oc-violet-0);
|
||||
--blame-recency-1: var(--oc-violet-1);
|
||||
--blame-recency-2: var(--oc-violet-2);
|
||||
--blame-recency-3: var(--oc-violet-3);
|
||||
--blame-recency-4: var(--oc-violet-4);
|
||||
--blame-recency-5: var(--oc-violet-5);
|
||||
--blame-recency-6: var(--oc-violet-6);
|
||||
--blame-recency-7: var(--oc-violet-7);
|
||||
--blame-recency-8: var(--oc-violet-8);
|
||||
--blame-recency-9: var(--oc-violet-9);
|
||||
|
||||
// Header colors
|
||||
--result-header-bg: #22283b;
|
||||
|
||||
|
||||
@ -123,6 +123,7 @@
|
||||
"@types/connect-history-api-fallback": "1.3.4",
|
||||
"@types/d3-format": "2.0.0",
|
||||
"@types/d3-scale": "^3.3.0",
|
||||
"@types/d3-scale-chromatic": "^3.0.3",
|
||||
"@types/d3-shape": "^1.3.1",
|
||||
"@types/d3-time-format": "3.0.0",
|
||||
"@types/d3-voronoi": "^1.1.9",
|
||||
@ -335,6 +336,7 @@
|
||||
"core-js": "^3.8.2",
|
||||
"d3-format": "^2.0.0",
|
||||
"d3-scale": "^3.3.0",
|
||||
"d3-scale-chromatic": "^3.0.0",
|
||||
"d3-shape": "^1.2.0",
|
||||
"d3-time-format": "^3.0.0",
|
||||
"d3-voronoi": "^1.1.2",
|
||||
|
||||
@ -211,6 +211,9 @@ importers:
|
||||
d3-scale:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
d3-scale-chromatic:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
d3-shape:
|
||||
specifier: ^1.2.0
|
||||
version: 1.3.7
|
||||
@ -653,6 +656,9 @@ importers:
|
||||
'@types/d3-scale':
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.2
|
||||
'@types/d3-scale-chromatic':
|
||||
specifier: ^3.0.3
|
||||
version: 3.0.3
|
||||
'@types/d3-shape':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.2
|
||||
@ -10776,6 +10782,10 @@ packages:
|
||||
/@types/d3-path@1.0.9:
|
||||
resolution: {integrity: sha512-NaIeSIBiFgSC6IGUBjZWcscUJEq7vpVu7KthHN8eieTV9d9MqkSOZLH4chq1PmcKy06PNe3axLeKmRIyxJ+PZQ==}
|
||||
|
||||
/@types/d3-scale-chromatic@3.0.3:
|
||||
resolution: {integrity: sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==}
|
||||
dev: true
|
||||
|
||||
/@types/d3-scale@3.3.2:
|
||||
resolution: {integrity: sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ==}
|
||||
dependencies:
|
||||
@ -14281,6 +14291,14 @@ packages:
|
||||
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
|
||||
dev: false
|
||||
|
||||
/d3-scale-chromatic@3.0.0:
|
||||
resolution: {integrity: sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-color: 1.2.3
|
||||
d3-interpolate: 1.4.0
|
||||
dev: false
|
||||
|
||||
/d3-scale@2.2.2:
|
||||
resolution: {integrity: sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==}
|
||||
dependencies:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user