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;
+ };
+ };
+}