Browse Source

Splits authentication out of ServerConnection

Wraps ServerConnection to handle auth specific requests
Consolidates GK token access in ServerConnection through SubscriptionService
main
Eric Amodio 1 year ago
parent
commit
3ce8201229
10 changed files with 728 additions and 786 deletions
  1. +13
    -14
      src/container.ts
  2. +13
    -76
      src/plus/gk/authenticationConnection.ts
  3. +12
    -6
      src/plus/gk/authenticationProvider.ts
  4. +116
    -0
      src/plus/gk/serverConnection.ts
  5. +35
    -86
      src/plus/subscription/subscriptionService.ts
  6. +44
    -109
      src/plus/workspaces/workspacesApi.ts
  7. +3
    -3
      src/plus/workspaces/workspacesService.ts
  8. +1
    -1
      src/uris/uriService.ts

+ 13
- 14
src/container.ts View File

@ -20,9 +20,9 @@ import { GitLabAuthenticationProvider } from './git/remotes/gitlab';
import { RichRemoteProviderService } from './git/remotes/remoteProviderService';
import { LineHoverController } from './hovers/lineHoverController';
import type { RepositoryPathMappingProvider } from './pathMapping/repositoryPathMappingProvider';
import { AccountAuthenticationProvider } from './plus/gk/authenticationProvider';
import { ServerConnection } from './plus/gk/serverConnection';
import { IntegrationAuthenticationService } from './plus/integrationAuthentication';
import { SubscriptionAuthenticationProvider } from './plus/subscription/authenticationProvider';
import { ServerConnection } from './plus/subscription/serverConnection';
import { SubscriptionService } from './plus/subscription/subscriptionService';
import { registerAccountWebviewView } from './plus/webviews/account/registration';
import { registerFocusWebviewPanel } from './plus/webviews/focus/registration';
@ -191,13 +191,12 @@ export class Container {
this._richRemoteProviders = new RichRemoteProviderService(this);
const server = new ServerConnection(this);
this._disposables.push(server);
this._disposables.push(
(this._subscriptionAuthentication = new SubscriptionAuthenticationProvider(this, server)),
);
this._disposables.push((this._subscription = new SubscriptionService(this, previousVersion)));
this._disposables.push((this._workspaces = new WorkspacesService(this, server)));
const connection = new ServerConnection(this);
this._disposables.push(connection);
this._disposables.push((this._accountAuthentication = new AccountAuthenticationProvider(this, connection)));
this._disposables.push((this._subscription = new SubscriptionService(this, connection, previousVersion)));
this._disposables.push((this._workspaces = new WorkspacesService(this, connection)));
this._disposables.push((this._git = new GitProviderService(this)));
this._disposables.push(new GitFileSystemProvider(this));
@ -337,6 +336,11 @@ export class Container {
}
}
private _accountAuthentication: AccountAuthenticationProvider;
get accountAuthentication() {
return this._accountAuthentication;
}
private readonly _actionRunners: ActionRunners;
get actionRunners() {
return this._actionRunners;
@ -581,11 +585,6 @@ export class Container {
return this._subscription;
}
private _subscriptionAuthentication: SubscriptionAuthenticationProvider;
get subscriptionAuthentication() {
return this._subscriptionAuthentication;
}
private readonly _richRemoteProviders: RichRemoteProviderService;
get richRemoteProviders(): RichRemoteProviderService {
return this._richRemoteProviders;

src/plus/subscription/serverConnection.ts → src/plus/gk/authenticationConnection.ts View File

@ -1,67 +1,35 @@
import type { CancellationToken, Disposable, StatusBarItem } from 'vscode';
import { CancellationTokenSource, env, StatusBarAlignment, Uri, window } from 'vscode';
import { uuid } from '@env/crypto';
import type { RequestInfo, RequestInit, Response } from '@env/fetch';
import { fetch, getProxyAgent } from '@env/fetch';
import type { Response } from '@env/fetch';
import type { Container } from '../../container';
import { debug } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import type { DeferredEvent, DeferredEventExecutor } from '../../system/event';
import { promisifyDeferred } from '../../system/event';
import { Logger } from '../../system/logger';
import { getLogScope } from '../../system/logger.scope';
import type { ServerConnection } from './serverConnection';
export const AuthenticationUriPathPrefix = 'did-authenticate';
// TODO: What user-agent should we use?
const userAgent = 'Visual-Studio-Code-GitLens';
interface AccountInfo {
id: string;
accountName: string;
}
interface GraphQLRequest {
query: string;
operationName?: string;
variables?: Record<string, unknown>;
}
export class ServerConnection implements Disposable {
export class AuthenticationConnection implements Disposable {
private _cancellationSource: CancellationTokenSource | undefined;
private _deferredCodeExchanges = new Map<string, DeferredEvent<string>>();
private _pendingStates = new Map<string, string[]>();
private _statusBarItem: StatusBarItem | undefined;
constructor(private readonly container: Container) {}
constructor(
private readonly container: Container,
private readonly connection: ServerConnection,
) {}
dispose() {}
@memoize()
private get baseApiUri(): Uri {
if (this.container.env === 'staging') {
return Uri.parse('https://stagingapi.gitkraken.com');
}
if (this.container.env === 'dev') {
return Uri.parse('https://devapi.gitkraken.com');
}
return Uri.parse('https://api.gitkraken.com');
}
@memoize()
private get baseAccountUri(): Uri {
if (this.container.env === 'staging') {
return Uri.parse('https://stagingapp.gitkraken.com');
}
if (this.container.env === 'dev') {
return Uri.parse('https://devapp.gitkraken.com');
}
return Uri.parse('https://app.gitkraken.com');
}
abort(): Promise<void> {
if (this._cancellationSource == null) return Promise.resolve();
@ -76,13 +44,11 @@ export class ServerConnection implements Disposable {
let rsp: Response;
try {
rsp = await fetch(Uri.joinPath(this.baseApiUri, 'user').toString(), {
agent: getProxyAgent(),
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': userAgent,
},
});
rsp = await this.connection.fetch(
Uri.joinPath(this.connection.baseApiUri, 'user').toString(),
undefined,
token,
);
} catch (ex) {
Logger.error(ex, scope);
throw ex;
@ -112,7 +78,7 @@ export class ServerConnection implements Disposable {
),
);
const uri = Uri.joinPath(this.baseAccountUri, 'register').with({
const uri = Uri.joinPath(this.connection.baseAccountUri, 'register').with({
query: `${
scopes.includes('gitlens') ? 'referrer=gitlens&' : ''
}pass-token=true&return-url=${encodeURIComponent(callbackUri.toString())}`,
@ -252,33 +218,4 @@ export class ServerConnection implements Disposable {
this._statusBarItem = undefined;
}
}
async fetchGraphql(data: GraphQLRequest, token: string, init?: RequestInit) {
return this.fetchCore(Uri.joinPath(this.baseAccountUri, 'api/projects/graphql').toString(), token, {
method: 'POST',
body: JSON.stringify(data),
...init,
});
}
private async fetchCore(url: RequestInfo, token: string, init?: RequestInit): Promise<Response> {
const scope = getLogScope();
try {
const options = {
agent: getProxyAgent(),
...init,
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': userAgent,
'Content-Type': 'application/json',
...init?.headers,
},
};
return await fetch(url, options);
} catch (ex) {
Logger.error(ex, scope);
throw ex;
}
}
}

src/plus/subscription/authenticationProvider.ts → src/plus/gk/authenticationProvider.ts View File

@ -9,6 +9,7 @@ import type { Container, Environment } from '../../container';
import { debug } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import { getLogScope, setLogScopeExit } from '../../system/logger.scope';
import { AuthenticationConnection } from './authenticationConnection';
import type { ServerConnection } from './serverConnection';
interface StoredSession {
@ -23,25 +24,30 @@ interface StoredSession {
}
export const authenticationProviderId = 'gitlens+';
export const authenticationProviderScopes = ['gitlens'];
const authenticationLabel = 'GitKraken: GitLens';
export class SubscriptionAuthenticationProvider implements AuthenticationProvider, Disposable {
export class AccountAuthenticationProvider implements AuthenticationProvider, Disposable {
private _onDidChangeSessions = new EventEmitter<AuthenticationProviderAuthenticationSessionsChangeEvent>();
get onDidChangeSessions() {
return this._onDidChangeSessions.event;
}
private readonly _disposable: Disposable;
private readonly _authConnection: AuthenticationConnection;
private _sessionsPromise: Promise<AuthenticationSession[]>;
constructor(
private readonly container: Container,
private readonly server: ServerConnection,
connection: ServerConnection,
) {
this._authConnection = new AuthenticationConnection(container, connection);
// Contains the current state of the sessions we have available.
this._sessionsPromise = this.getSessionsFromStorage();
this._disposable = Disposable.from(
this._authConnection,
authentication.registerAuthenticationProvider(authenticationProviderId, authenticationLabel, this, {
supportsMultipleAccounts: false,
}),
@ -58,7 +64,7 @@ export class SubscriptionAuthenticationProvider implements AuthenticationProvide
}
abort(): Promise<void> {
return this.server.abort();
return this._authConnection.abort();
}
@debug()
@ -70,7 +76,7 @@ export class SubscriptionAuthenticationProvider implements AuthenticationProvide
const scopesKey = getScopesKey(scopes);
try {
const token = await this.server.login(scopes, scopesKey);
const token = await this._authConnection.login(scopes, scopesKey);
const session = await this.createSessionForToken(token, scopes);
const sessions = await this._sessionsPromise;
@ -200,7 +206,7 @@ export class SubscriptionAuthenticationProvider implements AuthenticationProvide
}
private async createSessionForToken(token: string, scopes: string[]): Promise<AuthenticationSession> {
const userInfo = await this.server.getAccountInfo(token);
const userInfo = await this._authConnection.getAccountInfo(token);
return {
id: uuid(),
accessToken: token,
@ -238,7 +244,7 @@ export class SubscriptionAuthenticationProvider implements AuthenticationProvide
let userInfo: { id: string; accountName: string } | undefined;
if (session.account == null) {
try {
userInfo = await this.server.getAccountInfo(session.accessToken);
userInfo = await this._authConnection.getAccountInfo(session.accessToken);
Logger.debug(`Verified session with scopes=${scopesKey}`);
} catch (ex) {
// Remove sessions that return unauthorized response

+ 116
- 0
src/plus/gk/serverConnection.ts View File

@ -0,0 +1,116 @@
import type { Disposable } from 'vscode';
import { Uri } from 'vscode';
import type { RequestInfo, RequestInit, Response } from '@env/fetch';
import { fetch as _fetch, getProxyAgent } from '@env/fetch';
import type { Container } from '../../container';
import { memoize } from '../../system/decorators/memoize';
import { Logger } from '../../system/logger';
import { getLogScope } from '../../system/logger.scope';
export class ServerConnection implements Disposable {
constructor(private readonly container: Container) {}
dispose() {}
@memoize()
get baseApiUri(): Uri {
if (this.container.env === 'staging') {
return Uri.parse('https://stagingapi.gitkraken.com');
}
if (this.container.env === 'dev') {
return Uri.parse('https://devapi.gitkraken.com');
}
return Uri.parse('https://api.gitkraken.com');
}
@memoize()
get baseAccountUri(): Uri {
if (this.container.env === 'staging') {
return Uri.parse('https://stagingapp.gitkraken.com');
}
if (this.container.env === 'dev') {
return Uri.parse('https://devapp.gitkraken.com');
}
return Uri.parse('https://app.gitkraken.com');
}
@memoize()
get baseGkApiUri(): Uri {
if (this.container.env === 'staging') {
return Uri.parse('https://staging-api.gitkraken.dev');
}
if (this.container.env === 'dev') {
return Uri.parse('https://dev-api.gitkraken.dev');
}
return Uri.parse('https://api.gitkraken.dev');
}
@memoize()
get baseSiteUri(): Uri {
const { env } = this.container;
if (env === 'staging') {
return Uri.parse('https://staging.gitkraken.com');
}
if (env === 'dev') {
return Uri.parse('https://dev.gitkraken.com');
}
return Uri.parse('https://gitkraken.com');
}
@memoize()
get userAgent(): string {
// TODO@eamodio figure out standardized format/structure for our user agents
return 'Visual-Studio-Code-GitLens';
}
async fetch(url: RequestInfo, init?: RequestInit, token?: string): Promise<Response> {
const scope = getLogScope();
try {
token ??= await this.getAccessToken();
const options = {
agent: getProxyAgent(),
...init,
headers: {
Authorization: `Bearer ${token}`,
'User-Agent': this.userAgent,
'Content-Type': 'application/json',
...init?.headers,
},
};
return await _fetch(url, options);
} catch (ex) {
Logger.error(ex, scope);
throw ex;
}
}
async fetchGraphQL(url: RequestInfo, request: GraphQLRequest, init?: RequestInit) {
return this.fetch(url, {
method: 'POST',
...init,
body: JSON.stringify(request),
});
}
private async getAccessToken() {
const session = await this.container.subscription.getAuthenticationSession();
if (session != null) return session.accessToken;
throw new Error('Authentication required');
}
}
export interface GraphQLRequest {
query: string;
operationName?: string;
variables?: Record<string, unknown>;
}

+ 35
- 86
src/plus/subscription/subscriptionService.ts View File

@ -20,7 +20,6 @@ import {
Uri,
window,
} from 'vscode';
import { fetch, getProxyAgent } from '@env/fetch';
import { getPlatform } from '@env/platform';
import type { CoreColors } from '../../constants';
import { Commands } from '../../constants';
@ -48,7 +47,6 @@ import { setContext } from '../../system/context';
import { createFromDateDelta } from '../../system/date';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import type { Deferrable } from '../../system/function';
import { debounce, once } from '../../system/function';
import { Logger } from '../../system/logger';
@ -57,12 +55,10 @@ import { flatten } from '../../system/object';
import { pluralize } from '../../system/string';
import { openWalkthrough } from '../../system/utils';
import { satisfies } from '../../system/version';
import { authenticationProviderId } from './authenticationProvider';
import { authenticationProviderId, authenticationProviderScopes } from '../gk/authenticationProvider';
import type { ServerConnection } from '../gk/serverConnection';
import { ensurePlusFeaturesEnabled } from './utils';
// TODO: What user-agent should we use?
const userAgent = 'Visual-Studio-Code-GitLens';
export interface SubscriptionChangeEvent {
readonly current: Subscription;
readonly previous: Subscription;
@ -70,8 +66,6 @@ export interface SubscriptionChangeEvent {
}
export class SubscriptionService implements Disposable {
private static authenticationScopes = ['gitlens'];
private _onDidChange = new EventEmitter<SubscriptionChangeEvent>();
get onDidChange(): Event<SubscriptionChangeEvent> {
return this._onDidChange.event;
@ -84,11 +78,12 @@ export class SubscriptionService implements Disposable {
constructor(
private readonly container: Container,
private readonly connection: ServerConnection,
previousVersion: string | undefined,
) {
this._disposable = Disposable.from(
once(container.onReady)(this.onReady, this),
this.container.subscriptionAuthentication.onDidChangeSessions(
this.container.accountAuthentication.onDidChangeSessions(
e => setTimeout(() => this.onAuthenticationChanged(e), 0),
this,
),
@ -139,48 +134,6 @@ export class SubscriptionService implements Disposable {
void this.validate();
}
@memoize()
private get baseApiUri(): Uri {
const { env } = this.container;
if (env === 'staging') {
return Uri.parse('https://stagingapi.gitkraken.com');
}
if (env === 'dev') {
return Uri.parse('https://devapi.gitkraken.com');
}
return Uri.parse('https://api.gitkraken.com');
}
@memoize()
private get baseAccountUri(): Uri {
const { env } = this.container;
if (env === 'staging') {
return Uri.parse('https://stagingapp.gitkraken.com');
}
if (env === 'dev') {
return Uri.parse('https://devapp.gitkraken.com');
}
return Uri.parse('https://app.gitkraken.com');
}
@memoize()
private get baseSiteUri(): Uri {
const { env } = this.container;
if (env === 'staging') {
return Uri.parse('https://staging.gitkraken.com');
}
if (env === 'dev') {
return Uri.parse('https://dev.gitkraken.com');
}
return Uri.parse('https://gitkraken.com');
}
private _etag: number = 0;
get etag(): number {
return this._etag;
@ -222,6 +175,10 @@ export class SubscriptionService implements Disposable {
];
}
async getAuthenticationSession(createIfNeeded: boolean = false): Promise<AuthenticationSession | undefined> {
return this.ensureSession(createIfNeeded);
}
async getSubscription(cached = false): Promise<Subscription> {
const promise = this.ensureSession(false);
if (!cached) {
@ -255,7 +212,7 @@ export class SubscriptionService implements Disposable {
if (!(await ensurePlusFeaturesEnabled())) return false;
// Abort any waiting authentication to ensure we can start a new flow
await this.container.subscriptionAuthentication.abort();
await this.container.accountAuthentication.abort();
void this.showAccountView();
const session = await this.ensureSession(true);
@ -324,17 +281,15 @@ export class SubscriptionService implements Disposable {
this._validationTimer = undefined;
}
await this.container.subscriptionAuthentication.abort();
await this.container.accountAuthentication.abort();
this._sessionPromise = undefined;
if (this._session != null) {
void this.container.subscriptionAuthentication.removeSession(this._session.id);
void this.container.accountAuthentication.removeSession(this._session.id);
this._session = undefined;
} else {
// Even if we don't have a session, make sure to remove any other matching sessions
void this.container.subscriptionAuthentication.removeSessionsByScopes(
SubscriptionService.authenticationScopes,
);
void this.container.accountAuthentication.removeSessionsByScopes(authenticationProviderScopes);
}
if (reset && this.container.debugging) {
@ -369,7 +324,7 @@ export class SubscriptionService implements Disposable {
@log()
manage(): void {
void env.openExternal(this.baseAccountUri);
void env.openExternal(this.connection.baseAccountUri);
}
@log()
@ -380,7 +335,9 @@ export class SubscriptionService implements Disposable {
this.showPlans();
} else {
void env.openExternal(
Uri.joinPath(this.baseAccountUri, 'subscription').with({ query: 'product=gitlens&license=PRO' }),
Uri.joinPath(this.connection.baseAccountUri, 'subscription').with({
query: 'product=gitlens&license=PRO',
}),
);
}
await this.showAccountView();
@ -399,16 +356,14 @@ export class SubscriptionService implements Disposable {
if (session == null) return false;
try {
const rsp = await fetch(Uri.joinPath(this.baseApiUri, 'resend-email').toString(), {
method: 'POST',
agent: getProxyAgent(),
headers: {
Authorization: `Bearer ${session.accessToken}`,
'User-Agent': userAgent,
'Content-Type': 'application/json',
const rsp = await this.connection.fetch(
Uri.joinPath(this.connection.baseApiUri, 'resend-email').toString(),
{
method: 'POST',
body: JSON.stringify({ id: session.account.id }),
},
body: JSON.stringify({ id: session.account.id }),
});
session.accessToken,
);
if (!rsp.ok) {
debugger;
@ -455,7 +410,7 @@ export class SubscriptionService implements Disposable {
}
private showPlans(): void {
void env.openExternal(Uri.joinPath(this.baseSiteUri, 'gitlens/pricing'));
void env.openExternal(Uri.joinPath(this.connection.baseSiteUri, 'gitlens/pricing'));
}
@gate()
@ -595,16 +550,14 @@ export class SubscriptionService implements Disposable {
previewExpiresOn: this._subscription.previewTrial?.expiresOn,
};
const rsp = await fetch(Uri.joinPath(this.baseApiUri, 'gitlens/checkin').toString(), {
method: 'POST',
agent: getProxyAgent(),
headers: {
Authorization: `Bearer ${session.accessToken}`,
'User-Agent': userAgent,
'Content-Type': 'application/json',
const rsp = await this.connection.fetch(
Uri.joinPath(this.connection.baseApiUri, 'gitlens/checkin').toString(),
{
method: 'POST',
body: JSON.stringify(checkInData),
},
body: JSON.stringify(checkInData),
});
session.accessToken,
);
if (!rsp.ok) {
throw new AccountValidationError('Unable to validate account', undefined, rsp.status, rsp.statusText);
@ -766,14 +719,10 @@ export class SubscriptionService implements Disposable {
let session: AuthenticationSession | null | undefined;
try {
session = await authentication.getSession(
authenticationProviderId,
SubscriptionService.authenticationScopes,
{
createIfNone: createIfNeeded,
silent: !createIfNeeded,
},
);
session = await authentication.getSession(authenticationProviderId, authenticationProviderScopes, {
createIfNone: createIfNeeded,
silent: !createIfNeeded,
});
} catch (ex) {
session = null;

+ 44
- 109
src/plus/workspaces/workspacesApi.ts View File

@ -1,6 +1,8 @@
import { Uri } from 'vscode';
import type { RequestInit } from '@env/fetch';
import type { Container } from '../../container';
import { Logger } from '../../system/logger';
import type { ServerConnection } from '../subscription/serverConnection';
import type { GraphQLRequest, ServerConnection } from '../gk/serverConnection';
import type {
AddRepositoriesToWorkspaceResponse,
AddWorkspaceRepoDescriptor,
@ -19,20 +21,9 @@ import { CloudWorkspaceProviderInputType, defaultWorkspaceCount, defaultWorkspac
export class WorkspacesApi {
constructor(
private readonly container: Container,
private readonly server: ServerConnection,
private readonly connection: ServerConnection,
) {}
private async getAccessToken() {
// TODO: should probably get scopes from somewhere
const sessions = await this.container.subscriptionAuthentication.getSessions(['gitlens']);
if (!sessions.length) {
return;
}
const session = sessions[0];
return session.accessToken;
}
async getWorkspace(
id: string,
options?: {
@ -41,11 +32,6 @@ export class WorkspacesApi {
repoPage?: number;
},
): Promise<WorkspaceResponse | undefined> {
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
let repoQuery: string | undefined;
if (options?.includeRepositories) {
let repoQueryParams = `(first: ${options?.repoCount ?? defaultWorkspaceRepoCount}`;
@ -96,12 +82,7 @@ export class WorkspacesApi {
}
`;
const rsp = await this.server.fetchGraphql(
{
query: query,
},
accessToken,
);
const rsp = await this.fetch({ query: query });
if (!rsp.ok) {
Logger.error(undefined, `Getting workspace failed: (${rsp.status}) ${rsp.statusText}`);
@ -122,11 +103,6 @@ export class WorkspacesApi {
repoCount?: number;
repoPage?: number;
}): Promise<WorkspacesResponse | undefined> {
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
let repoQuery: string | undefined;
if (options?.includeRepositories) {
let repoQueryParams = `(first: ${options?.repoCount ?? defaultWorkspaceRepoCount}`;
@ -206,12 +182,7 @@ export class WorkspacesApi {
query += '}';
const rsp = await this.server.fetchGraphql(
{
query: query,
},
accessToken,
);
const rsp = await this.fetch({ query: query });
if (!rsp.ok) {
Logger.error(undefined, `Getting workspaces failed: (${rsp.status}) ${rsp.statusText}`);
@ -254,11 +225,6 @@ export class WorkspacesApi {
page?: number;
},
): Promise<WorkspaceRepositoriesResponse | undefined> {
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
let queryparams = `(first: ${options?.count ?? defaultWorkspaceRepoCount}`;
if (options?.cursor) {
queryparams += `, after: "${options.cursor}"`;
@ -267,12 +233,11 @@ export class WorkspacesApi {
}
queryparams += ')';
const rsp = await this.server.fetchGraphql(
{
query: `
query getWorkspaceRepos {
project (id: "${workspaceId}") {
provider_data {
const rsp = await this.fetch({
query: `
query getWorkspaceRepos {
project (id: "${workspaceId}") {
provider_data {
repositories ${queryparams} {
total_count
page_info {
@ -291,12 +256,10 @@ export class WorkspacesApi {
}
}
}
}
}
}
}
`,
},
accessToken,
);
});
if (!rsp.ok) {
Logger.error(undefined, `Getting workspace repos failed: (${rsp.status}) ${rsp.statusText}`);
@ -337,15 +300,9 @@ export class WorkspacesApi {
return;
}
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
const rsp = await this.server.fetchGraphql(
{
query: `
mutation createWorkspace {
const rsp = await this.fetch({
query: `
mutation createWorkspace {
create_project(
input: {
type: GK_PROJECT
@ -369,11 +326,9 @@ export class WorkspacesApi {
azure_project
repo_relation
}
}
}
`,
},
accessToken,
);
});
if (!rsp.ok) {
Logger.error(undefined, `Creating workspace failed: (${rsp.status}) ${rsp.statusText}`);
@ -386,25 +341,17 @@ export class WorkspacesApi {
}
async deleteWorkspace(workspaceId: string): Promise<DeleteWorkspaceResponse | undefined> {
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
const rsp = await this.server.fetchGraphql(
{
query: `
mutation deleteWorkspace {
const rsp = await this.fetch({
query: `
mutation deleteWorkspace {
delete_project(
id: "${workspaceId}"
) {
id
}
}
}
`,
},
accessToken,
);
});
if (!rsp.ok) {
Logger.error(undefined, `Deleting workspace failed: (${rsp.status}) ${rsp.statusText}`);
@ -427,14 +374,7 @@ export class WorkspacesApi {
workspaceId: string,
repos: AddWorkspaceRepoDescriptor[],
): Promise<AddRepositoriesToWorkspaceResponse | undefined> {
if (repos.length === 0) {
return;
}
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
if (repos.length === 0) return;
let reposQuery = '[';
reposQuery += repos.map(r => `{ provider_organization_id: "${r.owner}", name: "${r.repoName}" }`).join(',');
@ -456,10 +396,9 @@ export class WorkspacesApi {
)
.join(',');
const rsp = await this.server.fetchGraphql(
{
query: `
mutation addReposToWorkspace {
const rsp = await this.fetch({
query: `
mutation addReposToWorkspace {
add_repositories_to_project(
input: {
project_id: "${workspaceId}",
@ -471,11 +410,9 @@ export class WorkspacesApi {
${reposReturnQuery}
}
}
}
}
`,
},
accessToken,
);
});
if (!rsp.ok) {
Logger.error(undefined, `Adding repositories to workspace failed: (${rsp.status}) ${rsp.statusText}`);
@ -500,23 +437,15 @@ export class WorkspacesApi {
workspaceId: string,
repos: RemoveWorkspaceRepoDescriptor[],
): Promise<RemoveRepositoriesFromWorkspaceResponse | undefined> {
if (repos.length === 0) {
return;
}
const accessToken = await this.getAccessToken();
if (accessToken == null) {
return;
}
if (repos.length === 0) return;
let reposQuery = '[';
reposQuery += repos.map(r => `{ provider_organization_id: "${r.owner}", name: "${r.repoName}" }`).join(',');
reposQuery += ']';
const rsp = await this.server.fetchGraphql(
{
query: `
mutation removeReposFromWorkspace {
const rsp = await this.fetch({
query: `
mutation removeReposFromWorkspace {
remove_repositories_from_project(
input: {
project_id: "${workspaceId}",
@ -525,11 +454,9 @@ export class WorkspacesApi {
) {
id
}
}
}
`,
},
accessToken,
);
});
if (!rsp.ok) {
Logger.error(undefined, `Removing repositories from workspace failed: (${rsp.status}) ${rsp.statusText}`);
@ -549,4 +476,12 @@ export class WorkspacesApi {
return json;
}
private async fetch(request: GraphQLRequest, init?: RequestInit) {
return this.connection.fetchGraphQL(
Uri.joinPath(this.connection.baseApiUri, 'api/projects/graphql').toString(),
request,
init,
);
}
}

+ 3
- 3
src/plus/workspaces/workspacesService.ts View File

@ -9,7 +9,7 @@ import { showRepositoriesPicker } from '../../quickpicks/repositoryPicker';
import { SubscriptionState } from '../../subscription';
import { normalizePath } from '../../system/path';
import { openWorkspace, OpenWorkspaceLocation } from '../../system/utils';
import type { ServerConnection } from '../subscription/serverConnection';
import type { ServerConnection } from '../gk/serverConnection';
import type { SubscriptionChangeEvent } from '../subscription/subscriptionService';
import type {
AddWorkspaceRepoDescriptor,
@ -56,9 +56,9 @@ export class WorkspacesService implements Disposable {
constructor(
private readonly container: Container,
private readonly server: ServerConnection,
private readonly connection: ServerConnection,
) {
this._workspacesApi = new WorkspacesApi(this.container, this.server);
this._workspacesApi = new WorkspacesApi(this.container, this.connection);
this._workspacesPathProvider = getSupportedWorkspacesPathMappingProvider();
this._currentWorkspaceId = workspace.getConfiguration('gitkraken')?.get<string>('workspaceId');
this._currentWorkspaceAutoAddSetting =

+ 1
- 1
src/uris/uriService.ts View File

@ -1,7 +1,7 @@
import type { Disposable, Event, Uri, UriHandler } from 'vscode';
import { EventEmitter, window } from 'vscode';
import type { Container } from '../container';
import { AuthenticationUriPathPrefix } from '../plus/subscription/serverConnection';
import { AuthenticationUriPathPrefix } from '../plus/gk/authenticationConnection';
import { log } from '../system/decorators/log';
// This service is in charge of registering a URI handler and handling/emitting URI events received by GitLens.

Loading…
Cancel
Save