+
<${VscodeButton} onClick=${bridgeCommands.openDetailPage}>Detail/>
<${VscodeButton} loading=${validating} onClick=${handleValidateClick}>Validate/>
<${VscodeButton} onClick=${onEditClick}>Edit/>
<${VscodeButton} onClick=${handleClearClick}>Clear/>
@@ -142,31 +104,94 @@ const TokenDetailPage = ({ token, onEditClick, ...props }) => {
};
const PageFooter = () => {
- const [sgApiFirst, setSgApiFirst] = useState(false);
+ const [preferSgApi, setPreferSgApi] = useState(false);
- const updateSgApiFirst = useCallback(() => {
- postMessage('get-use-sourcegraph-api').then((value) => {
- setSgApiFirst(value);
+ const updatePreferSgApi = useCallback(() => {
+ bridgeCommands.getPreferSgApi().then((value) => {
+ setPreferSgApi(value);
});
}, []);
const handleCheckboxChange = useCallback(
(event) => {
- postMessage('set-use-sourcegraph-api', event.target.checked).then(() => {
- updateSgApiFirst();
+ bridgeCommands.setPreferSgApi(event.target.checked).then(() => {
+ updatePreferSgApi();
});
},
- [updateSgApiFirst]
+ [updatePreferSgApi]
);
useEffect(() => {
- updateSgApiFirst();
- }, [updateSgApiFirst]);
+ const handler = ({ data }) => {
+ if (data.type === 'prefer-sourcegraph-api-changed') {
+ updatePreferSgApi();
+ }
+ };
+ window.addEventListener('message', handler);
+ return () => window.removeEventListener('message', handler);
+ }, []);
+
+ useEffect(() => {
+ updatePreferSgApi();
+ }, [updatePreferSgApi]);
return html`
+ `;
+};
+
+const TokenEditPage = ({ token, onCancel, ...props }) => {
+ const pageDescription = (pageConfig.pageDescriptionLines || []).map((line) => html`
${line}
`);
+
+ return html`
+
+ <${PageHeader} title="Set AccessToken">${pageDescription}/>
+ <${OAuthBlock} buttonText=${pageConfig.OAuthButtonText} command=${pageConfig.OAuthCommand} />
+ <${InputTokenBlock} createLink=${pageConfig.createTokenLink} isEditing=${!!token} onCancel=${onCancel} />
+
+ `;
+};
+
+const TokenDetailPage = ({ token, onEditClick, ...props }) => {
+ const [tokenStatus, setTokenStatus] = useState(null);
+ const [validating, setValidating] = useState(true);
+
+ const validateToken = useCallback((token) => {
+ setValidating(true);
+ return bridgeCommands.validateToken(token).then((tokenStatus) => {
+ setValidating(false);
+ setTokenStatus(tokenStatus);
+ return tokenStatus;
+ });
+ }, []);
+
+ const validateResult = tokenStatus
+ ? html`
`
+ : html`
Current AccessToken is INVALID.
`;
+
+ useEffect(() => {
+ token && validateToken(token);
+ }, [token, validateToken]);
+
+ return html`
+
+ <${PageHeader} title="You have authenticated">
+ ${validating ? html`<${VscodeLoading} dots=${8} align="left" style="height: 14px" />` : validateResult}
+ />
+ <${TokenDetailBlock}
+ token=${token}
+ validating=${validating}
+ onEditClick=${onEditClick}
+ validateToken=${validateToken}
+ />
`;
};
@@ -180,7 +205,7 @@ const App = () => {
const switchToDetail = useCallback(() => setPageType('DETAIL'), []);
useEffect(() => {
- postMessage('get-token').then((token) => {
+ bridgeCommands.getToken().then((token) => {
setToken(token);
setLoading(false);
setPageType(token ? 'DETAIL' : 'EDIT');
@@ -202,14 +227,11 @@ const App = () => {
return html`<${VscodeLoading} />`;
}
- const tokenPage =
- pageType === 'DETAIL'
- ? html`<${TokenDetailPage} token=${token} onEditClick=${switchToEdit} />`
- : html`<${TokenEditPage} token=${token} onCancel=${switchToDetail} />`;
-
return html`
- ${tokenPage}
+ ${pageType === 'DETAIL'
+ ? html`<${TokenDetailPage} token=${token} onEditClick=${switchToEdit} />`
+ : html`<${TokenEditPage} token=${token} onCancel=${switchToDetail} />`}
<${PageFooter} />
`;
diff --git a/extensions/github1s/assets/pages/htm.module.js b/extensions/github1s/assets/pages/libraries/htm.module.js
similarity index 100%
rename from extensions/github1s/assets/pages/htm.module.js
rename to extensions/github1s/assets/pages/libraries/htm.module.js
diff --git a/extensions/github1s/assets/pages/preact-hooks.module.js b/extensions/github1s/assets/pages/libraries/preact-hooks.module.js
similarity index 100%
rename from extensions/github1s/assets/pages/preact-hooks.module.js
rename to extensions/github1s/assets/pages/libraries/preact-hooks.module.js
diff --git a/extensions/github1s/assets/pages/preact.module.js b/extensions/github1s/assets/pages/libraries/preact.module.js
similarity index 100%
rename from extensions/github1s/assets/pages/preact.module.js
rename to extensions/github1s/assets/pages/libraries/preact.module.js
diff --git a/extensions/github1s/assets/github1s.svg b/extensions/github1s/assets/settings.svg
similarity index 100%
rename from extensions/github1s/assets/github1s.svg
rename to extensions/github1s/assets/settings.svg
diff --git a/extensions/github1s/package.json b/extensions/github1s/package.json
index 0d2ee1391..590642323 100644
--- a/extensions/github1s/package.json
+++ b/extensions/github1s/package.json
@@ -29,14 +29,14 @@
"viewsContainers": {
"activitybar": [
{
- "id": "github1s",
- "title": "GitHub1s",
- "icon": "assets/github1s.svg"
+ "id": "settings",
+ "title": "Settings",
+ "icon": "assets/settings.svg"
}
]
},
"views": {
- "github1s": [
+ "settings": [
{
"id": "github1s.views.settings",
"name": "Settings",
@@ -298,6 +298,12 @@
"category": "GitHub1s",
"icon": "$(globe)",
"enablement": "github1s:adapters:default:platformName != 'GitHub' && github1s:adapters:default:platformName != 'GitLab' && github1s:adapters:default:platformName != 'Bitbucket' && github1s:adapters:default:platformName != 'npm'"
+ },
+ {
+ "command": "github1s.commands.syncSourcegraphRepository",
+ "title": "Sync Sourcegraph Repository",
+ "category": "GitHub1s",
+ "enablement": "resourceScheme =~ /^(github1s|gitlab1s|bitbucket1s)/"
}
],
"colors": [
@@ -562,22 +568,22 @@
{
"command": "github1s.commands.openFilePreviousRevision",
"when": "resourceScheme =~ /^(github1s|gitlab1s|bitbucket1s)/",
- "group": "navigation@98"
+ "group": "navigation@4"
},
{
"command": "github1s.commands.openFileNextRevision",
"when": "resourceScheme =~ /^(github1s|gitlab1s|bitbucket1s)/",
- "group": "navigation@99"
+ "group": "navigation@5"
},
{
"command": "github1s.commands.openEditorGutterBlame",
"when": "!isInDiffEditor && github1s:features:gutterBlame:enabled && !github1s:features:gutterBlame:open",
- "group": "navigation@4"
+ "group": "navigation@6"
},
{
"command": "github1s.commands.closeEditorGutterBlame",
"when": "!isInDiffEditor && github1s:features:gutterBlame:enabled && github1s:features:gutterBlame:open",
- "group": "navigation@4"
+ "group": "navigation@6"
}
]
}
diff --git a/extensions/github1s/src/adapters/github1s/authentication.ts b/extensions/github1s/src/adapters/github1s/authentication.ts
index b4e9854c6..41a914e2b 100644
--- a/extensions/github1s/src/adapters/github1s/authentication.ts
+++ b/extensions/github1s/src/adapters/github1s/authentication.ts
@@ -3,36 +3,61 @@
* @author netcon
*/
+import * as vscode from 'vscode';
import { Barrier } from '@/helpers/async';
import { getExtensionContext } from '@/helpers/context';
-import * as vscode from 'vscode';
-import { GitHubTokenManager } from './token';
import { createPageHtml, getWebviewOptions } from '@/helpers/page';
+import { GitHubTokenManager } from './token';
import { messageApiMap } from './settings';
export class GitHub1sAuthenticationView {
- private static instance: GitHub1sAuthenticationView | null = null;
+ protected static instance: GitHub1sAuthenticationView | null = null;
public static viewType = 'github1s.views.github1s-authentication';
+ protected tokenManager = GitHubTokenManager.getInstance();
private webviewPanel: vscode.WebviewPanel | null = null;
// using for waiting token
private tokenBarrier: Barrier | null = null;
// using for displaying open page reason
private notice: string = '';
- private constructor() {}
+ protected pageTitle = 'Authenticating to GitHub';
+ protected OAuthCommand = 'github1s.commands.vscode.connectToGitHub';
+ protected pageConfig: Record
= {
+ authenticationFormTitle: 'Authenticating to GitHub',
+ OAuthButtonText: 'Connect to GitHub',
+ OAuthButtonLogo: 'assets/pages/assets/github.svg',
+ createTokenLink: `${GITHUB_ORIGIN}/settings/tokens/new?scopes=repo&description=GitHub1s`,
+ rateLimitDocLink: 'https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting',
+ rateLimitDocLinkText: 'GitHub Rate limiting Documentation',
+ authenticationFeatures: [
+ {
+ text: 'Access GitHub personal repository',
+ link: 'https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-github-user-account/managing-access-to-your-personal-repositories',
+ },
+ {
+ text: 'Higher rate limit for GitHub official API',
+ link: 'https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting',
+ },
+ {
+ text: 'Support for GitHub GraphQL API',
+ link: 'https://docs.github.com/en/graphql/guides/forming-calls-with-graphql#authenticating-with-graphql',
+ },
+ ],
+ };
+
+ protected constructor() {}
public static getInstance(): GitHub1sAuthenticationView {
if (GitHub1sAuthenticationView.instance) {
return GitHub1sAuthenticationView.instance;
}
- return (GitHub1sAuthenticationView.instance = new GitHub1sAuthenticationView());
+ return (GitHub1sAuthenticationView.instance = new this());
}
private registerListeners() {
if (!this.webviewPanel) {
- throw new Error('webview is not inited yet');
+ throw new Error('webview is not init yet');
}
- const tokenManager = GitHubTokenManager.getInstance();
this.webviewPanel.webview.onDidReceiveMessage((message) => {
const commonResponse = { id: message.id, type: message.type };
@@ -43,21 +68,21 @@ export class GitHub1sAuthenticationView {
postMessage(this.notice);
break;
case 'get-token':
- postMessage(tokenManager.getToken());
+ postMessage(this.tokenManager.getToken());
break;
case 'set-token':
message.data && (this.notice = '');
- tokenManager.setToken(message.data || '').then(() => postMessage());
+ this.tokenManager.setToken(message.data || '').then(() => postMessage());
break;
case 'validate-token':
- tokenManager.validateToken(message.data).then((tokenStatus) => postMessage(tokenStatus));
+ this.tokenManager.validateToken(message.data).then((tokenStatus) => postMessage(tokenStatus));
break;
- case 'connect-to-github':
- vscode.commands.executeCommand('github1s.commands.vscode.connectToGitHub').then((data: any) => {
+ case 'oauth-authorizing':
+ vscode.commands.executeCommand(this.OAuthCommand).then((data: any) => {
if (data && data.error_description) {
vscode.window.showErrorMessage(data.error_description);
} else if (data && data.access_token) {
- tokenManager.setToken(data.access_token || '').then(() => postMessage());
+ this.tokenManager.setToken(data.access_token || '').then(() => postMessage());
}
postMessage();
});
@@ -69,23 +94,23 @@ export class GitHub1sAuthenticationView {
}
});
- tokenManager.onDidChangeToken((token) => {
+ this.tokenManager.onDidChangeToken((token) => {
this.tokenBarrier && this.tokenBarrier.open();
this.tokenBarrier && (this.tokenBarrier = null);
this.webviewPanel?.webview.postMessage({ type: 'token-changed', token });
});
}
- public open(notice: string = '', withBarriar = false) {
+ public open(notice: string = '', withBarrier = false) {
const extensionContext = getExtensionContext();
this.notice = notice;
- withBarriar && !this.tokenBarrier && (this.tokenBarrier = new Barrier(600 * 1000));
+ withBarrier && !this.tokenBarrier && (this.tokenBarrier = new Barrier(600 * 1000));
if (!this.webviewPanel) {
this.webviewPanel = vscode.window.createWebviewPanel(
GitHub1sAuthenticationView.viewType,
- 'Authenticating to GitHub',
+ this.pageTitle,
vscode.ViewColumn.One,
getWebviewOptions(extensionContext.extensionUri)
);
@@ -97,12 +122,15 @@ export class GitHub1sAuthenticationView {
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/components.css').toString(),
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/github1s-authentication.css').toString(),
];
+ const globalPageConfig = { ...this.pageConfig, extensionUri: extensionContext.extensionUri.toString() };
const scripts = [
+ 'data:text/javascript;base64,' +
+ Buffer.from(`window.pageConfig=${JSON.stringify(globalPageConfig)};`).toString('base64'),
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/github1s-authentication.js').toString(),
];
const webview = this.webviewPanel.webview;
- webview.html = createPageHtml('Authenticating To GitHub', styles, scripts);
- return withBarriar ? this.tokenBarrier!.wait() : Promise.resolve();
+ webview.html = createPageHtml(this.pageTitle, styles, scripts);
+ return withBarrier ? this.tokenBarrier!.wait() : Promise.resolve();
}
}
diff --git a/extensions/github1s/src/adapters/github1s/data-source.ts b/extensions/github1s/src/adapters/github1s/data-source.ts
index 1f9e46ef9..0f19ac7d6 100644
--- a/extensions/github1s/src/adapters/github1s/data-source.ts
+++ b/extensions/github1s/src/adapters/github1s/data-source.ts
@@ -32,6 +32,7 @@ import { matchSorter } from 'match-sorter';
import { FILE_BLAME_QUERY } from './graphql';
import { GitHubFetcher } from './fetcher';
import { SourcegraphDataSource } from '../sourcegraph/data-source';
+import { decorate, memorize } from '@/helpers/func';
const parseRepoFullName = (repoFullName: string) => {
const [owner, repo] = repoFullName.split('/');
@@ -68,7 +69,7 @@ const trySourcegraphApiFirst = (_target: any, propertyKey: string, descriptor: P
descriptor.value = async function Promise>(...args: Parameters) {
const githubFetcher = GitHubFetcher.getInstance();
- if (await githubFetcher.useSourcegraphApiFirst(args[0])) {
+ if (await githubFetcher.getPreferSourcegraphApi(args[0])) {
try {
return await sourcegraphDataSource[propertyKey](...args);
} catch (e) {}
@@ -91,6 +92,15 @@ export class GitHub1sDataSource extends DataSource {
return (GitHub1sDataSource.instance = new GitHub1sDataSource());
}
+ @trySourcegraphApiFirst
+ async provideRepository(repoFullName: string): Promise<{ private: boolean; defaultBranch: string } | null> {
+ const fetcher = GitHubFetcher.getInstance();
+ const { owner, repo } = parseRepoFullName(repoFullName);
+ const requestParams = { owner, repo };
+ const response = await fetcher.request('GET /repos/{owner}/{repo}', requestParams).catch(() => null);
+ return response?.data ? { private: response.data.private, defaultBranch: response.data.default_branch } : null;
+ }
+
@trySourcegraphApiFirst
async provideDirectory(repoFullName: string, ref: string, path: string, recursive = false): Promise {
const fetcher = GitHubFetcher.getInstance();
@@ -133,6 +143,11 @@ export class GitHub1sDataSource extends DataSource {
}));
}
+ @decorate(memorize)
+ async getDefaultBranch(repoFullName: string) {
+ return (await this.provideRepository(repoFullName))?.defaultBranch || 'HEAD';
+ }
+
@trySourcegraphApiFirst
async extractRefPath(repoFullName: string, refAndPath: string): Promise<{ ref: string; path: string }> {
if (!this.matchedRefsMap.has(repoFullName)) {
@@ -146,7 +161,10 @@ export class GitHub1sDataSource extends DataSource {
const mapKey = `${repoFullName} ${refAndPath}`;
if (!this.refPathPromiseMap.has(mapKey)) {
const refPathPromise = new Promise<{ ref: string; path: string }>(async (resolve, reject) => {
- if (!refAndPath || refAndPath.match(/^HEAD(\/.*)?$/i)) {
+ if (!refAndPath) {
+ return resolve({ ref: await this.getDefaultBranch(repoFullName), path: '' });
+ }
+ if (refAndPath.match(/^HEAD(\/.*)?$/i)) {
return resolve({ ref: 'HEAD', path: refAndPath.slice(5) });
}
@@ -307,13 +325,15 @@ export class GitHub1sDataSource extends DataSource {
createTime: new Date(item.created_at),
mergeTime: item.merged_at ? new Date(item.merged_at) : null,
closeTime: item.closed_at ? new Date(item.closed_at) : null,
- head: { label: item.head.label, commitSha: item.head.sha },
- base: { label: item.base.label, commitSha: item.base.sha },
+ source: item.head.label,
+ target: item.base.label,
+ sourceSha: item.head.sha,
+ targetSha: item.base.sha,
avatarUrl: item.user?.avatar_url,
}));
}
- async provideCodeReview(repoFullName: string, id: string): Promise {
+ async provideCodeReview(repoFullName: string, id: string) {
const fetcher = GitHubFetcher.getInstance();
const { owner, repo } = parseRepoFullName(repoFullName);
const pullRequestParams = { owner, repo, pull_number: Number(id) };
@@ -327,8 +347,10 @@ export class GitHub1sDataSource extends DataSource {
createTime: new Date(data.created_at),
mergeTime: data.merged_at ? new Date(data.merged_at) : null,
closeTime: data.closed_at ? new Date(data.closed_at) : null,
- head: { label: data.head.label, commitSha: data.head.sha },
- base: { label: data.base.label, commitSha: data.base.sha },
+ source: data.head.label,
+ target: data.base.label,
+ sourceSha: data.head.sha,
+ targetSha: data.base.sha,
avatarUrl: data.user?.avatar_url,
};
}
@@ -408,6 +430,6 @@ export class GitHub1sDataSource extends DataSource {
}
provideUserAvatarLink(user: string): string {
- return `https://github.com/${user}.png`;
+ return `${GITHUB_ORIGIN}/${user}.png`;
}
}
diff --git a/extensions/github1s/src/adapters/github1s/fetcher.ts b/extensions/github1s/src/adapters/github1s/fetcher.ts
index 2d847196d..d5ee6b472 100644
--- a/extensions/github1s/src/adapters/github1s/fetcher.ts
+++ b/extensions/github1s/src/adapters/github1s/fetcher.ts
@@ -10,6 +10,7 @@ import { Octokit } from '@octokit/core';
import { GitHub1sAuthenticationView } from './authentication';
import { GitHubTokenManager } from './token';
import { isNil } from '@/helpers/util';
+import { getCurrentRepo } from './parse-path';
import { SourcegraphDataSource } from '../sourcegraph/data-source';
export const errorMessages = {
@@ -21,7 +22,7 @@ export const errorMessages = {
anonymous: 'Bad credentials, please authenticate to github and retry',
authenticated: 'This token is invalid, please try another one',
},
- notFound: {
+ repoNotFound: {
anonymous: 'Repository not found, if it is private, you can provide an AccessToken to access it',
authenticated: 'Repository not found, if it is private, you can try change an AccessToken to access it',
},
@@ -31,15 +32,15 @@ export const errorMessages = {
},
};
-const detectErrorMessage = (response: any, authenticated: boolean, accessRepository: boolean) => {
+const detectErrorMessage = (response: any, authenticated: boolean) => {
if (response?.status === 403 && +response?.headers?.['x-ratelimit-remaining'] === 0) {
return errorMessages.rateLimited[authenticated ? 'authenticated' : 'anonymous'];
}
- if (response?.status === 401 && +response?.data?.message?.includes?.('Bad credentials')) {
+ if (response?.status === 401 && response?.data?.message?.includes?.('Bad credentials')) {
return errorMessages.badCredentials[authenticated ? 'authenticated' : 'anonymous'];
}
- if (response?.status === 404 && !accessRepository) {
- return errorMessages.notFound[authenticated ? 'authenticated' : 'anonymous'];
+ if (response?.status === 404) {
+ return errorMessages.repoNotFound[authenticated ? 'authenticated' : 'anonymous'];
}
if (response?.status === 403) {
return errorMessages.noPermission[authenticated ? 'authenticated' : 'anonymous'];
@@ -47,15 +48,14 @@ const detectErrorMessage = (response: any, authenticated: boolean, accessReposit
return response?.data?.message || '';
};
-const USE_SOURCEGRAPH_API_FIRST = 'USE_SOURCEGRAPH_API_FIRST';
+const PREFER_SOURCEGRAPH_API = 'PREFER_SOURCEGRAPH_API';
export class GitHubFetcher {
private static instance: GitHubFetcher | null = null;
private _emitter = new vscode.EventEmitter();
- private _ownerAndRepoPromise: Promise<[string, string]> | null = null;
- private _repositoryPromise: Promise<{ private: boolean } | null> | null = null;
- private _originalRequest: Octokit['request'] | null = null;
- public onDidChangeUseSourcegraphApiFirst = this._emitter.event;
+ private _request: Octokit['request'] | null = null;
+ public onDidChangePreferSourcegraphApi = this._emitter.event;
+ private _currentRepoPromise: Promise | null = null;
public request: Octokit['request'];
public graphql: Octokit['graphql'];
@@ -64,94 +64,78 @@ export class GitHubFetcher {
if (GitHubFetcher.instance) {
return GitHubFetcher.instance;
}
- return (GitHubFetcher.instance = new GitHubFetcher());
+ return (GitHubFetcher.instance = new this());
}
private constructor() {
this.initFetcherMethods();
- // turn off `useSourcegraphApiFirst` if current repository is private
- this.useSourcegraphApiFirst().then((useSourcegraphApiFirst) =>
- this.resolveCurrentRepository(useSourcegraphApiFirst).then(
- (repository) => repository?.private && !this.setUseSourcegraphApiFirst(false)
- )
- );
+ this.initPreferSourcegraphApi();
GitHubTokenManager.getInstance().onDidChangeToken(() => this.initFetcherMethods());
+ GitHubTokenManager.getInstance().onDidChangeToken(() => this.initPreferSourcegraphApi());
}
// initial fetcher methods in this way for correct `request/graphql` type inference
initFetcherMethods() {
const accessToken = GitHubTokenManager.getInstance().getToken();
- const octokit = new Octokit({ auth: accessToken, request: { fetch } });
+ const octokit = new Octokit({ auth: accessToken, request: { fetch }, baseUrl: GITHUB_API_PREFIX });
- this._originalRequest = octokit.request;
+ this._request = octokit.request;
this.request = Object.assign((...args: Parameters) => {
return octokit.request(...args).catch(async (error) => {
- const errorStatus = (error as any)?.response?.status;
- if ([401, 403, 404].includes(errorStatus)) {
+ const errorStatus = error?.response?.status as number | undefined;
+ const repoNotFound = errorStatus === 404 && !(await this.resolveCurrentRepo());
+ if ((errorStatus && [401, 403].includes(errorStatus)) || repoNotFound) {
// maybe we have to acquire github access token to continue
- const repository = await this.resolveCurrentRepository(false);
- const message = detectErrorMessage(error?.response, !!accessToken, !!repository);
+ const message = detectErrorMessage(error?.response, !!accessToken);
await GitHub1sAuthenticationView.getInstance().open(message, true);
- return octokit.request(...args);
+ return this._request!(...args);
}
});
- }, octokit.request);
+ }, this._request);
this.graphql = Object.assign(async (...args: Parameters) => {
- // graphql API only worked for authenticated users
if (!GitHubTokenManager.getInstance().getToken()) {
- const message = 'GraphQL API only worked for authenticated users';
+ const message = 'GraphQL API only works for authenticated users';
await GitHub1sAuthenticationView.getInstance().open(message, true);
}
return octokit.graphql(...args);
}, octokit.graphql);
}
- private getCurrentOwnerAndRepo() {
- if (this._ownerAndRepoPromise) {
- return this._ownerAndRepoPromise;
+ private resolveCurrentRepo(forceUpdate: boolean = false) {
+ if (this._currentRepoPromise && !forceUpdate) {
+ return this._currentRepoPromise;
}
- return (this._ownerAndRepoPromise = new Promise((resolve) => {
- return vscode.commands.executeCommand('github1s.commands.vscode.getBrowserUrl').then(
- (browserUrl: string) => {
- const pathParts = vscode.Uri.parse(browserUrl).path.split('/').filter(Boolean);
- resolve(pathParts.length >= 2 ? (pathParts.slice(0, 2) as [string, string]) : ['conwnet', 'github1s']);
- },
- () => resolve(['conwnet', 'github1s'])
- );
- }));
+ const requestPattern = '/repos/{owner}/{repo}' as const;
+ const getOwnerRepo = () => getCurrentRepo().then((repo) => repo.split('/'));
+ return (this._currentRepoPromise = Promise.resolve(getOwnerRepo())
+ .then(([owner, repo]) => this._request?.(requestPattern, { owner, repo }).then((res) => res.data))
+ .catch(() => null));
}
- private resolveCurrentRepository(useSourcegraphApiFirst: boolean) {
- if (this._repositoryPromise) {
- return this._repositoryPromise;
- }
- return (this._repositoryPromise = new Promise(async (resolve) => {
- const [owner = 'conwnet', repo = 'github1s'] = await this.getCurrentOwnerAndRepo();
- const dataSource = SourcegraphDataSource.getInstance('github');
- if (useSourcegraphApiFirst && !!(await dataSource.provideRepository(`${owner}/${repo}`))) {
- return resolve({ private: false });
+ private async initPreferSourcegraphApi() {
+ if (await this.getPreferSourcegraphApi()) {
+ const sgDataSource = SourcegraphDataSource.getInstance('github');
+ if (!(await sgDataSource.provideRepository(await getCurrentRepo()))) {
+ this.resolveCurrentRepo(true).then((repo) => {
+ repo?.private && this.setPreferSourcegraphApi(false);
+ });
}
- return this._originalRequest?.('GET /repos/{owner}/{repo}', { owner, repo }).then(
- (response) => resolve(response?.data || null),
- () => resolve(null)
- );
- }));
+ }
}
- public async useSourcegraphApiFirst(repoFullName?: string): Promise {
- const targetRepo = repoFullName || (await this.getCurrentOwnerAndRepo()).join('/');
+ public async getPreferSourcegraphApi(repo?: string): Promise {
+ const targetRepo = repo || (await getCurrentRepo());
const globalState = getExtensionContext().globalState;
- const cachedData: Record | undefined = globalState.get(USE_SOURCEGRAPH_API_FIRST);
+ const cachedData: Record | undefined = globalState.get(PREFER_SOURCEGRAPH_API);
return !isNil(cachedData?.[targetRepo]) ? !!cachedData?.[targetRepo] : true;
}
- public async setUseSourcegraphApiFirst(repoOrValue: string | boolean, value?: boolean) {
- const targetRepo = !isNil(value) ? (repoOrValue as string) : (await this.getCurrentOwnerAndRepo()).join('/');
- const targetValue = !isNil(value) ? value : !!repoOrValue;
+ public async setPreferSourcegraphApi(value: boolean, repo?: string) {
+ const targetRepo = repo || (await getCurrentRepo());
const globalState = getExtensionContext().globalState;
- const cachedData: Record | undefined = globalState.get(USE_SOURCEGRAPH_API_FIRST);
- await globalState.update(USE_SOURCEGRAPH_API_FIRST, { ...cachedData, [targetRepo]: targetValue });
+ const cachedData: Record | undefined = globalState.get(PREFER_SOURCEGRAPH_API);
+ await globalState.update(PREFER_SOURCEGRAPH_API, { ...cachedData, [targetRepo]: value });
this._emitter.fire(value);
}
}
diff --git a/extensions/github1s/src/adapters/github1s/index.ts b/extensions/github1s/src/adapters/github1s/index.ts
index 0a7979d16..760ce845c 100644
--- a/extensions/github1s/src/adapters/github1s/index.ts
+++ b/extensions/github1s/src/adapters/github1s/index.ts
@@ -10,6 +10,8 @@ import { GitHub1sRouterParser } from './router-parser';
import { GitHub1sSettingsViewProvider } from './settings';
import { GitHub1sAuthenticationView } from './authentication';
import { Adapter, CodeReviewType, PlatformName } from '../types';
+import { SourcegraphDataSource } from '../sourcegraph/data-source';
+import { getCurrentRepo } from './parse-path';
export class GitHub1sAdapter implements Adapter {
public scheme: string = 'github1s';
@@ -39,6 +41,11 @@ export class GitHub1sAdapter implements Adapter {
vscode.commands.registerCommand('github1s.commands.openGitHub1sAuthPage', () => {
return GitHub1sAuthenticationView.getInstance().open();
});
+ vscode.commands.registerCommand('github1s.commands.syncSourcegraphRepository', async () => {
+ const dataSource = SourcegraphDataSource.getInstance('github');
+ const randomRef = (Math.random() + 1).toString(36).slice(2);
+ return dataSource.provideCommit(await getCurrentRepo(), randomRef);
+ });
}
deactivateAsDefault() {
diff --git a/extensions/github1s/src/adapters/github1s/parse-path.ts b/extensions/github1s/src/adapters/github1s/parse-path.ts
index 0a7ad4d32..b16ab61a6 100644
--- a/extensions/github1s/src/adapters/github1s/parse-path.ts
+++ b/extensions/github1s/src/adapters/github1s/parse-path.ts
@@ -3,10 +3,27 @@
* @author netcon
*/
+import * as vscode from 'vscode';
import { parsePath } from 'history';
import { PageType, RouterState } from '@/adapters/types';
import { GitHub1sDataSource } from './data-source';
import * as queryString from 'query-string';
+import { memorize } from '@/helpers/func';
+import { getBrowserUrl } from '@/helpers/context';
+
+export const DEFAULT_REPO = 'conwnet/github1s';
+
+export const getCurrentRepo = memorize(() => {
+ return getBrowserUrl().then((browserUrl: string) => {
+ const pathParts = vscode.Uri.parse(browserUrl).path.split('/').filter(Boolean);
+ return pathParts.length >= 2 ? (pathParts.slice(0, 2) as [string, string]).join('/') : DEFAULT_REPO;
+ });
+});
+
+export const getDefaultBranch = async (repo: string): Promise => {
+ const dataSource = GitHub1sDataSource.getInstance();
+ return dataSource.getDefaultBranch(repo);
+};
const parseTreeUrl = async (path: string): Promise => {
const pathParts = parsePath(path).pathname!.split('/').filter(Boolean);
@@ -27,7 +44,7 @@ const parseBlobUrl = async (path: string): Promise => {
}
// get selected line number range from path which looks like:
- // `/conwnet/github1s/blob/HEAD/package.json#L10-L20`
+ // `/conwnet/github1s/blob/master/package.json#L10-L20`
const matches = routerHash.match(/^#L(\d+)(?:-L(\d+))?/);
const [_, startLineNumber = '0', endLineNumber] = matches ? matches : [];
@@ -46,7 +63,7 @@ const parseCommitsUrl = async (path: string): Promise => {
return {
repo: `${owner}/${repo}`,
pageType: PageType.CommitList,
- ref: refParts.length ? refParts.join('/') : 'HEAD',
+ ref: refParts.length ? refParts.join('/') : await getDefaultBranch(`${owner}/${repo}`),
};
};
@@ -63,7 +80,7 @@ const parsePullsUrl = async (path: string): Promise => {
return {
repo: `${owner}/${repo}`,
- ref: 'HEAD',
+ ref: await getDefaultBranch(`${owner}/${repo}`),
pageType: PageType.CodeReviewList,
};
};
@@ -77,7 +94,7 @@ const parsePullUrl = async (path: string): Promise => {
return {
repo: `${owner}/${repo}`,
pageType: PageType.CodeReview,
- ref: codeReview.head.commitSha,
+ ref: codeReview.targetSha,
codeReviewId,
};
};
@@ -97,7 +114,7 @@ const parseSearchUrl = async (path: string): Promise => {
return {
repo: `${owner}/${repo}`,
pageType: PageType.Search,
- ref: 'HEAD',
+ ref: await getDefaultBranch(`${owner}/${repo}`),
query,
isRegex,
isCaseSensitive,
@@ -144,8 +161,8 @@ export const parseGitHubPath = async (path: string): Promise => {
// fallback to default
return {
- repo: 'conwnet/github1s',
- ref: 'HEAD',
+ repo: DEFAULT_REPO,
+ ref: await getDefaultBranch(DEFAULT_REPO),
pageType: PageType.Tree,
filePath: '',
};
diff --git a/extensions/github1s/src/adapters/github1s/router-parser.ts b/extensions/github1s/src/adapters/github1s/router-parser.ts
index 15d911e81..99345bb33 100644
--- a/extensions/github1s/src/adapters/github1s/router-parser.ts
+++ b/extensions/github1s/src/adapters/github1s/router-parser.ts
@@ -3,6 +3,7 @@
* @author netcon
*/
+import { joinPath } from '@/helpers/util';
import * as adapterTypes from '../types';
import { parseGitHubPath } from './parse-path';
@@ -46,6 +47,6 @@ export class GitHub1sRouterParser extends adapterTypes.RouterParser {
}
buildExternalLink(path: string): string {
- return 'https://github.com' + (path.startsWith('/') ? path : `/${path}`);
+ return joinPath(GITHUB_ORIGIN, path);
}
}
diff --git a/extensions/github1s/src/adapters/github1s/settings.ts b/extensions/github1s/src/adapters/github1s/settings.ts
index ee628d711..2b2862632 100644
--- a/extensions/github1s/src/adapters/github1s/settings.ts
+++ b/extensions/github1s/src/adapters/github1s/settings.ts
@@ -18,33 +18,48 @@ export const messageApiMap = {
export class GitHub1sSettingsViewProvider implements vscode.WebviewViewProvider {
public static readonly viewType = 'github1s.views.settings';
- public registerListeners(webviewView: vscode.WebviewView) {
- const tokenManager = GitHubTokenManager.getInstance();
- const githubFetcher = GitHubFetcher.getInstance();
+ protected tokenManager = GitHubTokenManager.getInstance();
+ protected apiFetcher: Pick<
+ GitHubFetcher,
+ 'getPreferSourcegraphApi' | 'setPreferSourcegraphApi' | 'onDidChangePreferSourcegraphApi'
+ > = GitHubFetcher.getInstance();
+
+ protected pageTitle = 'GitHub1s Settings';
+ protected OAuthCommand = 'github1s.commands.vscode.connectToGitHub';
+ protected detailPageCommand = 'github1s.commands.openGitHub1sAuthPage';
+ protected pageConfig = {
+ pageDescriptionLines: [
+ 'For unauthenticated requests, the rate limit of GitHub allows for up to 60 requests per hour.',
+ 'For API requests using Authentication, you can make up to 5,000 requests per hour.',
+ ],
+ OAuthButtonText: 'Connect to GitHub',
+ createTokenLink: `${GITHUB_ORIGIN}/settings/tokens/new?scopes=repo&description=GitHub1s`,
+ };
+ public registerListeners(webviewView: vscode.WebviewView) {
webviewView.webview.onDidReceiveMessage((message) => {
const commonResponse = { id: message.id, type: message.type };
const postMessage = (data?: unknown) => webviewView.webview.postMessage({ ...commonResponse, data });
switch (message.type) {
case 'get-token':
- postMessage(tokenManager.getToken());
+ postMessage(this.tokenManager.getToken());
break;
case 'set-token':
- tokenManager.setToken(message.data || '').then(() => postMessage());
+ this.tokenManager.setToken(message.data || '').then(() => postMessage());
break;
case 'validate-token':
- tokenManager.validateToken(message.data).then((tokenStatus) => postMessage(tokenStatus));
+ this.tokenManager.validateToken(message.data).then((tokenStatus) => postMessage(tokenStatus));
break;
case 'open-detail-page':
- vscode.commands.executeCommand('github1s.commands.openGitHub1sAuthPage').then(() => postMessage());
+ vscode.commands.executeCommand(this.detailPageCommand).then(() => postMessage());
break;
- case 'connect-to-github':
- vscode.commands.executeCommand('github1s.commands.vscode.connectToGitHub').then((data: any) => {
+ case 'oauth-authorizing':
+ vscode.commands.executeCommand(this.OAuthCommand).then((data: any) => {
if (data && data.error_description) {
vscode.window.showErrorMessage(data.error_description);
} else if (data && data.access_token) {
- GitHubTokenManager.getInstance().setToken(data.access_token || '');
+ this.tokenManager.setToken(data.access_token || '');
}
postMessage();
});
@@ -53,21 +68,21 @@ export class GitHub1sSettingsViewProvider implements vscode.WebviewViewProvider
const messageApi = messageApiMap[message.data?.level];
messageApi && messageApi(...message.data?.args).then((response) => postMessage(response));
break;
- case 'get-use-sourcegraph-api':
- githubFetcher.useSourcegraphApiFirst().then((value) => postMessage(value));
+ case 'get-prefer-sourcegraph-api':
+ this.apiFetcher.getPreferSourcegraphApi().then((value) => postMessage(value));
break;
- case 'set-use-sourcegraph-api':
- githubFetcher.setUseSourcegraphApiFirst(message.data);
+ case 'set-prefer-sourcegraph-api':
+ this.apiFetcher.setPreferSourcegraphApi(message.data);
postMessage(message.data);
break;
}
});
- tokenManager.onDidChangeToken((token) => {
+ this.tokenManager.onDidChangeToken((token) => {
webviewView.webview.postMessage({ type: 'token-changed', token });
});
- githubFetcher.onDidChangeUseSourcegraphApiFirst((value) => {
- webviewView.webview.postMessage({ type: 'use-sourcegraph-api-changed', value });
+ this.apiFetcher.onDidChangePreferSourcegraphApi((value) => {
+ webviewView.webview.postMessage({ type: 'prefer-sourcegraph-api-changed', value });
});
}
@@ -81,9 +96,12 @@ export class GitHub1sSettingsViewProvider implements vscode.WebviewViewProvider
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/components.css').toString(),
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/github1s-settings.css').toString(),
];
+ const globalPageConfig = { ...this.pageConfig, extensionUri: extensionContext.extensionUri.toString() };
const scripts = [
+ 'data:text/javascript;base64,' +
+ Buffer.from(`window.pageConfig=${JSON.stringify(globalPageConfig)};`).toString('base64'),
vscode.Uri.joinPath(extensionContext.extensionUri, 'assets/pages/github1s-settings.js').toString(),
];
- webviewView.webview.html = createPageHtml('GitHub1s Settings', styles, scripts);
+ webviewView.webview.html = createPageHtml(this.pageTitle, styles, scripts);
}
}
diff --git a/extensions/github1s/src/adapters/github1s/token.ts b/extensions/github1s/src/adapters/github1s/token.ts
index 09fb703d2..69e4e8f4e 100644
--- a/extensions/github1s/src/adapters/github1s/token.ts
+++ b/extensions/github1s/src/adapters/github1s/token.ts
@@ -5,55 +5,67 @@
import * as vscode from 'vscode';
import { getExtensionContext } from '@/helpers/context';
-const GITHUB_OAUTH_TOKEN = 'github-oauth-token';
-
-export interface TokenStatus {
- ratelimitLimit: number;
- ratelimitRemaining: number;
- ratelimitReset: number;
- ratelimitResource: number;
- ratelimitUsed: number;
+export interface ValidateResult {
+ username: string;
+ avatar_url: string;
+ profile_url: string;
+ ratelimits?: {
+ limit?: number;
+ remaining?: number;
+ reset?: number;
+ resource?: number;
+ used?: number;
+ };
}
export class GitHubTokenManager {
- private static instance: GitHubTokenManager | null = null;
+ protected static instance: GitHubTokenManager | null = null;
private _emitter = new vscode.EventEmitter();
public onDidChangeToken = this._emitter.event;
+ public tokenStateKey = 'github-oauth-token';
- private constructor() {}
+ protected constructor() {}
public static getInstance(): GitHubTokenManager {
if (GitHubTokenManager.instance) {
return GitHubTokenManager.instance;
}
- return (GitHubTokenManager.instance = new GitHubTokenManager());
+ return (GitHubTokenManager.instance = new this());
}
public getToken(): string {
- return getExtensionContext().globalState.get(GITHUB_OAUTH_TOKEN) || '';
+ return getExtensionContext().globalState.get(this.tokenStateKey) || '';
}
public async setToken(token: string) {
const isTokenChanged = this.getToken() !== token;
return getExtensionContext()
- .globalState.update(GITHUB_OAUTH_TOKEN, token)
+ .globalState.update(this.tokenStateKey, token)
.then(() => isTokenChanged && this._emitter.fire(token));
}
- public async validateToken(token?: string): Promise {
+ public async validateToken(token?: string): Promise {
const accessToken = token === undefined ? this.getToken() : token;
+ if (!accessToken) {
+ return Promise.resolve(null);
+ }
const fetchOptions = accessToken ? { headers: { Authorization: `token ${accessToken}` } } : {};
- return fetch('https://api.github.com', fetchOptions)
+ return fetch(`${GITHUB_API_PREFIX}/user`, fetchOptions)
.then((response) => {
if (response.status === 401) {
return null;
}
- return {
- ratelimitLimit: +response.headers.get('x-ratelimit-limit')! || 0,
- ratelimitRemaining: +response.headers.get('x-ratelimit-remaining')! || 0,
- ratelimitReset: +response.headers.get('x-ratelimit-reset')! || 0,
- ratelimitResource: +response.headers.get('ratelimit-resource')! || 0,
- ratelimitUsed: +response.headers.get('x-ratelimit-used')! || 0,
- };
+ return response.json().then((data) => ({
+ username: data.login,
+ avatar_url: data.avatar_url,
+ profile_url: data.html_url,
+ rateLimits: {
+ limit: +response.headers.get('x-ratelimit-limit')! || 0,
+ remaining: +response.headers.get('x-ratelimit-remaining')! || 0,
+ reset: +response.headers.get('x-ratelimit-reset')! || 0,
+ resource: +response.headers.get('ratelimit-resource')! || 0,
+ used: +response.headers.get('x-ratelimit-used')! || 0,
+ },
+ }));
})
.catch(() => null);
}
diff --git a/extensions/github1s/src/adapters/gitlab1s/authentication.ts b/extensions/github1s/src/adapters/gitlab1s/authentication.ts
new file mode 100644
index 000000000..920ec6b63
--- /dev/null
+++ b/extensions/github1s/src/adapters/gitlab1s/authentication.ts
@@ -0,0 +1,29 @@
+/**
+ * @file gitlab authentication page
+ * @author netcon
+ */
+
+import { GitHub1sAuthenticationView } from '../github1s/authentication';
+import { GitLabTokenManager } from './token';
+
+export class GitLab1sAuthenticationView extends GitHub1sAuthenticationView {
+ protected tokenManager = GitLabTokenManager.getInstance();
+ protected pageTitle = 'Authenticating to GitLab';
+ protected OAuthCommand = 'github1s.commands.vscode.connectToGitLab';
+ protected pageConfig = {
+ authenticationFormTitle: 'Authenticating to GitLab',
+ OAuthButtonText: 'Connect to GitLab',
+ OAuthButtonLogo: 'assets/pages/assets/gitlab.svg',
+ createTokenLink: `${GITLAB_ORIGIN}/-/profile/personal_access_tokens?scopes=read_api&name=GitLab1s`,
+ authenticationFeatures: [
+ {
+ text: 'Access GitLab personal repository',
+ link: 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html',
+ },
+ {
+ text: 'Higher rate limit for GitLab official API',
+ link: 'https://docs.gitlab.com/ee/security/rate_limits.html',
+ },
+ ],
+ };
+}
diff --git a/extensions/github1s/src/adapters/gitlab1s/data-source.ts b/extensions/github1s/src/adapters/gitlab1s/data-source.ts
new file mode 100644
index 000000000..9b84a1cd8
--- /dev/null
+++ b/extensions/github1s/src/adapters/gitlab1s/data-source.ts
@@ -0,0 +1,462 @@
+/**
+ * @file gitlab1s data-source-provider
+ * @author netcon
+ */
+
+import {
+ Branch,
+ CodeReview,
+ CodeReviewState,
+ TextSearchOptions,
+ Commit,
+ CommonQueryOptions,
+ DataSource,
+ Directory,
+ DirectoryEntry,
+ File,
+ BlameRange,
+ FileType,
+ Tag,
+ TextSearchResults,
+ TextSearchQuery,
+ SymbolDefinitions,
+ SymbolReferences,
+ FileChangeStatus,
+ ChangedFile,
+ SymbolHover,
+ CommitsQueryOptions,
+ CodeReviewsQueryOptions,
+} from '../types';
+import { toUint8Array } from 'js-base64';
+import { matchSorter } from 'match-sorter';
+import { GitLabFetcher } from './fetcher';
+import { SourcegraphDataSource } from '../sourcegraph/data-source';
+import { decorate, memorize } from '@/helpers/func';
+
+const FileTypeMap = {
+ blob: FileType.File,
+ tree: FileType.Directory,
+ commit: FileType.Submodule,
+};
+
+const getMergeRequestState = (mergeRequest: { state: string; merged_at: string | null }): CodeReviewState => {
+ // current merge request is open
+ if (mergeRequest.state === 'opened') {
+ return CodeReviewState.Open;
+ }
+ // current merge request is merged
+ if (mergeRequest.state === 'closed' && mergeRequest.merged_at) {
+ return CodeReviewState.Merged;
+ }
+ // current merge is closed
+ return CodeReviewState.Merged;
+};
+
+const resolveComputeAge = (timestamps: number[], ageLimit = 10) => {
+ const maxTimestamp = Math.max(...timestamps);
+ const minTimestamp = Math.min(...timestamps);
+ const step = (maxTimestamp - minTimestamp) / ageLimit;
+ return (timestamp: number) => {
+ const age = Math.floor((timestamp - minTimestamp) / (step || 1));
+ return (((Math.max(age, ageLimit - 1) % ageLimit) + ageLimit) % ageLimit) + 1;
+ };
+};
+
+const sourcegraphDataSource = SourcegraphDataSource.getInstance('gitlab');
+const trySourcegraphApiFirst = (_target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
+ const originalMethod = descriptor.value;
+
+ descriptor.value = async function Promise>(...args: Parameters) {
+ const gitlabFetcher = GitLabFetcher.getInstance();
+ if (await gitlabFetcher.getPreferSourcegraphApi(args[0])) {
+ try {
+ return await sourcegraphDataSource[propertyKey](...args);
+ } catch (e) {}
+ }
+ return originalMethod.apply(this, args);
+ };
+};
+
+export class GitLab1sDataSource extends DataSource {
+ private static instance: GitLab1sDataSource | null = null;
+ private branchesPromiseMap: Map> = new Map();
+ private tagsPromiseMap: Map> = new Map();
+ private matchedRefsMap = new Map();
+
+ public static getInstance(): GitLab1sDataSource {
+ if (GitLab1sDataSource.instance) {
+ return GitLab1sDataSource.instance;
+ }
+ return (GitLab1sDataSource.instance = new GitLab1sDataSource());
+ }
+
+ async provideRepository(repo: string) {
+ const fetcher = GitLabFetcher.getInstance();
+ const { data } = await fetcher.request('GET /projects/{repo}', { repo });
+ return { private: data.visibility === 'private', defaultBranch: data.default_branch };
+ }
+
+ @trySourcegraphApiFirst
+ async provideDirectory(repo: string, ref: string, path: string, recursive = false): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ let page = 1;
+ let files = [];
+ const parseTreeItem = (treeItem): DirectoryEntry => ({
+ path: treeItem.path.slice(path.length),
+ type: FileTypeMap[treeItem.type] || FileType.File,
+ commitSha: FileTypeMap[treeItem.id] === FileType.Submodule ? treeItem.sha || 'HEAD' : undefined,
+ size: treeItem.size,
+ });
+ while (page > 0) {
+ const requestParams = { ref, page, path, repo, recursive };
+ const { data, headers } = await fetcher.request(
+ 'GET /projects/{repo}/repository/tree?recursive={recursive}&per_page=100&page={page}&ref={ref}&path={path}',
+ requestParams
+ );
+ files = files.concat(data);
+ const nextPage = Number(headers!.get('x-next-page'));
+ page = nextPage > page ? nextPage : 0;
+ }
+
+ return {
+ entries: files.map(parseTreeItem),
+ truncated: false,
+ };
+ }
+
+ @trySourcegraphApiFirst
+ async provideFile(repo: string, ref: string, path: string): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { ref, path, repo };
+ const { data } = await fetcher.request('GET /projects/{repo}/repository/files/{path}?ref={ref}', requestParams);
+ return { content: toUint8Array((data as any).content) };
+ }
+
+ @decorate(memorize)
+ async getBranches(repo: string, ref: 'heads' | 'tags'): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, ref };
+ const { data } = await fetcher.request('GET /projects/{repo}/repository/branches', requestParams);
+ return data.map((item) => ({
+ name: item.name,
+ commitSha: item.commit.id,
+ description: `${ref === 'heads' ? 'Branch' : 'Tag'} at ${item.commit.short_id}`,
+ }));
+ }
+
+ @decorate(memorize)
+ async getTags(repo: string, ref: 'heads' | 'tags'): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, ref };
+ const { data } = await fetcher.request('GET /projects/{repo}/repository/tags', requestParams);
+ return data.map((item) => ({
+ name: item.name,
+ commitSha: item.commit.id,
+ description: `${ref === 'heads' ? 'Branch' : 'Tag'} at ${item.commit.short_id}`,
+ }));
+ }
+
+ @decorate(memorize)
+ async getDefaultBranch(repo: string) {
+ return (await this.provideRepository(repo))?.defaultBranch || 'HEAD';
+ }
+
+ @trySourcegraphApiFirst
+ async extractRefPath(repo: string, refAndPath: string): Promise<{ ref: string; path: string }> {
+ if (!refAndPath) {
+ return { ref: await this.getDefaultBranch(repo), path: '' };
+ }
+ if (refAndPath.match(/^HEAD(\/.*)?$/i)) {
+ return { ref: 'HEAD', path: refAndPath.slice(5) };
+ }
+ if (!this.matchedRefsMap.has(repo)) {
+ this.matchedRefsMap.set(repo, []);
+ }
+ const matchPathRef = (ref) => refAndPath.startsWith(`${ref}/`) || refAndPath === ref;
+ const pathRef = this.matchedRefsMap.get(repo)?.find(matchPathRef);
+ if (pathRef) {
+ return { ref: pathRef, path: refAndPath.slice(pathRef.length + 1) };
+ }
+ const [branches, tags] = await this.prepareAllRefs(repo);
+ const exactRef = [...branches, ...tags].map((item) => item.name).find(matchPathRef);
+ const ref = exactRef || refAndPath.split('/')[0] || 'HEAD';
+ exactRef && this.matchedRefsMap.get(repo)?.push(ref);
+ return { ref, path: refAndPath.slice(ref.length + 1) };
+ }
+
+ async prepareAllRefs(repo: string) {
+ return Promise.all([this.provideBranches(repo), this.provideTags(repo)]);
+ }
+
+ @trySourcegraphApiFirst
+ async provideBranches(repo: string, options?: CommonQueryOptions): Promise {
+ if (!this.branchesPromiseMap.has(repo)) {
+ this.branchesPromiseMap.set(repo, this.getBranches(repo, 'heads'));
+ }
+ return this.branchesPromiseMap.get(repo)!.then((branches) => {
+ const matchOptions = { keys: ['name'] };
+ const matchedBranches = options?.query ? matchSorter(branches, options.query, matchOptions) : branches;
+ if (options?.pageSize) {
+ const page = options.page || 1;
+ const pageSize = options.pageSize;
+ return matchedBranches.slice(pageSize * (page - 1), pageSize * page);
+ }
+ return matchedBranches;
+ });
+ }
+
+ @trySourcegraphApiFirst
+ async provideBranch(repo: string, branchName: string): Promise {
+ const branches = await this.provideBranches(repo);
+ return branches.find((item) => item.name === branchName) || null;
+ }
+
+ @trySourcegraphApiFirst
+ async provideTags(repoFullName: string, options?: CommonQueryOptions): Promise {
+ if (!this.tagsPromiseMap.has(repoFullName)) {
+ this.tagsPromiseMap.set(repoFullName, this.getTags(repoFullName, 'tags'));
+ }
+ return this.tagsPromiseMap.get(repoFullName)!.then((tags) => {
+ const matchOptions = { keys: ['name'] };
+ const matchedTags = options?.query ? matchSorter(tags, options.query, matchOptions) : tags;
+ if (options?.pageSize) {
+ const page = options.page || 1;
+ const pageSize = options.pageSize;
+ return matchedTags.slice(pageSize * (page - 1), pageSize * page);
+ }
+ return matchedTags;
+ });
+ }
+
+ @trySourcegraphApiFirst
+ async provideTag(repoFullName: string, tagName: string): Promise {
+ const tags = await this.provideTags(repoFullName);
+ return tags.find((item) => item.name === tagName) || null;
+ }
+
+ async provideTextSearchResults(
+ repoFullName: string,
+ ref: string,
+ query: TextSearchQuery,
+ options: TextSearchOptions
+ ): Promise {
+ return sourcegraphDataSource.provideTextSearchResults(repoFullName, ref, query, options);
+ }
+
+ @trySourcegraphApiFirst
+ async provideCommits(repo: string, options?: CommitsQueryOptions): Promise<(Commit & { files?: ChangedFile[] })[]> {
+ const fetcher = GitLabFetcher.getInstance();
+ const queryParams = {
+ page: options?.page,
+ per_page: options?.pageSize,
+ sha: options?.from,
+ path: options?.path,
+ author: options?.author,
+ };
+ const requestParams = { repo, ...queryParams };
+ const { data } = await fetcher.request(
+ 'GET /projects/{repo}/repository/commits?per_page={per_page}&page={page}&path={path}&ref_name={sha}',
+ requestParams
+ );
+ return Promise.all(
+ data.map(async (item) => ({
+ sha: item.id,
+ author: item.author_name,
+ email: item.author_email,
+ message: item.message,
+ committer: item.committer_name,
+ createTime: item.created_at ? new Date(item.created_at) : undefined,
+ parents: item.parent_ids.map((parent) => parent) || [],
+ avatarUrl: item.author?.avatar_url || (await this.provideUserAvatarLink(item.author_name)),
+ }))
+ );
+ }
+
+ @trySourcegraphApiFirst
+ async provideCommit(repo: string, ref: string): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, ref };
+ const { data } = await fetcher.request('GET /projects/{repo}/repository/commits/{ref}', requestParams);
+ return {
+ sha: data.id,
+ author: data.author_name,
+ email: data.author_email,
+ message: data.message,
+ committer: data.committer_name,
+ createTime: data.created_at ? new Date(data.created_at) : undefined,
+ parents: data.parent_ids || [],
+ files: data.files?.map((item) => ({
+ path: item.filename || item.previous_filename!,
+ previousPath: item.previous_filename,
+ status: item.status as FileChangeStatus,
+ })),
+ avatarUrl: data?.avatar_url,
+ };
+ }
+
+ @trySourcegraphApiFirst
+ async provideCommitChangedFiles(repo: string, ref: string, _options?: CommonQueryOptions): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, ref };
+ const { data } = await fetcher.request('GET /projects/{repo}/repository/commits/{ref}/diff', requestParams);
+ return (
+ data?.map((item) => ({
+ path: item.new_path || item.old_path!,
+ previousPath: item.old_path,
+ status: item.new_file
+ ? FileChangeStatus.Added
+ : item.deleted_file
+ ? FileChangeStatus.Removed
+ : item.renamed_file
+ ? FileChangeStatus.Renamed
+ : FileChangeStatus.Modified,
+ })) || []
+ );
+ }
+
+ async provideCodeReviews(
+ repo: string,
+ options?: CodeReviewsQueryOptions
+ ): Promise<(CodeReview & { files?: ChangedFile[] })[]> {
+ const fetcher = GitLabFetcher.getInstance();
+ const state = options?.state ? (options.state === CodeReviewState.Open ? 'open' : 'closed') : 'all';
+ // per_page=100&page={page}
+ const queryParams = { state, page: options?.page, per_page: options?.pageSize, creator: options?.creator };
+ const requestParams = { repo, ...queryParams };
+ const { data } = await fetcher.request(
+ 'GET /projects/{repo}/merge_requests?per_page={per_page}&page={page}',
+ requestParams as any
+ );
+
+ return data.map((item) => ({
+ id: `${item.iid}`,
+ title: item.title,
+ state: getMergeRequestState(item),
+ creator: item.author?.name || item.author?.username,
+ createTime: new Date(item.created_at),
+ mergeTime: item.merged_at ? new Date(item.merged_at) : null,
+ closeTime: item.closed_at ? new Date(item.closed_at) : null,
+ source: item.source_branch,
+ target: item.target_branch,
+ avatarUrl: item.author?.avatar_url,
+ }));
+ }
+
+ async provideCodeReview(repo: string, id: string) {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, id };
+ const { data } = await fetcher.request('GET /projects/{repo}/merge_requests/{id}', requestParams);
+
+ return {
+ id: `${data.iid}`,
+ title: data.title,
+ state: getMergeRequestState(data),
+ creator: data.author?.name || data.author?.username,
+ createTime: new Date(data.created_at),
+ mergeTime: data.merged_at ? new Date(data.merged_at) : null,
+ closeTime: data.closed_at ? new Date(data.closed_at) : null,
+ source: data.source_branch,
+ target: data.target_branch,
+ sourceSha: data.diff_refs.head_sha,
+ targetSha: data.diff_refs.base_sha,
+ avatarUrl: data.author?.avatar_url,
+ };
+ }
+
+ // eslint-disable-next-line
+ async provideCodeReviewChangedFiles(repo: string, id: string, options?: CommonQueryOptions): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const pageParams = { per_page: options?.pageSize, page: options?.page };
+ const filesRequestParams = { repo, id, ...pageParams };
+ const { data } = await fetcher.request(
+ 'GET /projects/{repo}/merge_requests/{id}/changes?per_page={per_page}&page={page}',
+ filesRequestParams
+ );
+
+ return data.changes.map((item) => ({
+ path: item.new_path,
+ previousPath: item.old_path,
+ status: item.new_file
+ ? FileChangeStatus.Added
+ : item.deleted_file
+ ? FileChangeStatus.Removed
+ : item.renamed_file
+ ? FileChangeStatus.Renamed
+ : FileChangeStatus.Modified,
+ }));
+ }
+
+ @trySourcegraphApiFirst
+ async provideFileBlameRanges(repo: string, ref: string, path: string): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const requestParams = { repo, ref, path };
+ const { data } = await fetcher.request(
+ 'GET /projects/{repo}/repository/files/{path}/blame?ref={ref}',
+ requestParams
+ );
+ let startLine = 1;
+ const timestamps = data.map(({ commit }) => +new Date(commit.authored_date) || 0);
+ const computeAge = resolveComputeAge(timestamps);
+ return (data || []).map(({ commit, lines }) => {
+ let startingLine = startLine;
+ let endingLine = startingLine + lines.length;
+ startLine = endingLine + 1;
+ return {
+ age: computeAge(+new Date(commit?.authored_date) || 0),
+ startingLine,
+ endingLine,
+ commit: {
+ sha: commit?.id as string,
+ author: commit?.author_name as string,
+ email: commit?.author_email as string,
+ message: commit?.message as string,
+ createTime: new Date(commit?.authored_date),
+ avatarUrl: this.provideUserAvatarLink(encodeURIComponent(commit?.author?.name)),
+ },
+ };
+ });
+ }
+
+ async getAvatar(email): Promise {
+ const fetcher = GitLabFetcher.getInstance();
+ const { data } = await fetcher.request('GET /avatar?email={email}', { email });
+ return data.avatar_url;
+ }
+
+ provideSymbolDefinitions(
+ repoFullName: string,
+ ref: string,
+ path: string,
+ line: number,
+ character: number,
+ symbol: string
+ ): Promise {
+ return sourcegraphDataSource.provideSymbolDefinitions(repoFullName, ref, path, line, character, symbol);
+ }
+
+ async provideSymbolReferences(
+ repoFullName: string,
+ ref: string,
+ path: string,
+ line: number,
+ character: number,
+ symbol: string
+ ): Promise {
+ return sourcegraphDataSource.provideSymbolReferences(repoFullName, ref, path, line, character, symbol);
+ }
+
+ async provideSymbolHover(
+ repoFullName: string,
+ ref: string,
+ path: string,
+ line: number,
+ character: number,
+ _symbol: string
+ ): Promise {
+ return sourcegraphDataSource.provideSymbolHover(repoFullName, ref, path, line, character, _symbol);
+ }
+
+ provideUserAvatarLink(user: string): string {
+ return `https://www.gravatar.com/avatar/${user}?d=identicon`;
+ }
+}
diff --git a/extensions/github1s/src/adapters/gitlab1s/fetcher.ts b/extensions/github1s/src/adapters/gitlab1s/fetcher.ts
new file mode 100644
index 000000000..e64ad4aaf
--- /dev/null
+++ b/extensions/github1s/src/adapters/gitlab1s/fetcher.ts
@@ -0,0 +1,136 @@
+/**
+ * @file gitlab api fetcher
+ * @author netcon
+ */
+
+import * as vscode from 'vscode';
+import { getExtensionContext } from '@/helpers/context';
+import { GitLab1sAuthenticationView } from './authentication';
+import { GitLabTokenManager } from './token';
+import { isNil } from '@/helpers/util';
+import { reuseable } from '@/helpers/func';
+import { getCurrentRepo } from './parse-path';
+import { SourcegraphDataSource } from '../sourcegraph/data-source';
+
+export const errorMessages = {
+ badCredentials: {
+ anonymous: 'Bad credentials, please authenticate to gitlab and retry',
+ authenticated: 'This token is invalid, please try another one',
+ },
+ repoNotFound: {
+ anonymous: 'Repository not found, if it is private, you can provide an AccessToken to access it',
+ authenticated: 'Repository not found, if it is private, you can try change an AccessToken to access it',
+ },
+ noPermission: {
+ anonymous: 'You have no permission for this operation, please authenticate to gitlab and retry',
+ authenticated: 'You have no permission for this operation, please try another account',
+ },
+};
+
+const detectErrorMessage = (response: any, authenticated: boolean) => {
+ if (response?.status === 401 && response?.data?.message?.includes?.('Unauthorized')) {
+ return errorMessages.badCredentials[authenticated ? 'authenticated' : 'anonymous'];
+ }
+ if (response?.status === 404) {
+ return errorMessages.repoNotFound[authenticated ? 'authenticated' : 'anonymous'];
+ }
+ if (response?.status === 403) {
+ return errorMessages.noPermission[authenticated ? 'authenticated' : 'anonymous'];
+ }
+ return response?.data?.message || response?.data?.error || '';
+};
+
+const PREFER_SOURCEGRAPH_API = 'PREFER_SOURCEGRAPH_API';
+
+export class GitLabFetcher {
+ private static instance: GitLabFetcher | null = null;
+ private _emitter = new vscode.EventEmitter();
+ public onDidChangePreferSourcegraphApi = this._emitter.event;
+ private _currentRepoPromise: Promise | null = null;
+
+ public static getInstance(): GitLabFetcher {
+ if (GitLabFetcher.instance) {
+ return GitLabFetcher.instance;
+ }
+ return (GitLabFetcher.instance = new GitLabFetcher());
+ }
+
+ private constructor() {
+ this.initPreferSourcegraphApi();
+ GitLabTokenManager.getInstance().onDidChangeToken(() => this.initPreferSourcegraphApi());
+ }
+
+ private _request = reuseable(
+ (
+ command: string,
+ params: Record
+ ): Promise<{ status: number; data: any; headers: Headers }> => {
+ let [method, path] = command.split(/\s+/).filter(Boolean);
+ Object.keys(params).forEach((el) => {
+ path = path.replace(`{${el}}`, `${encodeURIComponent(params[el] || '')}`);
+ });
+ const accessToken = GitLabTokenManager.getInstance().getToken();
+ const fetchOptions: { headers: Record } =
+ accessToken?.length < 60
+ ? { headers: { 'PRIVATE-TOKEN': `${accessToken}` } }
+ : { headers: { Authorization: `Bearer ${accessToken}` } };
+ return fetch(GITLAB_API_PREFIX + path, {
+ ...fetchOptions,
+ method,
+ }).then(async (response: Response & { data: any }) => {
+ response.data = await response.json();
+ return response.ok ? response : Promise.reject({ response });
+ });
+ }
+ );
+
+ public request = (command: string, params: Record) => {
+ return this._request(command, params).catch(async (error: { response: any }) => {
+ const errorStatus = error?.response?.status as number | undefined;
+ const repoNotFound = errorStatus === 404 && !(await this.resolveCurrentRepo());
+ if ((errorStatus && [401, 403].includes(errorStatus)) || repoNotFound) {
+ // maybe we have to acquire github access token to continue
+ const accessToken = GitLabTokenManager.getInstance().getToken();
+ const message = detectErrorMessage(error?.response, !!accessToken);
+ await GitLab1sAuthenticationView.getInstance().open(message, true);
+ return this._request(command, params);
+ }
+ throw error;
+ });
+ };
+
+ private resolveCurrentRepo(forceUpdate: boolean = false) {
+ if (this._currentRepoPromise && !forceUpdate) {
+ return this._currentRepoPromise;
+ }
+ return (this._currentRepoPromise = Promise.resolve(getCurrentRepo())
+ .then(async (repo) => this._request('GET /projects/{repo}', { repo }).then((res) => res.data))
+ .catch(() => null));
+ }
+
+ private async initPreferSourcegraphApi() {
+ if (await this.getPreferSourcegraphApi()) {
+ const sgDataSource = SourcegraphDataSource.getInstance('github');
+ if (!(await sgDataSource.provideRepository(await getCurrentRepo()))) {
+ this.resolveCurrentRepo(true).then((repo) => {
+ repo?.visibility === 'private' && this.setPreferSourcegraphApi(false);
+ });
+ }
+ }
+ }
+
+ public async getPreferSourcegraphApi(repo?: string): Promise {
+ const targetRepo = repo || (await getCurrentRepo());
+ const globalState = getExtensionContext().globalState;
+ const cachedData: Record | undefined = globalState.get(PREFER_SOURCEGRAPH_API);
+ return !isNil(cachedData?.[targetRepo]) ? !!cachedData?.[targetRepo] : true;
+ }
+
+ public async setPreferSourcegraphApi(value: boolean, repo?: string) {
+ const targetRepo = repo || (await getCurrentRepo());
+ const globalState = getExtensionContext().globalState;
+ const cachedData: Record | undefined = globalState.get(PREFER_SOURCEGRAPH_API);
+ await globalState.update(PREFER_SOURCEGRAPH_API, { ...cachedData, [targetRepo]: value });
+ this._emitter.fire(value);
+ }
+}
diff --git a/extensions/github1s/src/adapters/gitlab1s/index.ts b/extensions/github1s/src/adapters/gitlab1s/index.ts
index d088e9548..33a499b43 100644
--- a/extensions/github1s/src/adapters/gitlab1s/index.ts
+++ b/extensions/github1s/src/adapters/gitlab1s/index.ts
@@ -3,10 +3,15 @@
* @author netcon
*/
+import * as vscode from 'vscode';
import { GitLab1sRouterParser } from './router-parser';
+import { GitLab1sDataSource } from './data-source';
import { SourcegraphDataSource } from '../sourcegraph/data-source';
import { Adapter, CodeReviewType, PlatformName } from '../types';
+import { GitLab1sSettingsViewProvider } from './settings';
+import { GitLab1sAuthenticationView } from './authentication';
import { setVSCodeContext } from '@/helpers/vscode';
+import { getCurrentRepo } from './parse-path';
export class GitLab1sAdapter implements Adapter {
public scheme: string = 'gitlab1s';
@@ -14,7 +19,7 @@ export class GitLab1sAdapter implements Adapter {
public codeReviewType = CodeReviewType.MergeRequest;
resolveDataSource() {
- return Promise.resolve(SourcegraphDataSource.getInstance('gitlab'));
+ return Promise.resolve(GitLab1sDataSource.getInstance());
}
resolveRouterParser() {
@@ -22,12 +27,30 @@ export class GitLab1sAdapter implements Adapter {
}
activateAsDefault() {
+ // register settings view and show it in activity bar
+ setVSCodeContext('github1s:views:settings:visible', true);
+ setVSCodeContext('github1s:views:codeReviewList:visible', true);
setVSCodeContext('github1s:views:commitList:visible', true);
setVSCodeContext('github1s:views:fileHistory:visible', true);
setVSCodeContext('github1s:features:gutterBlame:enabled', true);
+
+ vscode.window.registerWebviewViewProvider(
+ GitLab1sSettingsViewProvider.viewType,
+ new GitLab1sSettingsViewProvider()
+ );
+ vscode.commands.registerCommand('github1s.commands.openGitLab1sAuthPage', () => {
+ return GitLab1sAuthenticationView.getInstance().open();
+ });
+ vscode.commands.registerCommand('github1s.commands.syncSourcegraphRepository', async () => {
+ const dataSource = SourcegraphDataSource.getInstance('gitlab');
+ const randomRef = (Math.random() + 1).toString(36).slice(2);
+ return dataSource.provideCommit(await getCurrentRepo(), randomRef);
+ });
}
deactivateAsDefault() {
+ setVSCodeContext('github1s:views:settings:visible', false);
+ setVSCodeContext('github1s:views:codeReviewList:visible', false);
setVSCodeContext('github1s:views:commitList:visible', false);
setVSCodeContext('github1s:views:fileHistory:visible', false);
setVSCodeContext('github1s:features:gutterBlame:enabled', false);
diff --git a/extensions/github1s/src/adapters/gitlab1s/parse-path.ts b/extensions/github1s/src/adapters/gitlab1s/parse-path.ts
index 4d20c51fc..7dc02a531 100644
--- a/extensions/github1s/src/adapters/gitlab1s/parse-path.ts
+++ b/extensions/github1s/src/adapters/gitlab1s/parse-path.ts
@@ -3,16 +3,26 @@
* @author netcon
*/
+import * as vscode from 'vscode';
import { parsePath } from 'history';
import { PageType, RouterState } from '../types';
-import { SourcegraphDataSource } from '@/adapters/sourcegraph/data-source';
-
-const resolveBranchName = async (repo: string, ref = '') => {
- if (ref && ref.toUpperCase() !== 'HEAD') {
- return ref;
- }
- const dataSource = SourcegraphDataSource.getInstance('gitlab');
- return (await dataSource.provideRepository(repo))?.defaultBranch || 'HEAD';
+import { GitLab1sDataSource } from './data-source';
+import { memorize } from '@/helpers/func';
+import { getBrowserUrl } from '@/helpers/context';
+
+export const DEFAULT_REPO = 'gitlab-org/gitlab-docs';
+
+export const getCurrentRepo = memorize(() => {
+ return getBrowserUrl().then((browserUrl: string) => {
+ const pathParts = vscode.Uri.parse(browserUrl).path.split('/').filter(Boolean);
+ const dashIndex = pathParts.indexOf('-');
+ return (dashIndex < 0 ? pathParts : pathParts.slice(0, dashIndex)).join('/') || DEFAULT_REPO;
+ });
+});
+
+const getDefaultBranch = async (repo: string): Promise => {
+ const dataSource = GitLab1sDataSource.getInstance();
+ return dataSource.getDefaultBranch(repo);
};
const parseTreeUrl = async (path: string): Promise => {
@@ -20,10 +30,10 @@ const parseTreeUrl = async (path: string): Promise => {
const dashIndex = pathParts.indexOf('-');
const repo = (dashIndex < 0 ? pathParts : pathParts.slice(0, dashIndex)).join('/');
const restParts = dashIndex < 0 ? [] : pathParts.slice(dashIndex + 2);
- const dataSource = SourcegraphDataSource.getInstance('gitlab');
+ const dataSource = GitLab1sDataSource.getInstance();
const { ref, path: filePath } = await dataSource.extractRefPath(repo, restParts.join('/'));
- return { pageType: PageType.Tree, repo, ref: await resolveBranchName(repo, ref), filePath };
+ return { pageType: PageType.Tree, repo, ref, filePath };
};
const parseBlobUrl = async (path: string): Promise => {
@@ -35,7 +45,7 @@ const parseBlobUrl = async (path: string): Promise => {
}
// get selected line number range from path which looks like:
- // `/gitlab-org/gitlab/-/blob/HEAD/package.json#L10-L20`
+ // `/gitlab-org/gitlab/-/blob/main/package.json#L10-L20`
const matches = routerHash.match(/^#L(\d+)(?:-L(\d+))?/);
const [_, startLineNumber = '0', endLineNumber] = matches ? matches : [];
@@ -51,7 +61,7 @@ const parseCommitsUrl = async (path: string): Promise => {
const pathParts = parsePath(path).pathname!.split('/').filter(Boolean);
const dashIndex = pathParts.indexOf('-');
const repo = (dashIndex < 0 ? pathParts : pathParts.slice(0, dashIndex)).join('/');
- const ref = await resolveBranchName(repo, (dashIndex < 0 ? [] : pathParts.slice(dashIndex + 2)).join('/'));
+ const ref = dashIndex < 0 ? await getDefaultBranch(repo) : pathParts.slice(dashIndex + 2).join('/');
return { repo, pageType: PageType.CommitList, ref };
};
@@ -60,14 +70,64 @@ const parseCommitUrl = async (path: string): Promise => {
const pathParts = parsePath(path).pathname!.split('/').filter(Boolean);
const dashIndex = pathParts.indexOf('-');
const repo = (dashIndex < 0 ? pathParts : pathParts.slice(0, dashIndex)).join('/');
- const commitSha = await resolveBranchName(repo, (dashIndex < 0 ? [] : pathParts.slice(dashIndex + 2)).join('/'));
+ const commitSha = dashIndex < 0 ? await getDefaultBranch(repo) : pathParts.slice(dashIndex + 2).join('/');
return { repo, pageType: PageType.Commit, ref: commitSha, commitSha };
};
+const parseMergeRequestsUrl = async (path: string): Promise => {
+ const [owner, repo] = parsePath(path).pathname!.split('/').filter(Boolean);
+
+ return {
+ repo: `${owner}/${repo}`,
+ ref: await getDefaultBranch(`${owner}/${repo}`),
+ pageType: PageType.CodeReviewList,
+ };
+};
+
+const parseMergeRequestUrl = async (path: string): Promise => {
+ const pathParts = parsePath(path).pathname!.split('/').filter(Boolean);
+ const [owner, repo, , _pageType, codeReviewId] = pathParts;
+ const repoFullName = `${owner}/${repo}`;
+ const codeReview = await GitLab1sDataSource.getInstance().provideCodeReview(repoFullName, codeReviewId);
+
+ return {
+ repo: `${owner}/${repo}`,
+ pageType: PageType.CodeReview,
+ ref: codeReview.targetSha,
+ codeReviewId,
+ };
+};
+
+// const parseSearchUrl = async (path: string): Promise => {
+// const { pathname, search } = parsePath(path);
+// const pathParts = pathname!.split('/').filter(Boolean);
+// const [owner, repo, _pageType] = pathParts;
+// const queryOptions = queryString.parse(search || '');
+// const query = typeof queryOptions.q === 'string' ? queryOptions.q : '';
+// const isRegex = queryOptions.regex === 'yes';
+// const isCaseSensitive = queryOptions.case === 'yes';
+// const matchWholeWord = queryOptions.whole === 'yes';
+// const filesToInclude = typeof queryOptions['files-to-include'] === 'string' ? queryOptions['files-to-include'] : '';
+// const filesToExclude = typeof queryOptions['files-to-exclude'] === 'string' ? queryOptions['files-to-exclude'] : '';
+
+// return {
+// repo: `${owner}/${repo}`,
+// pageType: PageType.Search,
+// ref: 'HEAD',
+// query,
+// isRegex,
+// isCaseSensitive,
+// matchWholeWord,
+// filesToInclude,
+// filesToExclude,
+// };
+// };
+
const PAGE_TYPE_MAP = {
tree: PageType.Tree,
blob: PageType.Blob,
+ merge_requests: PageType.CodeReview,
commit: PageType.Commit,
commits: PageType.CommitList,
};
@@ -85,6 +145,13 @@ export const parseGitLabPath = async (path: string): Promise => {
return parseTreeUrl(path);
case PageType.Blob:
return parseBlobUrl(path);
+ case PageType.CodeReview:
+ if (pathParts.length > dashIndex + 2) {
+ return parseMergeRequestUrl(path);
+ }
+ return parseMergeRequestsUrl(path);
+ case PageType.CodeReviewList:
+ return parseMergeRequestsUrl(path);
case PageType.Commit:
return parseCommitUrl(path);
case PageType.CommitList:
@@ -93,10 +160,9 @@ export const parseGitLabPath = async (path: string): Promise => {
}
// fallback to default
- const fallbackRepository = 'gitlab-org/gitlab-docs';
return {
- repo: fallbackRepository,
- ref: await resolveBranchName(fallbackRepository),
+ repo: DEFAULT_REPO,
+ ref: await getDefaultBranch(DEFAULT_REPO),
pageType: PageType.Tree,
filePath: '',
};
diff --git a/extensions/github1s/src/adapters/gitlab1s/router-parser.ts b/extensions/github1s/src/adapters/gitlab1s/router-parser.ts
index adb978bbd..2e1809fbd 100644
--- a/extensions/github1s/src/adapters/gitlab1s/router-parser.ts
+++ b/extensions/github1s/src/adapters/gitlab1s/router-parser.ts
@@ -3,6 +3,7 @@
* @author netcon
*/
+import { joinPath } from '@/helpers/util';
import * as adapterTypes from '../types';
import { parseGitLabPath } from './parse-path';
@@ -46,6 +47,6 @@ export class GitLab1sRouterParser extends adapterTypes.RouterParser {
}
buildExternalLink(path: string): string {
- return 'https://gitlab.com' + (path.startsWith('/') ? path : `/${path}`);
+ return joinPath(GITLAB_ORIGIN, path);
}
}
diff --git a/extensions/github1s/src/adapters/gitlab1s/settings.ts b/extensions/github1s/src/adapters/gitlab1s/settings.ts
new file mode 100644
index 000000000..c77ea3e23
--- /dev/null
+++ b/extensions/github1s/src/adapters/gitlab1s/settings.ts
@@ -0,0 +1,24 @@
+/**
+ * @file GitLab1s Settings Webview Provider
+ * @author netcon
+ */
+
+import { GitLabTokenManager } from './token';
+import { GitLabFetcher } from './fetcher';
+import { GitHub1sSettingsViewProvider } from '../github1s/settings';
+
+export class GitLab1sSettingsViewProvider extends GitHub1sSettingsViewProvider {
+ protected tokenManager = GitLabTokenManager.getInstance();
+ protected apiFetcher = GitLabFetcher.getInstance();
+
+ protected OAuthCommand = 'github1s.commands.vscode.connectToGitLab';
+ protected detailPageCommand = 'github1s.commands.openGitLab1sAuthPage';
+ protected pageConfig = {
+ pageDescriptionLines: [
+ 'You can provide a Personal Access Token or an OAuth token to access private repositories or to increase rate limits.',
+ "Your token will only be stored locally in your browser. Don't forget to clean it while you are using a public device.",
+ ],
+ OAuthButtonText: 'Connect to GitLab',
+ createTokenLink: `${GITLAB_ORIGIN}/-/profile/personal_access_tokens?scopes=read_api&name=GitLab1s`,
+ };
+}
diff --git a/extensions/github1s/src/adapters/gitlab1s/token.ts b/extensions/github1s/src/adapters/gitlab1s/token.ts
new file mode 100644
index 000000000..19a9ea1e0
--- /dev/null
+++ b/extensions/github1s/src/adapters/gitlab1s/token.ts
@@ -0,0 +1,40 @@
+/**
+ * @file gitlab api auth token manager
+ */
+
+import { GitHubTokenManager, ValidateResult } from '../github1s/token';
+
+export class GitLabTokenManager extends GitHubTokenManager {
+ protected static instance: GitLabTokenManager | null = null;
+ public tokenStateKey = 'gitlab-oauth-token';
+
+ public static getInstance(): GitLabTokenManager {
+ if (GitLabTokenManager.instance) {
+ return GitLabTokenManager.instance;
+ }
+ return (GitLabTokenManager.instance = new GitLabTokenManager());
+ }
+
+ public async validateToken(token?: string): Promise {
+ const accessToken = token === undefined ? this.getToken() : token;
+ if (!accessToken) {
+ return Promise.resolve(null);
+ }
+ const fetchOptions: { headers: Record } =
+ accessToken?.length < 60
+ ? { headers: { 'PRIVATE-TOKEN': `${accessToken}` } }
+ : { headers: { Authorization: `Bearer ${accessToken}` } };
+ return fetch(`${GITLAB_API_PREFIX}/user`, fetchOptions)
+ .then((response) => {
+ if (response.status === 401) {
+ return null;
+ }
+ return response.json().then((data) => ({
+ username: data.username,
+ avatar_url: data.avatar_url,
+ profile_url: data.web_url,
+ }));
+ })
+ .catch(() => null);
+ }
+}
diff --git a/extensions/github1s/src/adapters/sourcegraph/data-source.ts b/extensions/github1s/src/adapters/sourcegraph/data-source.ts
index fbbd77fa9..47c24634c 100644
--- a/extensions/github1s/src/adapters/sourcegraph/data-source.ts
+++ b/extensions/github1s/src/adapters/sourcegraph/data-source.ts
@@ -34,13 +34,14 @@ import { getAllRefs } from './ref';
import { getSymbolReferences } from './reference';
import { getRepository } from './repository';
import { getTextSearchResults } from './search';
+import { decorate, memorize } from '@/helpers/func';
type SupportedPlatform = 'github' | 'gitlab' | 'bitbucket';
export class SourcegraphDataSource extends DataSource {
private static instanceMap: Map = new Map();
private refsPromiseMap: Map> = new Map();
- private repositoryPromiseMap: Map> = new Map();
+ private repositoryPromiseMap: Map> = new Map();
private fileTypeMap: Map = new Map(); // cache if path is a directory
private matchedRefsMap: Map = new Map();
private textEncoder = new TextEncoder();
@@ -102,11 +103,9 @@ export class SourcegraphDataSource extends DataSource {
async provideFile(repo: string, ref: string, path: string): Promise {
// sourcegraph api break binary files and text coding, so we use github api first here
if (this.platform === 'github') {
- try {
- return await fetch(encodeURI(`https://raw.githubusercontent.com/${repo}/${ref}/${path}`))
- .then((response) => response.arrayBuffer())
- .then((buffer) => ({ content: new Uint8Array(buffer) }));
- } catch (e) {}
+ return fetch(encodeURI(`https://raw.githubusercontent.com/${repo}/${ref}/${path}`))
+ .then((response) => (response.ok ? response.arrayBuffer() : Promise.reject({ response })))
+ .then((buffer) => ({ content: new Uint8Array(buffer) }));
}
// TODO: support binary files for other platforms
const { content } = await readFile(this.buildRepository(repo), ref, path);
@@ -120,8 +119,16 @@ export class SourcegraphDataSource extends DataSource {
return this.refsPromiseMap.get(repo)!;
}
+ @decorate(memorize)
+ private async getDefaultBranch(repo: string) {
+ return (await this.provideRepository(repo))?.defaultBranch || 'HEAD';
+ }
+
async extractRefPath(repo: string, refAndPath: string): Promise<{ ref: string; path: string }> {
- if (!refAndPath || refAndPath.match(/^HEAD(\/.*)?$/i)) {
+ if (!refAndPath) {
+ return { ref: await this.getDefaultBranch(repo), path: '' };
+ }
+ if (refAndPath.match(/^HEAD(\/.*)?$/i)) {
return { ref: 'HEAD', path: refAndPath.slice(5) };
}
if (!this.matchedRefsMap.has(repo)) {
diff --git a/extensions/github1s/src/adapters/sourcegraph/repository.ts b/extensions/github1s/src/adapters/sourcegraph/repository.ts
index 7c06604ed..14c93478f 100644
--- a/extensions/github1s/src/adapters/sourcegraph/repository.ts
+++ b/extensions/github1s/src/adapters/sourcegraph/repository.ts
@@ -10,6 +10,7 @@ const RepositoryQuery = gql`
query ($repository: String!) {
repository(name: $repository) {
name
+ isPrivate
defaultBranch {
displayName
}
@@ -17,13 +18,15 @@ const RepositoryQuery = gql`
}
`;
-export const getRepository = async (repository: string): Promise<{ name: string; defaultBranch: string } | null> => {
+export const getRepository = async (
+ repository: string
+): Promise<{ private: boolean; defaultBranch: string } | null> => {
const response = await sourcegraphClient.query({
query: RepositoryQuery,
variables: { repository },
});
const repositoryData = response.data?.repository;
return repositoryData
- ? { name: repositoryData.name, defaultBranch: repositoryData?.defaultBranch?.displayName }
+ ? { private: repositoryData.isPrivate, defaultBranch: repositoryData.defaultBranch?.displayName }
: null;
};
diff --git a/extensions/github1s/src/adapters/types.ts b/extensions/github1s/src/adapters/types.ts
index 582835a55..bdef13c89 100644
--- a/extensions/github1s/src/adapters/types.ts
+++ b/extensions/github1s/src/adapters/types.ts
@@ -126,14 +126,10 @@ export interface CodeReview {
createTime: Date;
mergeTime: Date | null;
closeTime: Date | null;
- head: {
- label: string;
- commitSha: string;
- };
- base: {
- label: string;
- commitSha: string;
- };
+ source: string;
+ target: string;
+ sourceSha?: string;
+ targetSha?: string;
avatarUrl?: string;
}
@@ -238,7 +234,10 @@ export class DataSource {
}
// optionally return changed files (if `files` exists can reduce api calls)
- provideCodeReview(repo: string, id: string): Promisable<(CodeReview & { files?: ChangedFile[] }) | null> {
+ provideCodeReview(
+ repo: string,
+ id: string
+ ): Promisable<(CodeReview & { sourceSha: string; targetSha: string; files?: ChangedFile[] }) | null> {
return null;
}
@@ -381,7 +380,7 @@ export class RouterParser {
// convert giving path to the external link (using for jumping back to origin platform)
buildExternalLink(path: string): Promisable {
- return 'https://github.com' + path;
+ return path;
}
}
diff --git a/extensions/github1s/src/changes/files.ts b/extensions/github1s/src/changes/files.ts
index bd87ecf0a..62d19665b 100644
--- a/extensions/github1s/src/changes/files.ts
+++ b/extensions/github1s/src/changes/files.ts
@@ -19,17 +19,20 @@ interface VSCodeChangedFile {
}
// get the change files of a codeReview
-export const getCodeReviewChangedFiles = async (codeReview: adapterTypes.CodeReview) => {
+export const getCodeReviewChangedFiles = async (
+ codeReview: adapterTypes.CodeReview & { sourceSha: string; targetSha: string }
+) => {
const scheme = adapterManager.getCurrentScheme();
const { repo } = await router.getState();
const baseRootUri = vscode.Uri.parse('').with({
scheme: scheme,
- authority: `${repo}+${codeReview.base.commitSha}`,
+ authority: `${repo}+${codeReview.targetSha}`,
path: '/',
});
const headRootUri = baseRootUri.with({
- authority: `${repo}+${codeReview.head.commitSha}`,
+ authority: `${repo}+${codeReview.sourceSha}`,
});
+
const repository = Repository.getInstance(scheme, repo);
const changedFiles = await repository.getCodeReviewChangedFiles(codeReview.id);
diff --git a/extensions/github1s/src/changes/quick-diff.ts b/extensions/github1s/src/changes/quick-diff.ts
index 654f57755..507b89b38 100644
--- a/extensions/github1s/src/changes/quick-diff.ts
+++ b/extensions/github1s/src/changes/quick-diff.ts
@@ -27,11 +27,11 @@ const getOriginalResourceForPull = async (uri: vscode.Uri, codeReviewId: string)
}
const codeReview = await repository.getCodeReviewItem(codeReviewId);
- if (!codeReview?.base?.commitSha) {
+ if (!codeReview?.targetSha) {
return null;
}
- const originalAuthority = `${routeState.repo}+${codeReview!.base.commitSha}`;
+ const originalAuthority = `${routeState.repo}+${codeReview!.targetSha}`;
const originalPath = changedFile.previousPath ? `/${changedFile.previousPath}` : uri.path;
return uri.with({ authority: originalAuthority, path: originalPath });
diff --git a/extensions/github1s/src/commands/blame.ts b/extensions/github1s/src/commands/blame.ts
index 344951af4..72e0e2203 100644
--- a/extensions/github1s/src/commands/blame.ts
+++ b/extensions/github1s/src/commands/blame.ts
@@ -134,7 +134,7 @@ const createFirstLineDecorationType = (blameRange: BlameRange) => {
contentText: blameRange.commit.message,
color: new vscode.ThemeColor('foreground'),
textDecoration: firstLineBeforeTextDecorationCss,
- borderColor: ageColors[blameRange.age || 10],
+ borderColor: ageColors[blameRange.age % 11 || 10],
},
after: {
...commonLineDecorationTypeOptions.after,
@@ -151,7 +151,7 @@ const createRestLinesDecorationType = (blameRange: BlameRange) => {
...commonLineDecorationTypeOptions,
before: {
...commonLineDecorationTypeOptions.before,
- borderColor: ageColors[blameRange.age || 10],
+ borderColor: ageColors[blameRange.age % 11 || 10],
},
});
};
diff --git a/extensions/github1s/src/commands/global.ts b/extensions/github1s/src/commands/global.ts
index 6d370b0f3..9f152f947 100644
--- a/extensions/github1s/src/commands/global.ts
+++ b/extensions/github1s/src/commands/global.ts
@@ -18,16 +18,6 @@ export const commandOpenOnOfficialPage = async () => {
return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(externalLink));
};
-export const commandOpenGitpod = () => {
- return router.getAuthority().then((currentAuthority) => {
- const [currentRepo] = currentAuthority.split('+');
- vscode.commands.executeCommand(
- 'vscode.open',
- vscode.Uri.parse(`https://gitpod.io/#https://github.com/${currentRepo}`)
- );
- });
-};
-
const repoPickItemButtons = [{ iconPath: new vscode.ThemeIcon('close') }];
const getRecentRepoPickItems = () =>
@@ -91,7 +81,6 @@ export const registerGlobalCommands = (context: vscode.ExtensionContext) => {
vscode.commands.registerCommand('github1s.commands.openOnBitbucket', commandOpenOnOfficialPage),
vscode.commands.registerCommand('github1s.commands.openOnNpm', commandOpenOnOfficialPage),
vscode.commands.registerCommand('github1s.commands.openOnOfficialPage', commandOpenOnOfficialPage),
- vscode.commands.registerCommand('github1s.commands.openOnGitPod', commandOpenGitpod),
vscode.commands.registerCommand('github1s.commands.openRepository', commandOpenRepository),
vscode.commands.registerCommand('github1s.commands.openOnlineEditor', commandOpenOnlineEditor),
vscode.commands.registerCommand('remoteHub.openRepository', commandOpenRepository)
diff --git a/extensions/github1s/src/global.d.ts b/extensions/github1s/src/global.d.ts
new file mode 100644
index 000000000..819440645
--- /dev/null
+++ b/extensions/github1s/src/global.d.ts
@@ -0,0 +1,4 @@
+declare const GITHUB_ORIGIN: string;
+declare const GITHUB_API_PREFIX: string;
+declare const GITLAB_ORIGIN: string;
+declare const GITLAB_API_PREFIX: string;
diff --git a/extensions/github1s/src/helpers/async.ts b/extensions/github1s/src/helpers/async.ts
index 93fa2cc15..6d37f3d72 100644
--- a/extensions/github1s/src/helpers/async.ts
+++ b/extensions/github1s/src/helpers/async.ts
@@ -3,7 +3,7 @@
*/
// below code is comes from:
-//https://github.com/microsoft/vscode/blob/a3415e669a8f3879c290af5616a8ed45dd0534af/src/vs/base/common/async.ts#L344
+// https://github.com/microsoft/vscode/blob/a3415e669a8f3879c290af5616a8ed45dd0534af/src/vs/base/common/async.ts#L344
export class Barrier {
private _isOpen: boolean;
private _promise: Promise;
diff --git a/extensions/github1s/src/helpers/context.ts b/extensions/github1s/src/helpers/context.ts
index af63b7c93..a1f8e40a5 100644
--- a/extensions/github1s/src/helpers/context.ts
+++ b/extensions/github1s/src/helpers/context.ts
@@ -35,3 +35,7 @@ export const removeRecentRepository = (name: string) => {
const newRecords = getRecentRepositories().filter((record) => record.name !== name);
return getExtensionContext().globalState.update(RECENT_REPOSITORIES, newRecords);
};
+
+export const getBrowserUrl = () => {
+ return vscode.commands.executeCommand('github1s.commands.vscode.getBrowserUrl');
+};
diff --git a/extensions/github1s/src/helpers/date.ts b/extensions/github1s/src/helpers/date.ts
index 0af117de9..0e36b4bff 100644
--- a/extensions/github1s/src/helpers/date.ts
+++ b/extensions/github1s/src/helpers/date.ts
@@ -9,3 +9,5 @@ import * as relativeTimePlugin from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTimePlugin);
export const relativeTimeTo = (date: dayjs.ConfigType) => dayjs().to(dayjs(date));
+
+export const toISOString = (date: dayjs.ConfigType) => dayjs(date).toISOString();
diff --git a/extensions/github1s/src/helpers/func.ts b/extensions/github1s/src/helpers/func.ts
index 34541d07d..2a029c1e4 100644
--- a/extensions/github1s/src/helpers/func.ts
+++ b/extensions/github1s/src/helpers/func.ts
@@ -79,3 +79,10 @@ export const memorize = any>(
return result;
};
};
+
+export const decorate = unknown>(transformer: (func: F) => F) => {
+ return (_target: T, _propertyKey: string, descriptor: PropertyDescriptor) => {
+ const originalMethod = descriptor.value;
+ descriptor.value = transformer(originalMethod as F);
+ };
+};
diff --git a/extensions/github1s/src/helpers/page.ts b/extensions/github1s/src/helpers/page.ts
index fcd606b75..4d24c7ec4 100644
--- a/extensions/github1s/src/helpers/page.ts
+++ b/extensions/github1s/src/helpers/page.ts
@@ -31,7 +31,7 @@ export const createPageHtml = (title: string, styles: string[] = [], scripts: st
-
+
${title}
${styles.map((style) => ``).join('')}