Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 7c0dc63

Browse files
mrnuggetjhchabranphilipp-spiesstaras-yemets
committed
Stream git blame results (#44199)
Co-authored-by: Jean-Hadrien Chabran <[email protected]> Co-authored-by: Philipp Spiess <[email protected]> Co-authored-by: Taras Yemets <[email protected]>
1 parent 321e965 commit 7c0dc63

File tree

16 files changed

+1413
-42
lines changed

16 files changed

+1413
-42
lines changed

client/web/src/featureFlags/featureFlags.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type FeatureFlagName =
1515
| 'admin-analytics-cache-disabled'
1616
| 'search-input-show-history'
1717
| 'user-management-disabled'
18+
| 'enable-streaming-git-blame'
1819

1920
interface OrgFlagOverride {
2021
orgID: string

client/web/src/repo/blame/useBlameHunks.ts

Lines changed: 142 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import { useMemo } from 'react'
22

3+
import { fetchEventSource } from '@microsoft/fetch-event-source'
34
import { formatDistanceStrict } from 'date-fns'
45
import { truncate } from 'lodash'
56
import { Observable, of } from 'rxjs'
6-
import { map } from 'rxjs/operators'
7+
import { map, throttleTime } from 'rxjs/operators'
78

89
import { memoizeObservable } from '@sourcegraph/common'
910
import { dataOrThrowErrors, gql } from '@sourcegraph/http-client'
1011
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
1112
import { useObservable } from '@sourcegraph/wildcard'
1213

1314
import { requestGraphQL } from '../../backend/graphql'
15+
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
1416
import { GitBlameResult, GitBlameVariables } from '../../graphql-operations'
1517

1618
import { useBlameVisibility } from './useBlameVisibility'
@@ -24,20 +26,45 @@ interface BlameHunkDisplayInfo {
2426
message: string
2527
}
2628

27-
export type BlameHunk = NonNullable<
28-
NonNullable<NonNullable<GitBlameResult['repository']>['commit']>['blob']
29-
>['blame'][number] & { displayInfo: BlameHunkDisplayInfo }
29+
export interface BlameHunk {
30+
startLine: number
31+
endLine: number
32+
message: string
33+
rev: string
34+
author: {
35+
date: string
36+
person: {
37+
email: string
38+
displayName: string
39+
user:
40+
| undefined
41+
| null
42+
| {
43+
username: string
44+
}
45+
}
46+
}
47+
commit: {
48+
url: string
49+
parents: {
50+
oid: string
51+
}[]
52+
}
53+
displayInfo: BlameHunkDisplayInfo
54+
}
3055

31-
const fetchBlame = memoizeObservable(
56+
const fetchBlameViaGraphQL = memoizeObservable(
3257
({
3358
repoName,
3459
revision,
3560
filePath,
61+
sourcegraphURL,
3662
}: {
3763
repoName: string
3864
revision: string
3965
filePath: string
40-
}): Observable<Omit<BlameHunk, 'displayInfo'>[] | undefined> =>
66+
sourcegraphURL: string
67+
}): Observable<{ current: BlameHunk[] | undefined }> =>
4168
requestGraphQL<GitBlameResult, GitBlameVariables>(
4269
gql`
4370
query GitBlame($repo: String!, $rev: String!, $path: String!) {
@@ -74,65 +101,151 @@ const fetchBlame = memoizeObservable(
74101
{ repo: repoName, rev: revision, path: filePath }
75102
).pipe(
76103
map(dataOrThrowErrors),
77-
map(({ repository }) => repository?.commit?.blob?.blame)
104+
map(({ repository }) => repository?.commit?.blob?.blame),
105+
map(hunks => (hunks ? hunks.map(blame => addDisplayInfoForHunk(blame, sourcegraphURL)) : undefined)),
106+
map(hunks => ({ current: hunks }))
78107
),
79108
makeRepoURI
80109
)
81110

111+
interface RawStreamHunk {
112+
author: {
113+
Name: string
114+
Email: string
115+
Date: string
116+
}
117+
commit: {
118+
parents: string[]
119+
url: string
120+
}
121+
commitID: string
122+
endLine: number
123+
startLine: number
124+
filename: string
125+
message: string
126+
}
127+
128+
const fetchBlameViaStreaming = memoizeObservable(
129+
({
130+
repoName,
131+
revision,
132+
filePath,
133+
sourcegraphURL,
134+
}: {
135+
repoName: string
136+
revision: string
137+
filePath: string
138+
sourcegraphURL: string
139+
}): Observable<{ current: BlameHunk[] | undefined }> =>
140+
new Observable<{ current: BlameHunk[] | undefined }>(subscriber => {
141+
const assembledHunks: BlameHunk[] = []
142+
const repoAndRevisionPath = `/${repoName}${revision ? `@${revision}` : ''}`
143+
fetchEventSource(`/.api/blame${repoAndRevisionPath}/stream/${filePath}`, {
144+
method: 'GET',
145+
headers: {
146+
'X-Requested-With': 'Sourcegraph',
147+
'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false',
148+
},
149+
onmessage(event) {
150+
if (event.event === 'hunk') {
151+
const rawHunks: RawStreamHunk[] = JSON.parse(event.data)
152+
for (const rawHunk of rawHunks) {
153+
const hunk: Omit<BlameHunk, 'displayInfo'> = {
154+
startLine: rawHunk.startLine,
155+
endLine: rawHunk.endLine,
156+
message: rawHunk.message,
157+
rev: rawHunk.commitID,
158+
author: {
159+
date: rawHunk.author.Date,
160+
person: {
161+
email: rawHunk.author.Email,
162+
displayName: rawHunk.author.Name,
163+
user: null,
164+
},
165+
},
166+
commit: {
167+
url: rawHunk.commit.url,
168+
parents: rawHunk.commit.parents ? rawHunk.commit.parents.map(oid => ({ oid })) : [],
169+
},
170+
}
171+
assembledHunks.push(addDisplayInfoForHunk(hunk, sourcegraphURL))
172+
}
173+
subscriber.next({ current: assembledHunks })
174+
}
175+
},
176+
onerror(event) {
177+
// eslint-disable-next-line no-console
178+
console.error(event)
179+
},
180+
}).then(
181+
() => subscriber.complete(),
182+
error => subscriber.error(error)
183+
)
184+
// Throttle the results to avoid re-rendering the blame sidebar for every hunk
185+
}).pipe(throttleTime(1000, undefined, { leading: true, trailing: true })),
186+
makeRepoURI
187+
)
188+
82189
/**
83190
* Get display info shared between status bar items and text document decorations.
84191
*/
85-
const getDisplayInfoFromHunk = (
86-
{ author, commit, message }: Omit<BlameHunk, 'displayInfo'>,
87-
sourcegraphURL: string,
88-
now: number
89-
): BlameHunkDisplayInfo => {
192+
const addDisplayInfoForHunk = (hunk: Omit<BlameHunk, 'displayInfo'>, sourcegraphURL: string): BlameHunk => {
193+
const now = Date.now()
194+
const { author, commit, message } = hunk
195+
90196
const displayName = truncate(author.person.displayName, { length: 25 })
91197
const username = author.person.user ? `(${author.person.user.username}) ` : ''
92198
const dateString = formatDistanceStrict(new Date(author.date), now, { addSuffix: true })
93199
const timestampString = new Date(author.date).toLocaleString()
94200
const linkURL = new URL(commit.url, sourcegraphURL).href
95201
const content = `${dateString}${username}${displayName} [${truncate(message, { length: 45 })}]`
96202

97-
return {
203+
;(hunk as BlameHunk).displayInfo = {
98204
displayName,
99205
username,
100206
dateString,
101207
timestampString,
102208
linkURL,
103209
message: content,
104210
}
211+
return hunk as BlameHunk
105212
}
106213

214+
/**
215+
* For performance reasons, the hunks array can be mutated in place. To still be
216+
* able to propagate updates accordingly, this is wrapped in a ref object that
217+
* can be recreated whenever we emit new values.
218+
*/
107219
export const useBlameHunks = (
108220
{
109221
repoName,
110222
revision,
111223
filePath,
224+
enableCodeMirror,
112225
}: {
113226
repoName: string
114227
revision: string
115228
filePath: string
229+
enableCodeMirror: boolean
116230
},
117231
sourcegraphURL: string
118-
): BlameHunk[] | undefined => {
232+
): { current: BlameHunk[] | undefined } => {
233+
const [enableStreamingGitBlame, status] = useFeatureFlag('enable-streaming-git-blame')
234+
119235
const [isBlameVisible] = useBlameVisibility()
236+
const shouldFetchBlame = isBlameVisible && status !== 'initial'
237+
120238
const hunks = useObservable(
121-
useMemo(() => (isBlameVisible ? fetchBlame({ revision, repoName, filePath }) : of(undefined)), [
122-
isBlameVisible,
123-
revision,
124-
repoName,
125-
filePath,
126-
])
239+
useMemo(
240+
() =>
241+
shouldFetchBlame
242+
? enableCodeMirror && enableStreamingGitBlame
243+
? fetchBlameViaStreaming({ revision, repoName, filePath, sourcegraphURL })
244+
: fetchBlameViaGraphQL({ revision, repoName, filePath, sourcegraphURL })
245+
: of({ current: undefined }),
246+
[shouldFetchBlame, enableCodeMirror, enableStreamingGitBlame, revision, repoName, filePath, sourcegraphURL]
247+
)
127248
)
128249

129-
const hunksWithDisplayInfo = useMemo(() => {
130-
const now = Date.now()
131-
return hunks?.map(hunk => ({
132-
...hunk,
133-
displayInfo: getDisplayInfoFromHunk(hunk, sourcegraphURL, now),
134-
}))
135-
}, [hunks, sourcegraphURL])
136-
137-
return hunksWithDisplayInfo
250+
return hunks || { current: undefined }
138251
}

client/web/src/repo/blob/BlameColumn.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import styles from './BlameColumn.module.scss'
1212

1313
interface BlameColumnProps {
1414
isBlameVisible?: boolean
15-
blameHunks?: BlameHunk[]
15+
blameHunks?: { current: BlameHunk[] | undefined }
1616
codeViewElements: ReplaySubject<HTMLElement | null>
1717
history: History
1818
}
@@ -98,7 +98,7 @@ export const BlameColumn = React.memo<BlameColumnProps>(({ isBlameVisible, codeV
9898
}
9999
}
100100

101-
const currentLineDecorations = blameHunks?.find(hunk => hunk.startLine - 1 === index)
101+
const currentLineDecorations = blameHunks?.current?.find(hunk => hunk.startLine - 1 === index)
102102

103103
// store created cell and corresponding blame hunk (or undefined if no blame hunk)
104104
addedCells.push([cell, currentLineDecorations])

client/web/src/repo/blob/Blob.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ export interface BlobProps
140140
ariaLabel?: string
141141

142142
isBlameVisible?: boolean
143-
blameHunks?: BlameHunk[]
143+
blameHunks?: { current: BlameHunk[] | undefined }
144144
}
145145

146146
export interface BlobInfo extends AbsoluteRepoFile, ModeSpec {

client/web/src/repo/blob/BlobPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,10 @@ export const BlobPage: React.FunctionComponent<React.PropsWithChildren<BlobPageP
330330
}
331331

332332
const [isBlameVisible] = useBlameVisibility()
333-
const blameDecorations = useBlameHunks({ repoName, revision, filePath }, props.platformContext.sourcegraphURL)
333+
const blameDecorations = useBlameHunks(
334+
{ repoName, revision, filePath, enableCodeMirror },
335+
props.platformContext.sourcegraphURL
336+
)
334337

335338
const isSearchNotebook = Boolean(
336339
blobInfoOrError &&

client/web/src/repo/blob/CodeMirrorBlob.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* An experimental implementation of the Blob view using CodeMirror
33
*/
44

5-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
5+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
66

77
import { openSearchPanel } from '@codemirror/search'
88
import { Compartment, EditorState, Extension } from '@codemirror/state'
@@ -122,7 +122,10 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
122122
[wrapCode, isLightTheme]
123123
)
124124

125-
const blameDecorations = useMemo(() => (blameHunks ? [showGitBlameDecorations.of(blameHunks)] : []), [blameHunks])
125+
const blameDecorations = useMemo(
126+
() => (blameHunks?.current ? [showGitBlameDecorations.of(blameHunks.current)] : []),
127+
[blameHunks]
128+
)
126129

127130
const preloadGoToDefinition = useExperimentalFeatures(features => features.preloadGoToDefinition ?? false)
128131

@@ -249,7 +252,7 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
249252
}, [blobProps])
250253

251254
// Update blame information
252-
useEffect(() => {
255+
useLayoutEffect(() => {
253256
if (editor) {
254257
editor.dispatch({ effects: blameDecorationsCompartment.reconfigure(blameDecorations) })
255258
}

client/web/src/repo/blob/codemirror/blame-decorations.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,10 @@ export const showGitBlameDecorations = Facet.define<BlameHunk[], BlameHunk[]>({
139139
const hunk = longestColumnDecorations(view.state.facet(facet))
140140
return new BlameDecoratorMarker(view, hunk, true)
141141
},
142-
// Markers need to be updated when theme changes
142+
// Markers need to be updated when theme changes or the hunks change
143143
lineMarkerChange: update =>
144-
update.startState.facet(EditorView.darkTheme) !== update.state.facet(EditorView.darkTheme),
144+
update.startState.facet(EditorView.darkTheme) !== update.state.facet(EditorView.darkTheme) ||
145+
update.startState.facet(facet) !== update.state.facet(facet),
145146
}),
146147
hoveredLine,
147148
],

cmd/frontend/internal/httpapi/httpapi.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ func NewHandler(
126126
// Return the minimum src-cli version that's compatible with this instance
127127
m.Get(apirouter.SrcCli).Handler(trace.Route(newSrcCliVersionHandler(logger)))
128128

129+
gsClient := gitserver.NewClient(db)
130+
m.Get(apirouter.GitBlameStream).Handler(trace.Route(handleStreamBlame(logger, db, gsClient)))
131+
129132
// Set up the src-cli version cache handler (this will effectively be a
130133
// no-op anywhere other than dot-com).
131134
m.Get(apirouter.SrcCliVersionCache).Handler(trace.Route(releasecache.NewHandler(logger)))

cmd/frontend/internal/httpapi/router/router.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const (
1111
LSIFUpload = "lsif.upload"
1212
GraphQL = "graphql"
1313

14-
SearchStream = "search.stream"
15-
ComputeStream = "compute.stream"
14+
SearchStream = "search.stream"
15+
ComputeStream = "compute.stream"
16+
GitBlameStream = "git.blame.stream"
1617

1718
SrcCli = "src-cli"
1819
SrcCliVersionCache = "src-cli.version-cache"
@@ -69,6 +70,7 @@ func New(base *mux.Router) *mux.Router {
6970
base.Path("/lsif/upload").Methods("POST").Name(LSIFUpload)
7071
base.Path("/search/stream").Methods("GET").Name(SearchStream)
7172
base.Path("/compute/stream").Methods("GET", "POST").Name(ComputeStream)
73+
base.Path("/blame/" + routevar.Repo + routevar.RepoRevSuffix + "/stream/{Path:.*}").Methods("GET").Name(GitBlameStream)
7274
base.Path("/src-cli/versions/{rest:.*}").Methods("GET", "POST").Name(SrcCliVersionCache)
7375
base.Path("/src-cli/{rest:.*}").Methods("GET").Name(SrcCli)
7476

0 commit comments

Comments
 (0)