1
1
import { useMemo } from 'react'
2
2
3
+ import { fetchEventSource } from '@microsoft/fetch-event-source'
3
4
import { formatDistanceStrict } from 'date-fns'
4
5
import { truncate } from 'lodash'
5
6
import { Observable , of } from 'rxjs'
6
- import { map } from 'rxjs/operators'
7
+ import { map , throttleTime } from 'rxjs/operators'
7
8
8
9
import { memoizeObservable } from '@sourcegraph/common'
9
10
import { dataOrThrowErrors , gql } from '@sourcegraph/http-client'
10
11
import { makeRepoURI } from '@sourcegraph/shared/src/util/url'
11
12
import { useObservable } from '@sourcegraph/wildcard'
12
13
13
14
import { requestGraphQL } from '../../backend/graphql'
15
+ import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
14
16
import { GitBlameResult , GitBlameVariables } from '../../graphql-operations'
15
17
16
18
import { useBlameVisibility } from './useBlameVisibility'
@@ -24,20 +26,45 @@ interface BlameHunkDisplayInfo {
24
26
message : string
25
27
}
26
28
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
+ }
30
55
31
- const fetchBlame = memoizeObservable (
56
+ const fetchBlameViaGraphQL = memoizeObservable (
32
57
( {
33
58
repoName,
34
59
revision,
35
60
filePath,
61
+ sourcegraphURL,
36
62
} : {
37
63
repoName : string
38
64
revision : string
39
65
filePath : string
40
- } ) : Observable < Omit < BlameHunk , 'displayInfo' > [ ] | undefined > =>
66
+ sourcegraphURL : string
67
+ } ) : Observable < { current : BlameHunk [ ] | undefined } > =>
41
68
requestGraphQL < GitBlameResult , GitBlameVariables > (
42
69
gql `
43
70
query GitBlame($repo: String!, $rev: String!, $path: String!) {
@@ -74,65 +101,151 @@ const fetchBlame = memoizeObservable(
74
101
{ repo : repoName , rev : revision , path : filePath }
75
102
) . pipe (
76
103
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 } ) )
78
107
) ,
79
108
makeRepoURI
80
109
)
81
110
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
+
82
189
/**
83
190
* Get display info shared between status bar items and text document decorations.
84
191
*/
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
+
90
196
const displayName = truncate ( author . person . displayName , { length : 25 } )
91
197
const username = author . person . user ? `(${ author . person . user . username } ) ` : ''
92
198
const dateString = formatDistanceStrict ( new Date ( author . date ) , now , { addSuffix : true } )
93
199
const timestampString = new Date ( author . date ) . toLocaleString ( )
94
200
const linkURL = new URL ( commit . url , sourcegraphURL ) . href
95
201
const content = `${ dateString } • ${ username } ${ displayName } [${ truncate ( message , { length : 45 } ) } ]`
96
202
97
- return {
203
+ ; ( hunk as BlameHunk ) . displayInfo = {
98
204
displayName,
99
205
username,
100
206
dateString,
101
207
timestampString,
102
208
linkURL,
103
209
message : content ,
104
210
}
211
+ return hunk as BlameHunk
105
212
}
106
213
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
+ */
107
219
export const useBlameHunks = (
108
220
{
109
221
repoName,
110
222
revision,
111
223
filePath,
224
+ enableCodeMirror,
112
225
} : {
113
226
repoName : string
114
227
revision : string
115
228
filePath : string
229
+ enableCodeMirror : boolean
116
230
} ,
117
231
sourcegraphURL : string
118
- ) : BlameHunk [ ] | undefined => {
232
+ ) : { current : BlameHunk [ ] | undefined } => {
233
+ const [ enableStreamingGitBlame , status ] = useFeatureFlag ( 'enable-streaming-git-blame' )
234
+
119
235
const [ isBlameVisible ] = useBlameVisibility ( )
236
+ const shouldFetchBlame = isBlameVisible && status !== 'initial'
237
+
120
238
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
+ )
127
248
)
128
249
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 }
138
251
}
0 commit comments