Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3ede673
WIP HACK: Streaming git blame
mrnugget Nov 10, 2022
b6ca12b
Setup infrastructure, dummy reader and failing tests
mrnugget Nov 11, 2022
451e290
Get line-by-line parsing of `git blame --incremental` working
mrnugget Nov 11, 2022
ac134e5
Debug printing
mrnugget Nov 11, 2022
a828cdb
Add failing test and explanation
mrnugget Nov 14, 2022
b007808
Hastily patch the parser to handle --incremental outputs
jhchabran Nov 14, 2022
b1765f2
Rewrite the parser and clean things a bit
jhchabran Nov 14, 2022
d639898
Put the route behind a feature flag
jhchabran Nov 14, 2022
11de772
deal with nits
jhchabran Nov 14, 2022
dd36dee
Update mocks
jhchabran Nov 14, 2022
893721f
fix things, it's the morning
jhchabran Nov 15, 2022
36365d2
Implement chunk loading for blame data
philipp-spiess Nov 16, 2022
d0371b6
Fix blame facets not updating in UI
philipp-spiess Nov 16, 2022
2129743
Compute remaining data
philipp-spiess Nov 16, 2022
5ebf583
push my stuff plz
jhchabran Nov 16, 2022
6967622
facepalme
jhchabran Nov 16, 2022
d22e378
Use streamhttp
jhchabran Nov 17, 2022
27277e1
Patch the client side
jhchabran Nov 17, 2022
2d87679
Move it under .api
jhchabran Nov 17, 2022
aa9396e
add tracing
jhchabran Nov 17, 2022
9d4b299
Clean stuff and add tests
jhchabran Nov 18, 2022
e6b15a7
Remove wip leftovers
jhchabran Nov 18, 2022
670315c
woopr
jhchabran Nov 18, 2022
856360d
Drop changelog note
jhchabran Nov 18, 2022
c099d69
Linter stuff
jhchabran Nov 18, 2022
37a1758
Remove todos
jhchabran Nov 18, 2022
8e1489f
Frontend cleanups
philipp-spiess Nov 21, 2022
c83e53c
Merge remote-tracking branch 'origin/main' into mrn+jh/streaming-git-…
philipp-spiess Nov 21, 2022
08ce771
Fix goroutines
jhchabran Nov 21, 2022
7708327
Test a bit further
jhchabran Nov 21, 2022
31c07c1
More frontend cleanups
philipp-spiess Nov 21, 2022
ee3f156
Merge branch 'mrn+jh/streaming-git-blame' of github.com:sourcegraph/s…
philipp-spiess Nov 21, 2022
bcaf2aa
Fix types and commit URL
philipp-spiess Nov 21, 2022
31f72db
Stop closing dumbly
jhchabran Nov 21, 2022
c1c0e41
Fill the commit field in the response
jhchabran Nov 21, 2022
10176c5
Fix tests
jhchabran Nov 21, 2022
b5ec8ce
Final round of cleanups and add FE throttling
philipp-spiess Nov 21, 2022
199147a
Construct commit url
jhchabran Nov 21, 2022
d284511
Fix throttle logic
philipp-spiess Nov 21, 2022
878f880
Merge branch 'mrn+jh/streaming-git-blame' of github.com:sourcegraph/s…
philipp-spiess Nov 21, 2022
82cb86e
Fix commit url
jhchabran Nov 21, 2022
105245c
remove cruft
jhchabran Nov 21, 2022
60a9ad3
Don't set default
philipp-spiess Nov 21, 2022
fb6c032
Fix flickering in UI by updating codemirror synchronously with new data
philipp-spiess Nov 21, 2022
43ac0ab
Fix ctx.done
jhchabran Nov 21, 2022
b260ae8
Fix stray stuff and dumb mistake
jhchabran Nov 22, 2022
3168a6c
Fix concurrency
jhchabran Nov 22, 2022
068a537
Frontend: Avoid array copies
philipp-spiess Nov 22, 2022
8dd7c16
Frontend: Fix types
philipp-spiess Nov 22, 2022
5e82da2
Update client/web/src/repo/blame/useBlameHunks.ts
philipp-spiess Nov 22, 2022
7ba3a6b
Merge remote-tracking branch 'origin/main' into mrn+jh/streaming-git-…
philipp-spiess Nov 24, 2022
e29a2f9
Fix nits
jhchabran Nov 24, 2022
aa7b04a
Fix authentication case not being handled properly
jhchabran Nov 24, 2022
cfb65bd
Fix typo
philipp-spiess Nov 24, 2022
55825b2
Merge branch 'mrn+jh/streaming-git-blame' of github.com:sourcegraph/s…
philipp-spiess Nov 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/web/src/featureFlags/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type FeatureFlagName =
| 'admin-analytics-cache-disabled'
| 'search-input-show-history'
| 'user-management-disabled'
| 'enable-streaming-git-blame'

interface OrgFlagOverride {
orgID: string
Expand Down
171 changes: 142 additions & 29 deletions client/web/src/repo/blame/useBlameHunks.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { useMemo } from 'react'

import { fetchEventSource } from '@microsoft/fetch-event-source'
import { formatDistanceStrict } from 'date-fns'
import { truncate } from 'lodash'
import { Observable, of } from 'rxjs'
import { map } from 'rxjs/operators'
import { map, throttleTime } from 'rxjs/operators'

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

import { requestGraphQL } from '../../backend/graphql'
import { useFeatureFlag } from '../../featureFlags/useFeatureFlag'
import { GitBlameResult, GitBlameVariables } from '../../graphql-operations'

import { useBlameVisibility } from './useBlameVisibility'
Expand All @@ -24,20 +26,45 @@ interface BlameHunkDisplayInfo {
message: string
}

export type BlameHunk = NonNullable<
NonNullable<NonNullable<GitBlameResult['repository']>['commit']>['blob']
>['blame'][number] & { displayInfo: BlameHunkDisplayInfo }
export interface BlameHunk {
startLine: number
endLine: number
message: string
rev: string
author: {
date: string
person: {
email: string
displayName: string
user:
| undefined
| null
| {
username: string
}
}
}
commit: {
url: string
parents: {
oid: string
}[]
}
displayInfo: BlameHunkDisplayInfo
}

const fetchBlame = memoizeObservable(
const fetchBlameViaGraphQL = memoizeObservable(
({
repoName,
revision,
filePath,
sourcegraphURL,
}: {
repoName: string
revision: string
filePath: string
}): Observable<Omit<BlameHunk, 'displayInfo'>[] | undefined> =>
sourcegraphURL: string
}): Observable<{ current: BlameHunk[] | undefined }> =>
requestGraphQL<GitBlameResult, GitBlameVariables>(
gql`
query GitBlame($repo: String!, $rev: String!, $path: String!) {
Expand Down Expand Up @@ -74,65 +101,151 @@ const fetchBlame = memoizeObservable(
{ repo: repoName, rev: revision, path: filePath }
).pipe(
map(dataOrThrowErrors),
map(({ repository }) => repository?.commit?.blob?.blame)
map(({ repository }) => repository?.commit?.blob?.blame),
map(hunks => (hunks ? hunks.map(blame => addDisplayInfoForHunk(blame, sourcegraphURL)) : undefined)),
map(hunks => ({ current: hunks }))
),
makeRepoURI
)

interface RawStreamHunk {
author: {
Name: string
Email: string
Date: string
}
commit: {
parents: string[]
url: string
}
commitID: string
endLine: number
startLine: number
filename: string
message: string
}

const fetchBlameViaStreaming = memoizeObservable(
({
repoName,
revision,
filePath,
sourcegraphURL,
}: {
repoName: string
revision: string
filePath: string
sourcegraphURL: string
}): Observable<{ current: BlameHunk[] | undefined }> =>
new Observable<{ current: BlameHunk[] | undefined }>(subscriber => {
const assembledHunks: BlameHunk[] = []
const repoAndRevisionPath = `/${repoName}${revision ? `@${revision}` : ''}`
fetchEventSource(`/.api/blame${repoAndRevisionPath}/stream/${filePath}`, {
method: 'GET',
headers: {
'X-Requested-With': 'Sourcegraph',
'X-Sourcegraph-Should-Trace': new URLSearchParams(window.location.search).get('trace') || 'false',
},
onmessage(event) {
if (event.event === 'hunk') {
const rawHunks: RawStreamHunk[] = JSON.parse(event.data)
for (const rawHunk of rawHunks) {
const hunk: Omit<BlameHunk, 'displayInfo'> = {
startLine: rawHunk.startLine,
endLine: rawHunk.endLine,
message: rawHunk.message,
rev: rawHunk.commitID,
author: {
date: rawHunk.author.Date,
person: {
email: rawHunk.author.Email,
displayName: rawHunk.author.Name,
user: null,
},
},
commit: {
url: rawHunk.commit.url,
parents: rawHunk.commit.parents ? rawHunk.commit.parents.map(oid => ({ oid })) : [],
},
}
assembledHunks.push(addDisplayInfoForHunk(hunk, sourcegraphURL))
}
subscriber.next({ current: assembledHunks })
}
},
onerror(event) {
// eslint-disable-next-line no-console
console.error(event)
},
}).then(
() => subscriber.complete(),
error => subscriber.error(error)
)
// Throttle the results to avoid re-rendering the blame sidebar for every hunk
}).pipe(throttleTime(1000, undefined, { leading: true, trailing: true })),
makeRepoURI
)

/**
* Get display info shared between status bar items and text document decorations.
*/
const getDisplayInfoFromHunk = (
{ author, commit, message }: Omit<BlameHunk, 'displayInfo'>,
sourcegraphURL: string,
now: number
): BlameHunkDisplayInfo => {
const addDisplayInfoForHunk = (hunk: Omit<BlameHunk, 'displayInfo'>, sourcegraphURL: string): BlameHunk => {
const now = Date.now()
const { author, commit, message } = hunk

const displayName = truncate(author.person.displayName, { length: 25 })
const username = author.person.user ? `(${author.person.user.username}) ` : ''
const dateString = formatDistanceStrict(new Date(author.date), now, { addSuffix: true })
const timestampString = new Date(author.date).toLocaleString()
const linkURL = new URL(commit.url, sourcegraphURL).href
const content = `${dateString} • ${username}${displayName} [${truncate(message, { length: 45 })}]`

return {
;(hunk as BlameHunk).displayInfo = {
displayName,
username,
dateString,
timestampString,
linkURL,
message: content,
}
return hunk as BlameHunk
}

/**
* For performance reasons, the hunks array can be mutated in place. To still be
* able to propagate updates accordingly, this is wrapped in a ref object that
* can be recreated whenever we emit new values.
*/
export const useBlameHunks = (
{
repoName,
revision,
filePath,
enableCodeMirror,
}: {
repoName: string
revision: string
filePath: string
enableCodeMirror: boolean
},
sourcegraphURL: string
): BlameHunk[] | undefined => {
): { current: BlameHunk[] | undefined } => {
const [enableStreamingGitBlame, status] = useFeatureFlag('enable-streaming-git-blame')

const [isBlameVisible] = useBlameVisibility()
const shouldFetchBlame = isBlameVisible && status !== 'initial'

const hunks = useObservable(
useMemo(() => (isBlameVisible ? fetchBlame({ revision, repoName, filePath }) : of(undefined)), [
isBlameVisible,
revision,
repoName,
filePath,
])
useMemo(
() =>
shouldFetchBlame
? enableCodeMirror && enableStreamingGitBlame
? fetchBlameViaStreaming({ revision, repoName, filePath, sourcegraphURL })
: fetchBlameViaGraphQL({ revision, repoName, filePath, sourcegraphURL })
: of({ current: undefined }),
[shouldFetchBlame, enableCodeMirror, enableStreamingGitBlame, revision, repoName, filePath, sourcegraphURL]
)
)

const hunksWithDisplayInfo = useMemo(() => {
const now = Date.now()
return hunks?.map(hunk => ({
...hunk,
displayInfo: getDisplayInfoFromHunk(hunk, sourcegraphURL, now),
}))
}, [hunks, sourcegraphURL])

return hunksWithDisplayInfo
return hunks || { current: undefined }
}
4 changes: 2 additions & 2 deletions client/web/src/repo/blob/BlameColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import styles from './BlameColumn.module.scss'

interface BlameColumnProps {
isBlameVisible?: boolean
blameHunks?: BlameHunk[]
blameHunks?: { current: BlameHunk[] | undefined }
codeViewElements: ReplaySubject<HTMLElement | null>
history: History
}
Expand Down Expand Up @@ -98,7 +98,7 @@ export const BlameColumn = React.memo<BlameColumnProps>(({ isBlameVisible, codeV
}
}

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

// store created cell and corresponding blame hunk (or undefined if no blame hunk)
addedCells.push([cell, currentLineDecorations])
Expand Down
2 changes: 1 addition & 1 deletion client/web/src/repo/blob/Blob.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export interface BlobProps
ariaLabel?: string

isBlameVisible?: boolean
blameHunks?: BlameHunk[]
blameHunks?: { current: BlameHunk[] | undefined }
}

export interface BlobInfo extends AbsoluteRepoFile, ModeSpec {
Expand Down
5 changes: 4 additions & 1 deletion client/web/src/repo/blob/BlobPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,10 @@ export const BlobPage: React.FunctionComponent<React.PropsWithChildren<BlobPageP
}

const [isBlameVisible] = useBlameVisibility()
const blameDecorations = useBlameHunks({ repoName, revision, filePath }, props.platformContext.sourcegraphURL)
const blameDecorations = useBlameHunks(
{ repoName, revision, filePath, enableCodeMirror },
props.platformContext.sourcegraphURL
)

const isSearchNotebook = Boolean(
blobInfoOrError &&
Expand Down
6 changes: 3 additions & 3 deletions client/web/src/repo/blob/CodeMirrorBlob.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* An experimental implementation of the Blob view using CodeMirror
*/

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'

import { openSearchPanel } from '@codemirror/search'
import { Compartment, EditorState, Extension } from '@codemirror/state'
Expand Down Expand Up @@ -129,7 +129,7 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
isBlameVisible,
])
const blameDecorations = useMemo(
() => (isBlameVisible && blameHunks ? [showGitBlameDecorations.of(blameHunks)] : []),
() => (isBlameVisible && blameHunks?.current ? [showGitBlameDecorations.of(blameHunks.current)] : []),
[isBlameVisible, blameHunks]
)

Expand Down Expand Up @@ -269,7 +269,7 @@ export const Blob: React.FunctionComponent<BlobProps> = props => {
}, [blameVisibility])

// Update blame decorations
useEffect(() => {
useLayoutEffect(() => {
if (editor) {
editor.dispatch({ effects: blameDecorationsCompartment.reconfigure(blameDecorations) })
}
Expand Down
6 changes: 5 additions & 1 deletion client/web/src/repo/blob/codemirror/blame-decorations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,18 @@ export const showGitBlameDecorations = Facet.define<BlameHunk[], BlameHunk[]>({
ViewPlugin.fromClass(
class {
public decorations: DecorationSet
private previousHunkLength = -1

constructor(view: EditorView) {
this.decorations = this.computeDecorations(view, facet)
}

public update(update: ViewUpdate): void {
if (update.docChanged || update.viewportChanged) {
const hunks = update.view.state.facet(facet)

if (update.docChanged || update.viewportChanged || this.previousHunkLength !== hunks.length) {
this.decorations = this.computeDecorations(update.view, facet)
this.previousHunkLength = update.state.facet(facet).length
}
}

Expand Down
3 changes: 3 additions & 0 deletions cmd/frontend/internal/httpapi/httpapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ func NewHandler(
// Return the minimum src-cli version that's compatible with this instance
m.Get(apirouter.SrcCli).Handler(trace.Route(newSrcCliVersionHandler(logger)))

gsClient := gitserver.NewClient(db)
m.Get(apirouter.GitBlameStream).Handler(trace.Route(handleStreamBlame(logger, db, gsClient)))

// Set up the src-cli version cache handler (this will effectively be a
// no-op anywhere other than dot-com).
m.Get(apirouter.SrcCliVersionCache).Handler(trace.Route(releasecache.NewHandler(logger)))
Expand Down
6 changes: 4 additions & 2 deletions cmd/frontend/internal/httpapi/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const (
LSIFUpload = "lsif.upload"
GraphQL = "graphql"

SearchStream = "search.stream"
ComputeStream = "compute.stream"
SearchStream = "search.stream"
ComputeStream = "compute.stream"
GitBlameStream = "git.blame.stream"

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

Expand Down
Loading