From ca2c712fcca43fffd9db3687e22ebacdc9fbbf68 Mon Sep 17 00:00:00 2001 From: netcon Date: Sat, 17 Apr 2021 22:20:01 +0800 Subject: [PATCH] feat: support for the file git blame --- .../assets/icons/dark/close-blame.svg | 1 + .../github1s/assets/icons/dark/open-blame.svg | 1 + .../assets/icons/light/close-blame.svg | 1 + .../assets/icons/light/open-blame.svg | 1 + extensions/github1s/package.json | 67 ++++ extensions/github1s/src/commands/blame.ts | 290 ++++++++++++++++++ extensions/github1s/src/commands/commit.ts | 19 +- extensions/github1s/src/commands/index.ts | 19 ++ extensions/github1s/src/helpers/util.ts | 4 + extensions/github1s/src/helpers/vscode.ts | 9 + .../github1s/src/interfaces/github-api-gql.ts | 36 +++ extensions/github1s/src/listeners/vscode.ts | 10 +- extensions/github1s/src/messages.ts | 13 +- extensions/github1s/src/repository/index.ts | 23 ++ extensions/github1s/src/repository/types.ts | 16 + 15 files changed, 497 insertions(+), 13 deletions(-) create mode 100644 extensions/github1s/assets/icons/dark/close-blame.svg create mode 100644 extensions/github1s/assets/icons/dark/open-blame.svg create mode 100644 extensions/github1s/assets/icons/light/close-blame.svg create mode 100644 extensions/github1s/assets/icons/light/open-blame.svg create mode 100644 extensions/github1s/src/commands/blame.ts create mode 100644 extensions/github1s/src/helpers/vscode.ts diff --git a/extensions/github1s/assets/icons/dark/close-blame.svg b/extensions/github1s/assets/icons/dark/close-blame.svg new file mode 100644 index 000000000..b7dc9c026 --- /dev/null +++ b/extensions/github1s/assets/icons/dark/close-blame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/github1s/assets/icons/dark/open-blame.svg b/extensions/github1s/assets/icons/dark/open-blame.svg new file mode 100644 index 000000000..4f3790514 --- /dev/null +++ b/extensions/github1s/assets/icons/dark/open-blame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/github1s/assets/icons/light/close-blame.svg b/extensions/github1s/assets/icons/light/close-blame.svg new file mode 100644 index 000000000..b7dc9c026 --- /dev/null +++ b/extensions/github1s/assets/icons/light/close-blame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/github1s/assets/icons/light/open-blame.svg b/extensions/github1s/assets/icons/light/open-blame.svg new file mode 100644 index 000000000..00300397b --- /dev/null +++ b/extensions/github1s/assets/icons/light/open-blame.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/github1s/package.json b/extensions/github1s/package.json index 4df26d0f7..a592b1ef8 100644 --- a/extensions/github1s/package.json +++ b/extensions/github1s/package.json @@ -55,6 +55,11 @@ ] }, "commands": [ + { + "command": "github1s.dev-test", + "title": "GitHub1s Dev Test", + "category": "GitHub1s" + }, { "command": "github1s.update-token", "title": "Update GitHub OAuth Token", @@ -167,6 +172,32 @@ "category": "GitHub1s", "icon": "$(arrow-right)", "enablement": "resource =~ /^[^?]*\\?[^#]*(%26|\\b)hasNextRevision(=|%3D|%3d)true/" + }, + { + "command": "github1s.toggle-editor-gutter-blame", + "title": "Toggle File Blame", + "category": "GitHub1s", + "enablement": "!isInDiffEditor && resourceScheme =~ /^github1s$/" + }, + { + "command": "github1s.open-editor-gutter-blame", + "title": "Toggle File Blame", + "category": "GitHub1s", + "icon": { + "dark": "assets/icons/dark/open-blame.svg", + "light": "assets/icons/light/open-blame.svg" + }, + "enablement": "!isInDiffEditor && resourceScheme =~ /^github1s$/" + }, + { + "command": "github1s.close-editor-gutter-blame", + "title": "Toggle File Blame", + "category": "GitHub1s", + "icon": { + "dark": "assets/icons/dark/close-blame.svg", + "light": "assets/icons/light/close-blame.svg" + }, + "enablement": "!isInDiffEditor && resourceScheme =~ /^github1s$/" } ], "colors": [ @@ -214,6 +245,24 @@ "dark": "#1890ff", "highContrast": "#1890ff" } + }, + { + "id": "github1s.colors.gutterBlameBackground", + "description": "gutter blame background color", + "defaults": { + "light": "#00000013", + "dark": "#ffffff13", + "highContrast": "#ffffff13" + } + }, + { + "id": "github1s.colors.highlightGutterBlameBackground", + "description": "highlight gutter blame background color", + "defaults": { + "light": "#00bcf233", + "dark": "#00bce233", + "highContrast": "#00bce233" + } } ], "menus": { @@ -261,6 +310,14 @@ { "command": "github1s.editor-view-open-next-revision", "when": "false" + }, + { + "command": "github1s.open-editor-gutter-blame", + "when": "false" + }, + { + "command": "github1s.close-editor-gutter-blame", + "when": "false" } ], "view/title": [ @@ -323,6 +380,16 @@ "when": "isInDiffEditor", "group": "navigation@3" }, + { + "command": "github1s.open-editor-gutter-blame", + "when": "!isInDiffEditor && resourceScheme =~ /^github1s$/ && !github1s.context.gutterBlameOpening", + "group": "navigation@4" + }, + { + "command": "github1s.close-editor-gutter-blame", + "when": "!isInDiffEditor && resourceScheme =~ /^github1s$/ && github1s.context.gutterBlameOpening", + "group": "navigation@4" + }, { "command": "github1s.editor-view-open-prev-revision", "when": "resourceScheme =~ /^github1s/", diff --git a/extensions/github1s/src/commands/blame.ts b/extensions/github1s/src/commands/blame.ts new file mode 100644 index 000000000..21f38237f --- /dev/null +++ b/extensions/github1s/src/commands/blame.ts @@ -0,0 +1,290 @@ +/** + * @file GitHub1s Blame Related Commands + * @author netcon + */ + +import * as vscode from 'vscode'; +import { relativeTimeTo } from '@/helpers/date'; +import { last } from '@/helpers/util'; +import { hasValidToken } from '@/helpers/context'; +import { setVSCodeContext } from '@/helpers/vscode'; +import router from '@/router'; +import repository from '@/repository'; +import { BlameRange } from '@/repository/types'; +import { showFileBlameAuthorizedRequiredMessage } from '@/messages'; + +const ageColors = [ + '#f66a0a', + '#ef6939', + '#e26862', + '#d3677e', + '#c46696', + '#b365a9', + '#a064bb', + '#8a63cc', + '#7162db', + '#5061e9', + '#0a60f6', +]; + +const createCommitMessagePreviewMarkdown = (blameRange: BlameRange) => { + const commit = blameRange.commit; + const commitAuthor = blameRange.commit.author; + const messageTextLines = []; + + messageTextLines.push( + `![avatar](${commitAuthor.avatarUrl}|width=16px,height=16px) [${commitAuthor.name}](mailto:${commit.author.email}), ${relativeTimeTo(commit.authoredDate)} (*${commit.authoredDate}*)` // prettier-ignore + ); + messageTextLines.push(`Commit ID: ${commit.sha}`); + + messageTextLines.push('---'); + messageTextLines.push(`~~~\n${commit.message}\n~~~`); + messageTextLines.push('---'); + + const switchToCommitCommandText = `command:github1s.switch-to-commit?${encodeURIComponent(JSON.stringify([commit.sha]))}`; // prettier-ignore + const openOnGitHubCommandText = `command:github1s.open-commit-on-github?${encodeURIComponent(JSON.stringify([commit.sha]))}`; // prettier-ignore + messageTextLines.push( + `[$(log-in) Switch to Commit](${switchToCommitCommandText}) | [$(globe) Open on GitHub](${openOnGitHubCommandText})` + ); + + const markdownString = new vscode.MarkdownString( + messageTextLines.join('\n\n'), + true + ); + markdownString.isTrusted = true; + return markdownString; +}; + +const commonLineDecorationTypeOptions = { + before: { + width: '460px', + height: '100%', + contentText: '.', + color: 'rgba(0, 0, 0, 0)', + margin: '0 20px 0 0', + borderStyle: 'none', + textDecoration: `none; + box-sizing: border-box; + padding-right: 110px; + border-right-width: 2px; + border-right-style: solid`, + }, + after: { + width: '460px', + height: '100%', + contentText: '.', + color: 'rgba(0, 0, 0, 0)', + textDecoration: `none; + box-sizing: border-box; + position: absolute; + left: 0; + z-index: -1; + text-align: right; + padding-right: 6px`, + backgroundColor: new vscode.ThemeColor( + 'github1s.colors.gutterBlameBackground' + ), + }, +}; + +// the decoration type for the all lines that belong +// to the commit which user are focusing to +const createSelectedLineDecorationType = () => + vscode.window.createTextEditorDecorationType({ + isWholeLine: true, + overviewRulerColor: new vscode.ThemeColor( + 'github1s.colors.highlightGutterBlameBackground' + ), + backgroundColor: new vscode.ThemeColor( + 'github1s.colors.highlightGutterBlameBackground' + ), + }); + +// the decoration type for the first line of **a blame block** +const createFirstLineDecorationType = (blameRange: BlameRange) => { + // put the avatar of commit author to the beginning of the line + const firstLineBeforeTextDecorationCss = + commonLineDecorationTypeOptions.before.textDecoration + + `; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + text-indent: 2em; + background-image: url(${encodeURI(blameRange.commit.author.avatarUrl)}); + background-size: auto 95%; + background-position: 0 50%; + background-repeat: no-repeat`; + // set the split line with other blame blocks + const firstLineAfterTextDecorationCss = + commonLineDecorationTypeOptions.after.textDecoration + + `; box-shadow: 0 -1px 0 rgba(0, 0, 0, .3)`; + + return vscode.window.createTextEditorDecorationType({ + ...commonLineDecorationTypeOptions, + before: { + ...commonLineDecorationTypeOptions.before, + contentText: blameRange.commit.message, + color: new vscode.ThemeColor('foreground'), + textDecoration: firstLineBeforeTextDecorationCss, + borderColor: ageColors[blameRange.age || 10], + }, + after: { + ...commonLineDecorationTypeOptions.after, + contentText: relativeTimeTo(blameRange.commit.authoredDate), + color: new vscode.ThemeColor('descriptionForeground'), + textDecoration: firstLineAfterTextDecorationCss, + }, + }); +}; + +// the decoration type for the rest lines (not the first line) of a blame block +const createRestLinesDecorationType = (blameRange: BlameRange) => { + return vscode.window.createTextEditorDecorationType({ + ...commonLineDecorationTypeOptions, + before: { + ...commonLineDecorationTypeOptions.before, + borderColor: ageColors[blameRange.age || 10], + }, + }); +}; + +// create decoration option for each line of a blame block +const createLineDecorationOptions = ( + blameRange: BlameRange, + hoverMessage?: vscode.MarkdownString | string +): vscode.DecorationOptions[] => { + const decorationOptions = []; + const { startingLine, endingLine } = blameRange; + + for (let lineIndex = startingLine - 1; lineIndex < endingLine; lineIndex++) { + decorationOptions.push({ + range: new vscode.Range( + new vscode.Position(lineIndex, 0), + new vscode.Position(lineIndex, 0) + ), + hoverMessage, + }); + } + return decorationOptions; +}; + +class EditorGitBlame { + private static instanceMap = new WeakMap(); + private opening: boolean = false; // if the editor blame is showing now + private refreshDisposables: vscode.Disposable[] = []; + private selectionDisposables: vscode.Disposable[] = []; + + private constructor(private editor: vscode.TextEditor) { + vscode.window.onDidChangeTextEditorSelection((event) => { + if (this.opening && event.selections.length) { + this.selection(last(event.selections).active.line); + } + }); + } + + public static getInstance(editor) { + if (!EditorGitBlame.instanceMap.has(editor)) { + EditorGitBlame.instanceMap.set(editor, new EditorGitBlame(editor)); + } + return EditorGitBlame.instanceMap.get(editor); + } + + async getBlameRanges() { + const filePath = this.editor.document?.uri.path; + const fileAuthority = + this.editor.document?.uri.authority || (await router.getAuthority()); + const [_owner, _repo, ref] = fileAuthority.split('+').filter(Boolean); + return filePath ? repository.getFileBlame(filePath, ref) : []; + } + + async open() { + this.refreshDisposables.forEach((disposable) => disposable.dispose()); + setVSCodeContext('github1s.context.gutterBlameOpening', true); + + (await this.getBlameRanges()).forEach((blameRange) => { + const hoverMessage = createCommitMessagePreviewMarkdown(blameRange); + const firstLineDecorationType = createFirstLineDecorationType(blameRange); + const lineDecorationOptions = createLineDecorationOptions( + blameRange, + hoverMessage + ); + + this.refreshDisposables.push(firstLineDecorationType); + this.editor.setDecorations(firstLineDecorationType, [ + lineDecorationOptions[0], + ]); + + if (lineDecorationOptions.length > 1) { + const restLinesDecorationType = createRestLinesDecorationType( + blameRange + ); + + this.refreshDisposables.push(restLinesDecorationType); + this.editor.setDecorations( + restLinesDecorationType, + lineDecorationOptions.slice(1) + ); + } + }); + this.opening = true; + } + + async selection(lineIndex: number) { + this.selectionDisposables.forEach((disposable) => disposable.dispose()); + + const blameRanges = await this.getBlameRanges(); + const selectedCommitSha = blameRanges.find( + (blameRange) => + blameRange.startingLine - 1 <= lineIndex && + blameRange.endingLine - 1 >= lineIndex + )?.commit.sha; + const selectedBlameRanges = blameRanges.filter( + (blameRange) => blameRange.commit.sha === selectedCommitSha + ); + const decorationType = createSelectedLineDecorationType(); + const decorationOptions = []; + + this.selectionDisposables.push(decorationType); + selectedBlameRanges.forEach((blameRange) => { + decorationOptions.push(...createLineDecorationOptions(blameRange)); + }); + this.editor.setDecorations(decorationType, decorationOptions); + } + + close() { + setVSCodeContext('github1s.context.gutterBlameOpening', false); + this.refreshDisposables.forEach((disposable) => disposable.dispose()); + this.selectionDisposables.forEach((disposable) => disposable.dispose()); + this.opening = false; + } + + toggle() { + return this.opening ? this.close() : this.open(); + } +} + +const ensureUserAuthorized = () => { + if (hasValidToken()) { + return true; + } + showFileBlameAuthorizedRequiredMessage(); + return false; +}; + +export const commandToggleEditorGutterBlame = () => { + if (ensureUserAuthorized() && vscode.window.activeTextEditor) { + return EditorGitBlame.getInstance(vscode.window.activeTextEditor).toggle(); + } +}; + +export const commandOpenEditorGutterBlame = () => { + if (ensureUserAuthorized() && vscode.window.activeTextEditor) { + return EditorGitBlame.getInstance(vscode.window.activeTextEditor).open(); + } +}; + +export const commandCloseEditorGutterBlame = () => { + if (ensureUserAuthorized() && vscode.window.activeTextEditor) { + return EditorGitBlame.getInstance(vscode.window.activeTextEditor).close(); + } +}; diff --git a/extensions/github1s/src/commands/commit.ts b/extensions/github1s/src/commands/commit.ts index 81debffcd..8f3941b26 100644 --- a/extensions/github1s/src/commands/commit.ts +++ b/extensions/github1s/src/commands/commit.ts @@ -82,19 +82,18 @@ export const commandCommitViewItemSwitchToCommit = ( return commandSwitchToCommit(viewItem?.commit?.sha); }; +export const commandOpenCommitOnGitHub = async (commitSha: string) => { + const { owner, repo } = await router.getState(); + return vscode.commands.executeCommand( + 'vscode.open', + vscode.Uri.parse(`https://github.com/${owner}/${repo}/commit/${commitSha}`) + ); +}; + // this command is used in `source control commit list view` export const commandCommitViewItemOpenOnGitHub = async ( viewItem: CommitTreeItem ) => { const commitSha = viewItem?.commit?.sha; - - if (commitSha) { - const { owner, repo } = await router.getState(); - return vscode.commands.executeCommand( - 'vscode.open', - vscode.Uri.parse( - `https://github.com/${owner}/${repo}/commit/${commitSha}` - ) - ); - } + commitSha && commandOpenCommitOnGitHub(commitSha); }; diff --git a/extensions/github1s/src/commands/index.ts b/extensions/github1s/src/commands/index.ts index a52c66e59..101a3ef62 100644 --- a/extensions/github1s/src/commands/index.ts +++ b/extensions/github1s/src/commands/index.ts @@ -19,6 +19,7 @@ import { } from './pull'; import { commandSwitchToCommit, + commandOpenCommitOnGitHub, commandCommitViewItemSwitchToCommit, commandCommitViewItemOpenOnGitHub, } from './commit'; @@ -30,6 +31,11 @@ import { commandEditorViewOpenNextRevision, commandEditorViewOpenPrevRevision, } from './editor'; +import { + commandToggleEditorGutterBlame, + commandOpenEditorGutterBlame, + commandCloseEditorGutterBlame, +} from './blame'; const commands: { id: string; callback: (...args: any[]) => any }[] = [ // validate GitHub OAuth Token @@ -55,6 +61,8 @@ const commands: { id: string; callback: (...args: any[]) => any }[] = [ // switch to a commit & input pull number manually { id: 'github1s.switch-to-commit', callback: commandSwitchToCommit }, + // open a commit on GitHub's website + { id: 'github1s.open-commit-on-github', callback: commandOpenCommitOnGitHub }, // update the commit list in the commits view { id: 'github1s.commit-view-refresh-commit-list', callback: () => commitTreeDataProvider.updateTree() }, // prettier-ignore // switch to a commit in the commits view @@ -75,6 +83,13 @@ const commands: { id: string; callback: (...args: any[]) => any }[] = [ { id: 'github1s.editor-view-open-prev-revision', callback: commandEditorViewOpenPrevRevision }, // prettier-ignore // open the next revision of a file { id: 'github1s.editor-view-open-next-revision', callback: commandEditorViewOpenNextRevision }, // prettier-ignore + + // toggle the gutter blame of a editor + { id: 'github1s.toggle-editor-gutter-blame', callback: commandToggleEditorGutterBlame }, // prettier-ignore + // open the gutter blame of a editor + { id: 'github1s.open-editor-gutter-blame', callback: commandOpenEditorGutterBlame }, // prettier-ignore + // close the gutter blame of a editor + { id: 'github1s.close-editor-gutter-blame', callback: commandCloseEditorGutterBlame }, // prettier-ignore ]; export const registerGitHub1sCommands = () => { @@ -85,4 +100,8 @@ export const registerGitHub1sCommands = () => { vscode.commands.registerCommand(command.id, command.callback) ) ); + + vscode.commands.registerCommand('github1s.dev-test', () => { + console.log(vscode.window.activeTextEditor); + }); }; diff --git a/extensions/github1s/src/helpers/util.ts b/extensions/github1s/src/helpers/util.ts index 84bbeeacf..88bd962b6 100644 --- a/extensions/github1s/src/helpers/util.ts +++ b/extensions/github1s/src/helpers/util.ts @@ -51,6 +51,10 @@ export const prop = (obj: object, path: (string | number)[] = []): any => { return cur; }; +export const last = (array: readonly T[]): T => { + return array[array.length - 1]; +}; + export const getNonce = (): string => { let text: string = ''; const possible = diff --git a/extensions/github1s/src/helpers/vscode.ts b/extensions/github1s/src/helpers/vscode.ts new file mode 100644 index 000000000..e89a31cf4 --- /dev/null +++ b/extensions/github1s/src/helpers/vscode.ts @@ -0,0 +1,9 @@ +/** + * @file helper functions about vscode + * @author netcon + */ + +import * as vscode from 'vscode'; + +export const setVSCodeContext = (key, value) => + vscode.commands.executeCommand('setContext', key, value); diff --git a/extensions/github1s/src/interfaces/github-api-gql.ts b/extensions/github1s/src/interfaces/github-api-gql.ts index 51d2456c5..e3e18bee2 100644 --- a/extensions/github1s/src/interfaces/github-api-gql.ts +++ b/extensions/github1s/src/interfaces/github-api-gql.ts @@ -88,3 +88,39 @@ export const githubObjectQuery = gql` } } `; + +/** + * GraphQL to get git blame data of a file + */ +export const githubFileBlameQuery = gql` + query fileBlameQuery( + $owner: String! + $repo: String! + $ref: String! + $path: String! + ) { + repository(owner: $owner, name: $repo) { + object(expression: $ref) { + ... on Commit { + blame(path: $path) { + ranges { + age + startingLine + endingLine + commit { + sha: oid + message + authoredDate + author { + avatarUrl + name + email + } + } + } + } + } + } + } + } +`; diff --git a/extensions/github1s/src/listeners/vscode.ts b/extensions/github1s/src/listeners/vscode.ts index 64acaafbe..1e52f48a3 100644 --- a/extensions/github1s/src/listeners/vscode.ts +++ b/extensions/github1s/src/listeners/vscode.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import router from '@/router'; import { PageType } from '@/router/types'; +import { setVSCodeContext } from '@/helpers/vscode'; import { getChangedFileFromSourceControl } from '@/commands/editor'; import { GitHub1sFileSystemProvider } from '@/providers/fileSystemProvider'; @@ -48,16 +49,21 @@ const handleOpenChangesContextOnActiveEditorChange = async ( ? await getChangedFileFromSourceControl(editor.document.uri) : undefined; - return vscode.commands.executeCommand( - 'setContext', + return setVSCodeContext( 'github1s.context.showOpenChangesInEditorTitle', !!changedFile ); }; +// set the `gutterBlameOpening` to false when the active editor changed +const handleGutterBlameOpeningContextOnActiveEditorChange = async () => { + return setVSCodeContext('github1s.context.gutterBlameOpening', false); +}; + export const registerVSCodeEventListeners = () => { vscode.window.onDidChangeActiveTextEditor(async (editor) => { handleRouterOnActiveEditorChange(editor); handleOpenChangesContextOnActiveEditorChange(editor); + handleGutterBlameOpeningContextOnActiveEditorChange(); }); }; diff --git a/extensions/github1s/src/messages.ts b/extensions/github1s/src/messages.ts index 6863c6e23..15e6f7a10 100644 --- a/extensions/github1s/src/messages.ts +++ b/extensions/github1s/src/messages.ts @@ -13,7 +13,18 @@ export const showSourcegraphSearchMessage = (() => { } alreadyShown = true; vscode.window.showInformationMessage( - 'The code search ability is powered by Sourcegraph (https://sourcegraph.com)' + 'The code search ability is powered by [Sourcegraph](https://sourcegraph.com)' ); }; })(); + +export const showFileBlameAuthorizedRequiredMessage = async () => { + const selectedValue = await vscode.window.showInformationMessage( + 'The file blame feature only works for authorized users due to the limit of [GitHub GraphQL API](https://docs.github.com/en/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql), please provide an OAuth Token to enable it.', + 'Ignore', + 'Set OAuth Token' + ); + if (selectedValue === 'Set OAuth Token') { + vscode.commands.executeCommand('github1s.views.settings.focus'); + } +}; diff --git a/extensions/github1s/src/repository/index.ts b/extensions/github1s/src/repository/index.ts index e632424cf..ed9099136 100644 --- a/extensions/github1s/src/repository/index.ts +++ b/extensions/github1s/src/repository/index.ts @@ -15,6 +15,8 @@ import { getGitHubCommits, getGitHubFileCommits, } from '@/interfaces/github-api-rest'; +import { apolloClient } from '@/interfaces/client'; +import { githubFileBlameQuery } from '@/interfaces/github-api-gql'; import { getFetchOptions } from '@/helpers/fetch'; import { LinkedList, LinkedListDirection } from './linked-list'; import { @@ -22,6 +24,7 @@ import { RepositoryCommit, RepositoryPull, RepositoryRef, + BlameRange, } from './types'; export class Repository { @@ -29,6 +32,7 @@ export class Repository { private _fileCommitIdListMap: Map; private _pullMap: Map; private _commitMap: Map; + private _fileBlameMap: Map; public static getInstance() { if (Repository.instance) { @@ -41,6 +45,7 @@ export class Repository { this._fileCommitIdListMap = new Map(); this._pullMap = new Map(); this._commitMap = new Map(); + this._fileBlameMap = new Map(); } // get current repo owner @@ -242,6 +247,24 @@ export class Repository { ?.getNodeId(commitSha, LinkedListDirection.NEXT); } ); + + public getFileBlame = reuseable( + async (filePath: string, commitSha: string): Promise => { + const cacheKey = `${commitSha}:${filePath}`; + + if (!this._fileBlameMap.has(cacheKey)) { + const [owner, repo] = [this.getOwner(), this.getRepo()]; + const response = await apolloClient.query({ + query: githubFileBlameQuery, + variables: { owner, repo, ref: commitSha, path: filePath.slice(1) }, + }); + const blameRanges = + response.data?.repository?.object?.blame?.ranges || []; + this._fileBlameMap.set(cacheKey, blameRanges); + } + return this._fileBlameMap.get(cacheKey); + } + ); } export default Repository.getInstance(); diff --git a/extensions/github1s/src/repository/types.ts b/extensions/github1s/src/repository/types.ts index 165cd5fef..542c3fe80 100644 --- a/extensions/github1s/src/repository/types.ts +++ b/extensions/github1s/src/repository/types.ts @@ -64,3 +64,19 @@ export interface RepositoryChangedFile { previous_filename?: string; status: FileChangeType; } + +export interface BlameRange { + age: number; + startingLine: number; + endingLine: number; + commit: { + sha: string; + message: string; + authoredDate: string; + author: { + avatarUrl: string; + name: string; + email: string; + }; + }; +}