diff --git a/src/annotations/lineAnnotationController.ts b/src/annotations/lineAnnotationController.ts index 97fa4e2..9e63373 100644 --- a/src/annotations/lineAnnotationController.ts +++ b/src/annotations/lineAnnotationController.ts @@ -12,7 +12,7 @@ import type { Container } from '../container'; import { CommitFormatter } from '../git/formatters/commitFormatter'; import type { GitCommit } from '../git/models/commit'; import type { PullRequest } from '../git/models/pullRequest'; -import { Authentication } from '../git/remotes/provider'; +import { RichRemoteProviders } from '../git/remotes/remoteProviderConnections'; import type { LogScope } from '../logger'; import { Logger } from '../logger'; import { debug, getLogScope, log } from '../system/decorators/log'; @@ -44,7 +44,7 @@ export class LineAnnotationController implements Disposable { once(container.onReady)(this.onReady, this), configuration.onDidChange(this.onConfigurationChanged, this), container.fileAnnotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this), - Authentication.onDidChange(() => void this.refresh(window.activeTextEditor)), + RichRemoteProviders.onDidChangeConnectionState(() => void this.refresh(window.activeTextEditor)), ); } diff --git a/src/env/node/git/localGitProvider.ts b/src/env/node/git/localGitProvider.ts index 57175c2..4511525 100644 --- a/src/env/node/git/localGitProvider.ts +++ b/src/env/node/git/localGitProvider.ts @@ -93,7 +93,7 @@ import { GitTagParser } from '../../../git/parsers/tagParser'; import { GitTreeParser } from '../../../git/parsers/treeParser'; import { GitWorktreeParser } from '../../../git/parsers/worktreeParser'; import type { RemoteProviders } from '../../../git/remotes/factory'; -import { RemoteProviderFactory } from '../../../git/remotes/factory'; +import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../git/remotes/factory'; import type { RemoteProvider, RichRemoteProvider } from '../../../git/remotes/provider'; import { RemoteResourceType } from '../../../git/remotes/provider'; import { SearchPattern } from '../../../git/search'; @@ -3634,11 +3634,11 @@ export class LocalGitProvider implements GitProvider, Disposable { ): Promise[]> { if (repoPath == null) return []; - const providers = options?.providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null)); + const providers = options?.providers ?? loadRemoteProviders(configuration.get('remotes', null)); try { const data = await this.git.remote(repoPath); - const remotes = GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providers)); + const remotes = GitRemoteParser.parse(data, repoPath, getRemoteProviderMatcher(this.container, providers)); if (remotes == null) return []; if (options?.sort) { diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index d65f795..0dc3830 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -68,7 +68,7 @@ import type { GitUser } from './models/user'; import type { GitWorktree } from './models/worktree'; import type { RemoteProviders } from './remotes/factory'; import type { RemoteProvider, RichRemoteProvider } from './remotes/provider'; -import { Authentication } from './remotes/provider'; +import { RichRemoteProviders } from './remotes/remoteProviderConnections'; import type { SearchPattern } from './search'; const maxDefaultBranchWeight = 100; @@ -154,7 +154,7 @@ export class GitProviderService implements Disposable { window.onDidChangeWindowState(this.onWindowStateChanged, this), workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), configuration.onDidChange(this.onConfigurationChanged, this), - Authentication.onDidChange(e => { + RichRemoteProviders.onDidChangeConnectionState(e => { if (e.reason === 'connected') { resetAvatarCache('failed'); } diff --git a/src/git/models/repository.ts b/src/git/models/repository.ts index c2a0cb4..a87220b 100644 --- a/src/git/models/repository.ts +++ b/src/git/models/repository.ts @@ -21,7 +21,7 @@ import { basename, normalizePath } from '../../system/path'; import { runGitCommandInTerminal } from '../../terminal'; import type { GitProviderDescriptor } from '../gitProvider'; import type { RemoteProviders } from '../remotes/factory'; -import { RemoteProviderFactory } from '../remotes/factory'; +import { loadRemoteProviders } from '../remotes/factory'; import { RichRemoteProvider } from '../remotes/provider'; import type { SearchPattern } from '../search'; import type { BranchSortOptions, GitBranch } from './branch'; @@ -284,9 +284,7 @@ export class Repository implements Disposable { private onConfigurationChanged(e?: ConfigurationChangeEvent) { if (configuration.changed(e, 'remotes', this.folder?.uri)) { - this._providers = RemoteProviderFactory.loadProviders( - configuration.get('remotes', this.folder?.uri ?? null), - ); + this._providers = loadRemoteProviders(configuration.get('remotes', this.folder?.uri ?? null)); if (e != null) { this.resetCaches('remotes'); @@ -577,7 +575,7 @@ export class Repository implements Disposable { if (this._remotes == null) { if (this._providers == null) { const remotesCfg = configuration.get('remotes', this.folder?.uri ?? null); - this._providers = RemoteProviderFactory.loadProviders(remotesCfg); + this._providers = loadRemoteProviders(remotesCfg); } // Since we are caching the results, always sort diff --git a/src/git/remotes/factory.ts b/src/git/remotes/factory.ts index 56692b3..76cedac 100644 --- a/src/git/remotes/factory.ts +++ b/src/git/remotes/factory.ts @@ -1,5 +1,6 @@ import type { RemotesConfig } from '../../configuration'; import { CustomRemoteType } from '../../configuration'; +import type { Container } from '../../container'; import { Logger } from '../../logger'; import { AzureDevOpsRemote } from './azure-devops'; import { BitbucketRemote } from './bitbucket'; @@ -12,156 +13,165 @@ import { GitLabRemote } from './gitlab'; import { GoogleSourceRemote } from './google-source'; import type { RemoteProvider } from './provider'; -// export { RemoteProvider, RichRemoteProvider }; export type RemoteProviders = { custom: boolean; matcher: string | RegExp; - creator: (domain: string, path: string) => RemoteProvider; + creator: (container: Container, domain: string, path: string) => RemoteProvider; }[]; const builtInProviders: RemoteProviders = [ { custom: false, matcher: 'bitbucket.org', - creator: (domain: string, path: string) => new BitbucketRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new BitbucketRemote(domain, path), }, { custom: false, matcher: 'github.com', - creator: (domain: string, path: string) => new GitHubRemote(domain, path), + creator: (container: Container, domain: string, path: string) => new GitHubRemote(container, domain, path), }, { custom: false, matcher: 'gitlab.com', - creator: (domain: string, path: string) => new GitLabRemote(domain, path), + creator: (container: Container, domain: string, path: string) => new GitLabRemote(container, domain, path), }, { custom: false, matcher: /\bdev\.azure\.com$/i, - creator: (domain: string, path: string) => new AzureDevOpsRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new AzureDevOpsRemote(domain, path), }, { custom: true, matcher: /^(.+\/(?:bitbucket|stash))\/scm\/(.+)$/i, - creator: (domain: string, path: string) => new BitbucketServerRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new BitbucketServerRemote(domain, path), }, { custom: false, matcher: /\bgitlab\b/i, - creator: (domain: string, path: string) => new GitLabRemote(domain, path), + creator: (container: Container, domain: string, path: string) => new GitLabRemote(container, domain, path), }, { custom: false, matcher: /\bvisualstudio\.com$/i, - creator: (domain: string, path: string) => new AzureDevOpsRemote(domain, path, undefined, undefined, true), + creator: (_container: Container, domain: string, path: string) => + new AzureDevOpsRemote(domain, path, undefined, undefined, true), }, { custom: false, matcher: /\bgitea\b/i, - creator: (domain: string, path: string) => new GiteaRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new GiteaRemote(domain, path), }, { custom: false, matcher: /\bgerrithub\.io$/i, - creator: (domain: string, path: string) => new GerritRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new GerritRemote(domain, path), }, { custom: false, matcher: /\bgooglesource\.com$/i, - creator: (domain: string, path: string) => new GoogleSourceRemote(domain, path), + creator: (_container: Container, domain: string, path: string) => new GoogleSourceRemote(domain, path), }, ]; -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class RemoteProviderFactory { - static factory( - providers: RemoteProviders, - ): (url: string, domain: string, path: string) => RemoteProvider | undefined { - return (url: string, domain: string, path: string) => this.create(providers, url, domain, path); - } - - static create(providers: RemoteProviders, url: string, domain: string, path: string): RemoteProvider | undefined { - try { - const key = domain.toLowerCase(); - for (const { custom, matcher, creator } of providers) { - if (typeof matcher === 'string') { - if (matcher === key) return creator(domain, path); - - continue; - } +export function loadRemoteProviders(cfg: RemotesConfig[] | null | undefined): RemoteProviders { + const providers: RemoteProviders = []; - if (matcher.test(key)) return creator(domain, path); - if (!custom) continue; + if (cfg?.length) { + for (const rc of cfg) { + const providerCreator = getCustomProviderCreator(rc); + if (providerCreator == null) continue; - const match = matcher.exec(url); - if (match != null) { - return creator(match[1], match[2]); - } + let matcher: string | RegExp | undefined; + try { + matcher = rc.regex ? new RegExp(rc.regex, 'i') : rc.domain?.toLowerCase(); + if (matcher == null) throw new Error('No matcher found'); + } catch (ex) { + Logger.error(ex, `Loading remote provider '${rc.name ?? ''}' failed`); } - return undefined; - } catch (ex) { - Logger.error(ex, 'RemoteProviderFactory'); - return undefined; + providers.push({ + custom: true, + matcher: matcher!, + creator: providerCreator, + }); } } - static loadProviders(cfg: RemotesConfig[] | null | undefined): RemoteProviders { - const providers: RemoteProviders = []; + providers.push(...builtInProviders); - if (cfg != null && cfg.length > 0) { - for (const rc of cfg) { - const provider = this.getCustomProvider(rc); - if (provider == null) continue; + return providers; +} - let matcher: string | RegExp | undefined; - try { - matcher = rc.regex ? new RegExp(rc.regex, 'i') : rc.domain?.toLowerCase(); - if (matcher == null) throw new Error('No matcher found'); - } catch (ex) { - Logger.error(ex, `Loading remote provider '${rc.name ?? ''}' failed`); - } +function getCustomProviderCreator(cfg: RemotesConfig) { + switch (cfg.type) { + case CustomRemoteType.AzureDevOps: + return (_container: Container, domain: string, path: string) => + new AzureDevOpsRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.Bitbucket: + return (_container: Container, domain: string, path: string) => + new BitbucketRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.BitbucketServer: + return (_container: Container, domain: string, path: string) => + new BitbucketServerRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.Custom: + return (_container: Container, domain: string, path: string) => + new CustomRemote(domain, path, cfg.urls!, cfg.protocol, cfg.name); + case CustomRemoteType.Gerrit: + return (_container: Container, domain: string, path: string) => + new GerritRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.GoogleSource: + return (_container: Container, domain: string, path: string) => + new GoogleSourceRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.Gitea: + return (_container: Container, domain: string, path: string) => + new GiteaRemote(domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.GitHub: + return (container: Container, domain: string, path: string) => + new GitHubRemote(container, domain, path, cfg.protocol, cfg.name, true); + case CustomRemoteType.GitLab: + return (container: Container, domain: string, path: string) => + new GitLabRemote(container, domain, path, cfg.protocol, cfg.name, true); + default: + return undefined; + } +} - providers.push({ - custom: true, - matcher: matcher!, - creator: provider, - }); - } - } +export function getRemoteProviderMatcher( + container: Container, + providers: RemoteProviders, +): (url: string, domain: string, path: string) => RemoteProvider | undefined { + return (url: string, domain: string, path: string) => + createBestRemoteProvider(container, providers, url, domain, path); +} - providers.push(...builtInProviders); +function createBestRemoteProvider( + container: Container, + providers: RemoteProviders, + url: string, + domain: string, + path: string, +): RemoteProvider | undefined { + try { + const key = domain.toLowerCase(); + for (const { custom, matcher, creator } of providers) { + if (typeof matcher === 'string') { + if (matcher === key) return creator(container, domain, path); - return providers; - } + continue; + } + + if (matcher.test(key)) return creator(container, domain, path); + if (!custom) continue; - private static getCustomProvider(cfg: RemotesConfig) { - switch (cfg.type) { - case CustomRemoteType.AzureDevOps: - return (domain: string, path: string) => - new AzureDevOpsRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.Bitbucket: - return (domain: string, path: string) => - new BitbucketRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.BitbucketServer: - return (domain: string, path: string) => - new BitbucketServerRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.Custom: - return (domain: string, path: string) => - new CustomRemote(domain, path, cfg.urls!, cfg.protocol, cfg.name); - case CustomRemoteType.Gerrit: - return (domain: string, path: string) => new GerritRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.GoogleSource: - return (domain: string, path: string) => - new GoogleSourceRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.Gitea: - return (domain: string, path: string) => new GiteaRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.GitHub: - return (domain: string, path: string) => new GitHubRemote(domain, path, cfg.protocol, cfg.name, true); - case CustomRemoteType.GitLab: - return (domain: string, path: string) => new GitLabRemote(domain, path, cfg.protocol, cfg.name, true); - default: - return undefined; + const match = matcher.exec(url); + if (match != null) { + return creator(container, match[1], match[2]); + } } + + return undefined; + } catch (ex) { + Logger.error(ex, 'createRemoteProvider'); + return undefined; } } diff --git a/src/git/remotes/github.ts b/src/git/remotes/github.ts index 5a6f026..a4b2b61 100644 --- a/src/git/remotes/github.ts +++ b/src/git/remotes/github.ts @@ -2,7 +2,7 @@ import type { AuthenticationSession, Range } from 'vscode'; import { Uri, window } from 'vscode'; import type { Autolink, DynamicAutolinkReference } from '../../annotations/autolinks'; import type { AutolinkReference } from '../../config'; -import { Container } from '../../container'; +import type { Container } from '../../container'; import { isSubscriptionPaidPlan, isSubscriptionPreviewTrialExpired } from '../../subscription'; import { log } from '../../system/decorators/log'; import { memoize } from '../../system/decorators/memoize'; @@ -28,8 +28,15 @@ export class GitHubRemote extends RichRemoteProvider { return equalsIgnoreCase(this.domain, 'github.com') ? authProvider : enterpriseAuthProvider; } - constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) { - super(domain, path, protocol, name, custom); + constructor( + container: Container, + domain: string, + path: string, + protocol?: string, + name?: string, + custom: boolean = false, + ) { + super(container, domain, path, protocol, name, custom); } get apiBaseUrl() { @@ -109,12 +116,11 @@ export class GitHubRemote extends RichRemoteProvider { @log() override async connect(): Promise { if (!equalsIgnoreCase(this.domain, 'github.com')) { - const container = Container.instance; const title = 'Connecting to a GitHub Enterprise instance for rich integration features requires a paid GitLens+ account.'; while (true) { - const subscription = await container.subscription.getSubscription(); + const subscription = await this.container.subscription.getSubscription(); if (subscription.account?.verified === false) { const resend = { title: 'Resend Verification' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; @@ -126,7 +132,7 @@ export class GitHubRemote extends RichRemoteProvider { ); if (result === resend) { - if (await container.subscription.resendVerification()) { + if (await this.container.subscription.resendVerification()) { continue; } } @@ -149,7 +155,7 @@ export class GitHubRemote extends RichRemoteProvider { if (result !== startTrial) return false; - void container.subscription.startPreviewTrial(); + void this.container.subscription.startPreviewTrial(); break; } else if (subscription.account == null) { const signIn = { title: 'Sign In to GitLens+' }; @@ -162,7 +168,7 @@ export class GitHubRemote extends RichRemoteProvider { ); if (result === signIn) { - if (await container.subscription.loginOrSignUp()) { + if (await this.container.subscription.loginOrSignUp()) { continue; } } @@ -177,7 +183,7 @@ export class GitHubRemote extends RichRemoteProvider { ); if (result === upgrade) { - void container.subscription.purchase(); + void this.container.subscription.purchase(); } } @@ -306,7 +312,7 @@ export class GitHubRemote extends RichRemoteProvider { }, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.github)?.getAccountForCommit(this, accessToken, owner, repo, ref, { + return (await this.container.github)?.getAccountForCommit(this, accessToken, owner, repo, ref, { ...options, baseUrl: this.apiBaseUrl, }); @@ -320,7 +326,7 @@ export class GitHubRemote extends RichRemoteProvider { }, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.github)?.getAccountForEmail(this, accessToken, owner, repo, email, { + return (await this.container.github)?.getAccountForEmail(this, accessToken, owner, repo, email, { ...options, baseUrl: this.apiBaseUrl, }); @@ -330,7 +336,7 @@ export class GitHubRemote extends RichRemoteProvider { accessToken, }: AuthenticationSession): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.github)?.getDefaultBranch(this, accessToken, owner, repo, { + return (await this.container.github)?.getDefaultBranch(this, accessToken, owner, repo, { baseUrl: this.apiBaseUrl, }); } @@ -340,7 +346,7 @@ export class GitHubRemote extends RichRemoteProvider { id: string, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.github)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { + return (await this.container.github)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { baseUrl: this.apiBaseUrl, }); } @@ -358,7 +364,7 @@ export class GitHubRemote extends RichRemoteProvider { const GitHubPullRequest = (await import(/* webpackChunkName: "github" */ '../../plus/github/models')) .GitHubPullRequest; - return (await Container.instance.github)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, { + return (await this.container.github)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, { ...opts, include: include?.map(s => GitHubPullRequest.toState(s)), baseUrl: this.apiBaseUrl, @@ -370,7 +376,7 @@ export class GitHubRemote extends RichRemoteProvider { ref: string, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.github)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, { + return (await this.container.github)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, { baseUrl: this.apiBaseUrl, }); } diff --git a/src/git/remotes/gitlab.ts b/src/git/remotes/gitlab.ts index 10cf038..743906d 100644 --- a/src/git/remotes/gitlab.ts +++ b/src/git/remotes/gitlab.ts @@ -3,7 +3,7 @@ import { env, ThemeIcon, Uri, window } from 'vscode'; import type { Autolink, DynamicAutolinkReference } from '../../annotations/autolinks'; import type { AutolinkReference } from '../../config'; import { AutolinkType } from '../../config'; -import { Container } from '../../container'; +import type { Container } from '../../container'; import type { IntegrationAuthenticationProvider, IntegrationAuthenticationSessionDescriptor, @@ -31,8 +31,15 @@ export class GitLabRemote extends RichRemoteProvider { return authProvider; } - constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) { - super(domain, path, protocol, name, custom); + constructor( + container: Container, + domain: string, + path: string, + protocol?: string, + name?: string, + custom: boolean = false, + ) { + super(container, domain, path, protocol, name, custom); } get apiBaseUrl() { @@ -139,12 +146,11 @@ export class GitLabRemote extends RichRemoteProvider { @log() override async connect(): Promise { if (!equalsIgnoreCase(this.domain, 'gitlab.com')) { - const container = Container.instance; const title = 'Connecting to a GitLab self-managed instance for rich integration features requires a paid GitLens+ account.'; while (true) { - const subscription = await container.subscription.getSubscription(); + const subscription = await this.container.subscription.getSubscription(); if (subscription.account?.verified === false) { const resend = { title: 'Resend Verification' }; const cancel = { title: 'Cancel', isCloseAffordance: true }; @@ -156,7 +162,7 @@ export class GitLabRemote extends RichRemoteProvider { ); if (result === resend) { - if (await container.subscription.resendVerification()) { + if (await this.container.subscription.resendVerification()) { continue; } } @@ -179,7 +185,7 @@ export class GitLabRemote extends RichRemoteProvider { if (result !== startTrial) return false; - void container.subscription.startPreviewTrial(); + void this.container.subscription.startPreviewTrial(); break; } else if (subscription.account == null) { const signIn = { title: 'Sign In to GitLens+' }; @@ -192,7 +198,7 @@ export class GitLabRemote extends RichRemoteProvider { ); if (result === signIn) { - if (await container.subscription.loginOrSignUp()) { + if (await this.container.subscription.loginOrSignUp()) { continue; } } @@ -207,7 +213,7 @@ export class GitLabRemote extends RichRemoteProvider { ); if (result === upgrade) { - void container.subscription.purchase(); + void this.container.subscription.purchase(); } } @@ -324,7 +330,7 @@ export class GitLabRemote extends RichRemoteProvider { }, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.gitlab)?.getAccountForCommit(this, accessToken, owner, repo, ref, { + return (await this.container.gitlab)?.getAccountForCommit(this, accessToken, owner, repo, ref, { ...options, baseUrl: this.apiBaseUrl, }); @@ -338,7 +344,7 @@ export class GitLabRemote extends RichRemoteProvider { }, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.gitlab)?.getAccountForEmail(this, accessToken, owner, repo, email, { + return (await this.container.gitlab)?.getAccountForEmail(this, accessToken, owner, repo, email, { ...options, baseUrl: this.apiBaseUrl, }); @@ -348,7 +354,7 @@ export class GitLabRemote extends RichRemoteProvider { accessToken, }: AuthenticationSession): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.gitlab)?.getDefaultBranch(this, accessToken, owner, repo, { + return (await this.container.gitlab)?.getDefaultBranch(this, accessToken, owner, repo, { baseUrl: this.apiBaseUrl, }); } @@ -358,7 +364,7 @@ export class GitLabRemote extends RichRemoteProvider { id: string, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.gitlab)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { + return (await this.container.gitlab)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), { baseUrl: this.apiBaseUrl, }); } @@ -376,7 +382,7 @@ export class GitLabRemote extends RichRemoteProvider { const GitLabMergeRequest = (await import(/* webpackChunkName: "gitlab" */ '../../plus/gitlab/models')) .GitLabMergeRequest; - return (await Container.instance.gitlab)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, { + return (await this.container.gitlab)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, { ...opts, include: include?.map(s => GitLabMergeRequest.toState(s)), baseUrl: this.apiBaseUrl, @@ -388,7 +394,7 @@ export class GitLabRemote extends RichRemoteProvider { ref: string, ): Promise { const [owner, repo] = this.splitPath(); - return (await Container.instance.gitlab)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, { + return (await this.container.gitlab)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, { baseUrl: this.apiBaseUrl, }); } diff --git a/src/git/remotes/provider.ts b/src/git/remotes/provider.ts index 54b654c..88abf2b 100644 --- a/src/git/remotes/provider.ts +++ b/src/git/remotes/provider.ts @@ -5,7 +5,7 @@ import { isWeb } from '@env/platform'; import type { DynamicAutolinkReference } from '../../annotations/autolinks'; import type { AutolinkReference } from '../../config'; import { configuration } from '../../configuration'; -import { Container } from '../../container'; +import type { Container } from '../../container'; import { AuthenticationError, ProviderRequestClientError } from '../../errors'; import { Logger } from '../../logger'; import { showIntegrationDisconnectedTooManyFailedRequestsWarningMessage } from '../../messages'; @@ -21,6 +21,7 @@ import type { IssueOrPullRequest } from '../models/issue'; import type { PullRequest, PullRequestState } from '../models/pullRequest'; import type { RemoteProviderReference } from '../models/remoteProvider'; import type { Repository } from '../models/repository'; +import { RichRemoteProviders } from './remoteProviderConnections'; export const enum RemoteResourceType { Branch = 'branch', @@ -244,28 +245,6 @@ export abstract class RemoteProvider implements RemoteProviderReference { } } -const _connectedCache = new Set(); -const _onDidChangeAuthentication = new EventEmitter<{ reason: 'connected' | 'disconnected'; key: string }>(); -function fireAuthenticationChanged(key: string, reason: 'connected' | 'disconnected') { - // Only fire events if the key is being connected for the first time (we could probably do the same for disconnected, but better safe on those imo) - if (_connectedCache.has(key)) { - if (reason === 'connected') return; - - _connectedCache.delete(key); - } else if (reason === 'connected') { - _connectedCache.add(key); - } - - _onDidChangeAuthentication.fire({ key: key, reason: reason }); -} - -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class Authentication { - static get onDidChange(): Event<{ reason: 'connected' | 'disconnected'; key: string }> { - return _onDidChangeAuthentication.event; - } -} - // TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart export abstract class RichRemoteProvider extends RemoteProvider { @@ -280,17 +259,24 @@ export abstract class RichRemoteProvider extends RemoteProvider { return this._onDidChange.event; } - constructor(domain: string, path: string, protocol?: string, name?: string, custom?: boolean) { + constructor( + protected readonly container: Container, + domain: string, + path: string, + protocol?: string, + name?: string, + custom?: boolean, + ) { super(domain, path, protocol, name, custom); - Container.instance.context.subscriptions.push( + this.container.context.subscriptions.push( configuration.onDidChange(e => { if (configuration.changed(e, 'remotes')) { this._ignoreSSLErrors.clear(); } }), // TODO@eamodio revisit how connections are linked or not - Authentication.onDidChange(e => { + RichRemoteProviders.onDidChangeConnectionState(e => { if (e.key !== this.key) return; if (e.reason === 'disconnected') { @@ -354,8 +340,6 @@ export abstract class RichRemoteProvider extends RemoteProvider { const connected = this._session != null; - const container = Container.instance; - if (connected && !options?.silent) { if (options?.currentSessionOnly) { void showIntegrationDisconnectedTooManyFailedRequestsWarningMessage(this.name); @@ -365,7 +349,7 @@ export abstract class RichRemoteProvider extends RemoteProvider { const cancel = { title: 'Cancel', isCloseAffordance: true }; let result: MessageItem | undefined; - if (container.integrationAuthentication.hasProvider(this.authProvider.id)) { + if (this.container.integrationAuthentication.hasProvider(this.authProvider.id)) { result = await window.showWarningMessage( `Are you sure you want to disable the rich integration with ${this.name}?\n\nNote: signing out clears the saved authentication.`, { modal: true }, @@ -384,7 +368,7 @@ export abstract class RichRemoteProvider extends RemoteProvider { if (result == null || result === cancel) return; if (result === signout) { - void container.integrationAuthentication.deleteSession(this.id, this.authProviderDescriptor); + void this.container.integrationAuthentication.deleteSession(this.id, this.authProviderDescriptor); } } } @@ -396,12 +380,12 @@ export abstract class RichRemoteProvider extends RemoteProvider { if (connected) { // Don't store the disconnected flag if this only for this current VS Code session (will be re-connected on next restart) if (!options?.currentSessionOnly) { - void container.storage.storeWorkspace(this.connectedKey, false); + void this.container.storage.storeWorkspace(this.connectedKey, false); } this._onDidChange.fire(); if (!options?.silent && !options?.currentSessionOnly) { - fireAuthenticationChanged(this.key, 'disconnected'); + RichRemoteProviders.disconnected(this.key); } } } @@ -663,18 +647,16 @@ export abstract class RichRemoteProvider extends RemoteProvider { if (this._session != null) return this._session; if (!configuration.get('integrations.enabled')) return undefined; - const { instance: container } = Container; - if (createIfNeeded) { - await container.storage.deleteWorkspace(this.connectedKey); - } else if (container.storage.getWorkspace(this.connectedKey) === false) { + await this.container.storage.deleteWorkspace(this.connectedKey); + } else if (this.container.storage.getWorkspace(this.connectedKey) === false) { return undefined; } let session: AuthenticationSession | undefined | null; try { - if (container.integrationAuthentication.hasProvider(this.authProvider.id)) { - session = await container.integrationAuthentication.getSession( + if (this.container.integrationAuthentication.hasProvider(this.authProvider.id)) { + session = await this.container.integrationAuthentication.getSession( this.authProvider.id, this.authProviderDescriptor, { createIfNeeded: createIfNeeded, forceNewSession: forceNewSession }, @@ -689,7 +671,7 @@ export abstract class RichRemoteProvider extends RemoteProvider { ); } } catch (ex) { - await container.storage.deleteWorkspace(this.connectedKey); + await this.container.storage.deleteWorkspace(this.connectedKey); if (ex instanceof Error && ex.message.includes('User did not consent')) { return undefined; @@ -699,18 +681,18 @@ export abstract class RichRemoteProvider extends RemoteProvider { } if (session === undefined && !createIfNeeded) { - await container.storage.deleteWorkspace(this.connectedKey); + await this.container.storage.deleteWorkspace(this.connectedKey); } this._session = session ?? null; this.resetRequestExceptionCount(); if (session != null) { - await container.storage.storeWorkspace(this.connectedKey, true); + await this.container.storage.storeWorkspace(this.connectedKey, true); queueMicrotask(() => { this._onDidChange.fire(); - fireAuthenticationChanged(this.key, 'connected'); + RichRemoteProviders.connected(this.key); }); } diff --git a/src/git/remotes/remoteProviderConnections.ts b/src/git/remotes/remoteProviderConnections.ts new file mode 100644 index 0000000..edbcdd6 --- /dev/null +++ b/src/git/remotes/remoteProviderConnections.ts @@ -0,0 +1,30 @@ +import { EventEmitter } from 'vscode'; +import type { Event } from 'vscode'; + +export interface ConnectionStateChangeEvent { + key: string; + reason: 'connected' | 'disconnected'; +} + +export namespace RichRemoteProviders { + const _connectedCache = new Set(); + export const _onDidChangeConnectionState = new EventEmitter(); + export const onDidChangeConnectionState: Event = _onDidChangeConnectionState.event; + + export function connected(key: string): void { + // Only fire events if the key is being connected for the first time + if (_connectedCache.has(key)) return; + + _connectedCache.add(key); + + _onDidChangeConnectionState.fire({ key: key, reason: 'connected' }); + } + + export function disconnected(key: string): void { + // Probably shouldn't bother to fire the event if we don't already think we are connected, but better to be safe + // if (!_connectedCache.has(key)) return; + _connectedCache.delete(key); + + _onDidChangeConnectionState.fire({ key: key, reason: 'disconnected' }); + } +} diff --git a/src/plus/github/githubGitProvider.ts b/src/plus/github/githubGitProvider.ts index 58b3852..464f7cd 100644 --- a/src/plus/github/githubGitProvider.ts +++ b/src/plus/github/githubGitProvider.ts @@ -61,7 +61,7 @@ import type { GitTreeEntry } from '../../git/models/tree'; import type { GitUser } from '../../git/models/user'; import { isUserMatch } from '../../git/models/user'; import type { RemoteProviders } from '../../git/remotes/factory'; -import { RemoteProviderFactory } from '../../git/remotes/factory'; +import { getRemoteProviderMatcher, loadRemoteProviders } from '../../git/remotes/factory'; import type { RemoteProvider, RichRemoteProvider } from '../../git/remotes/provider'; import { SearchPattern } from '../../git/search'; import type { LogScope } from '../../logger'; @@ -2303,7 +2303,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { ): Promise[]> { if (repoPath == null) return []; - const providers = options?.providers ?? RemoteProviderFactory.loadProviders(configuration.get('remotes', null)); + const providers = options?.providers ?? loadRemoteProviders(configuration.get('remotes', null)); const uri = Uri.parse(repoPath, true); const [, owner, repo] = uri.path.split('/', 3); @@ -2320,7 +2320,7 @@ export class GitHubGitProvider implements GitProvider, Disposable { 'https', domain, path, - RemoteProviderFactory.factory(providers)(url, domain, path), + getRemoteProviderMatcher(this.container, providers)(url, domain, path), [ { type: GitRemoteType.Fetch, url: url }, { type: GitRemoteType.Push, url: url },