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
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ All notable changes to Sourcegraph are documented in this file.
### Changed

- Code Insights no longer uses a custom index of commits to compress historical backfill and instead queries the repository log directly. This allows the compression algorithm to span any arbitrary time frame, and should improve the reliability of the compression in general. [#45644](https://github.com/sourcegraph/sourcegraph/pull/45644)
- Git blame view got a user-interface overhaul and now shows data in a more structured way with additional visual hints. [#44397](https://github.com/sourcegraph/sourcegraph/issues/44397)

### Fixed

Expand Down
197 changes: 148 additions & 49 deletions client/web/src/repo/blame/useBlameHunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ import { useObservable } from '@sourcegraph/wildcard'

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

import { useBlameVisibility } from './useBlameVisibility'

Expand All @@ -24,6 +29,7 @@ interface BlameHunkDisplayInfo {
timestampString: string
linkURL: string
message: string
commitDate: Date
}

export interface BlameHunk {
Expand All @@ -36,11 +42,14 @@ export interface BlameHunk {
person: {
email: string
displayName: string
avatarURL: string | null
user:
| undefined
| null
| {
username: string
username: string | null
displayName: string | null
avatarURL: string | null
}
}
}
Expand All @@ -64,11 +73,16 @@ const fetchBlameViaGraphQL = memoizeObservable(
revision: string
filePath: string
sourcegraphURL: string
}): Observable<{ current: BlameHunk[] | undefined }> =>
}): Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }> =>
requestGraphQL<GitBlameResult, GitBlameVariables>(
gql`
query GitBlame($repo: String!, $rev: String!, $path: String!) {
repository(name: $repo) {
firstEverCommit {
author {
date
}
}
commit(rev: $rev) {
blob(path: $path) {
blame(startLine: 0, endLine: 0) {
Expand All @@ -78,8 +92,11 @@ const fetchBlameViaGraphQL = memoizeObservable(
person {
email
displayName
avatarURL
user {
username
displayName
avatarURL
}
}
date
Expand All @@ -101,9 +118,18 @@ const fetchBlameViaGraphQL = memoizeObservable(
{ repo: repoName, rev: revision, path: filePath }
).pipe(
map(dataOrThrowErrors),
map(({ repository }) => repository?.commit?.blob?.blame),
map(hunks => (hunks ? hunks.map(blame => addDisplayInfoForHunk(blame, sourcegraphURL)) : undefined)),
map(hunks => ({ current: hunks }))
map(({ repository }) => {
const hunks = repository?.commit?.blob?.blame
const firstCommitDate = repository?.firstEverCommit?.author?.date
if (hunks) {
return {
current: hunks.map(blame => addDisplayInfoForHunk(blame, sourcegraphURL)),
firstCommitDate: firstCommitDate ? new Date(firstCommitDate) : undefined,
}
}

return { current: undefined, firstCommitDate: undefined }
})
),
makeRepoURI
)
Expand All @@ -125,6 +151,19 @@ interface RawStreamHunk {
message: string
}

/**
* Calculating blame hunks on the backend is an expensive operation that gets
* slower the larger the file and the longer the commit history.
*
* To reduce the backend pressure and improve the experience, this fetch
* implementation uses a SSE stream to load the blame hunks in chunks.
*
* It is controlled via the `enable-streaming-git-blame` feature flag and is
* currently not enabled by default.
*
* Since we also need the first commit date for the blame recency calculations,
* this implementation uses Promise.all() to load both data sources in parallel.
*/
const fetchBlameViaStreaming = memoizeObservable(
({
repoName,
Expand All @@ -136,56 +175,103 @@ const fetchBlameViaStreaming = memoizeObservable(
revision: string
filePath: string
sourcegraphURL: string
}): Observable<{ current: BlameHunk[] | undefined }> =>
new Observable<{ current: BlameHunk[] | undefined }>(subscriber => {
}): Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }> =>
new Observable<{ current: BlameHunk[] | undefined; firstCommitDate: Date | undefined }>(subscriber => {
let didEmitFirstCommitDate = false
let firstCommitDate: Date | undefined

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,

Promise.all([
fetchFirstCommitDate(repoName).then(date => {
firstCommitDate = date
}),
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,
avatarURL: null,
user: null,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jhchabran Do we have a list of changes needed before we roll out the git streaming more broadly? Adding support for the user query would be necessary as well.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philipp-spiess catching up with GH as I'm back from PTO. It seems that the new code handles this differently, is this comment still relevant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jhchabran Yeah this is still required. With the GraphQL query it's possible to load the user from a git author but with our streaming endpoint this is not included yet. Do you think it would be a lot of work to add this to the JSON payloads?

},
},
commit: {
url: rawHunk.commit.url,
parents: rawHunk.commit.parents
? rawHunk.commit.parents.map(oid => ({ oid }))
: [],
},
},
commit: {
url: rawHunk.commit.url,
parents: rawHunk.commit.parents ? rawHunk.commit.parents.map(oid => ({ oid })) : [],
},
}
assembledHunks.push(addDisplayInfoForHunk(hunk, sourcegraphURL))
}
if (firstCommitDate !== undefined) {
didEmitFirstCommitDate = true
}
assembledHunks.push(addDisplayInfoForHunk(hunk, sourcegraphURL))
subscriber.next({ current: assembledHunks, firstCommitDate })
}
subscriber.next({ current: assembledHunks })
},
onerror(event) {
// eslint-disable-next-line no-console
console.error(event)
},
}),
]).then(
() => {
// This case can happen when the event source yields before the commit date is resolved
if (!didEmitFirstCommitDate) {
subscriber.next({ current: assembledHunks, firstCommitDate })
}

subscriber.complete()
},
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 })),
.pipe(throttleTime(1000, undefined, { leading: true, trailing: true })),
makeRepoURI
)

async function fetchFirstCommitDate(repoName: string): Promise<Date | undefined> {
return requestGraphQL<FirstCommitDateResult, FirstCommitDateVariables>(
gql`
query FirstCommitDate($repo: String!) {
repository(name: $repo) {
firstEverCommit {
author {
date
}
}
}
}
`,
{ repo: repoName }
)
.pipe(
map(dataOrThrowErrors),
map(({ repository }) => repository?.firstEverCommit?.author?.date),
map(date => (date ? new Date(date) : undefined))
)
.toPromise()
}

/**
* Get display info shared between status bar items and text document decorations.
*/
Expand All @@ -195,14 +281,16 @@ const addDisplayInfoForHunk = (hunk: Omit<BlameHunk, 'displayInfo'>, sourcegraph

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 commitDate = new Date(author.date)
const dateString = formatDateForBlame(commitDate, now)
const timestampString = commitDate.toLocaleString()
const linkURL = new URL(commit.url, sourcegraphURL).href
const content = `${dateString} • ${username}${displayName} [${truncate(message, { length: 45 })}]`
const content = truncate(message, { length: 45 })

;(hunk as BlameHunk).displayInfo = {
displayName,
username,
commitDate,
dateString,
timestampString,
linkURL,
Expand All @@ -229,7 +317,7 @@ export const useBlameHunks = (
enableCodeMirror: boolean
},
sourcegraphURL: string
): { current: BlameHunk[] | undefined } => {
): { current: BlameHunk[] | undefined; firstCommitDate: Date | undefined } => {
const [enableStreamingGitBlame, status] = useFeatureFlag('enable-streaming-git-blame')

const [isBlameVisible] = useBlameVisibility()
Expand All @@ -242,10 +330,21 @@ export const useBlameHunks = (
? enableCodeMirror && enableStreamingGitBlame
? fetchBlameViaStreaming({ revision, repoName, filePath, sourcegraphURL })
: fetchBlameViaGraphQL({ revision, repoName, filePath, sourcegraphURL })
: of({ current: undefined }),
: of({ current: undefined, firstCommitDate: undefined }),
[shouldFetchBlame, enableCodeMirror, enableStreamingGitBlame, revision, repoName, filePath, sourcegraphURL]
)
)

return hunks || { current: undefined }
return hunks || { current: undefined, firstCommitDate: undefined }
}

const ONE_MONTH = 30 * 24 * 60 * 60 * 1000
function formatDateForBlame(commitDate: Date, now: number): string {
if (now - commitDate.getTime() < ONE_MONTH) {
return formatDistanceStrict(commitDate, now, { addSuffix: true })
}
if (commitDate.getFullYear() === new Date(now).getFullYear()) {
return commitDate.toLocaleString('default', { month: 'short', day: 'numeric' })
}
return commitDate.toLocaleString('default', { year: 'numeric', month: 'short', day: 'numeric' })
}
Loading