Sfoglia il codice sorgente

Adds GK Provider API Library Integration (#2883)

* Providers -> integrations (wip)

* Updates folder structure of provider and auth logic

* Adds basic providers api integration

Adds filtering support on inputs

Improves types and adds getCurrentUser support

Updates getCurrentUser Fns

Adds GetReposFn for Azure

Reorganizes types and constants

Adds  providers service

Uses correct filter property for Bitbucket PRs

Switches to logged errors

Adds filter compatibility check

Adds paging and per-repo PR/Issue support

Uses PagedResult for paging

Updates dependencies

Updates dependency

Reorganizes and moves provider service logic to providerIntegration

Updates missing provider mappings, ProviderId usage

Updates provider api dependency

Adds enterprise domain passing and GLSH

Uses the correct base api urls

Updates dependency

* Refactors provider integrations (wip)
 - Removes `RichRemoteProvider` in favor of `ProviderIntegration`
 - Implements GitLab integration
 - Fixes caching
 - Fixes GitHub authentication

* Avoids allocating integration auth before required

* Unifies remote provider service into integrations

* Fixes GitLab API base url

* Moves fallback for getting vscode session to integration auth service

---------

Co-authored-by: Eric Amodio <eamodio@gmail.com>
main
Ramin Tadayon 1 anno fa
committed by GitHub
parent
commit
690e17403f
Non sono state trovate chiavi note per questa firma nel database ID Chiave GPG: 4AEE18F83AFDEB23
61 ha cambiato i file con 11259 aggiunte e 9336 eliminazioni
  1. +1
    -0
      package.json
  2. +8
    -8
      src/annotations/autolinks.ts
  3. +2
    -2
      src/annotations/lineAnnotationController.ts
  4. +7
    -2
      src/avatars.ts
  5. +19
    -22
      src/cache.ts
  6. +1
    -1
      src/commands/openAssociatedPullRequestOnRemote.ts
  7. +6
    -3
      src/commands/openPullRequestOnRemote.ts
  8. +18
    -16
      src/commands/remoteProviders.ts
  9. +19
    -22
      src/container.ts
  10. +1
    -1
      src/env/browser/providers.ts
  11. +1
    -16
      src/env/node/git/localGitProvider.ts
  12. +3
    -2
      src/env/node/providers.ts
  13. +2
    -2
      src/git/formatters/commitFormatter.ts
  14. +25
    -104
      src/git/gitProviderService.ts
  15. +9
    -6
      src/git/models/branch.ts
  16. +7
    -4
      src/git/models/commit.ts
  17. +15
    -8
      src/git/models/remote.ts
  18. +6
    -0
      src/git/models/remoteProvider.ts
  19. +3
    -3
      src/git/models/repository.ts
  20. +13
    -8
      src/git/parsers/remoteParser.ts
  21. +16
    -245
      src/git/remotes/github.ts
  22. +21
    -239
      src/git/remotes/gitlab.ts
  23. +13
    -16
      src/git/remotes/remoteProvider.ts
  24. +0
    -49
      src/git/remotes/remoteProviderService.ts
  25. +5
    -5
      src/git/remotes/remoteProviders.ts
  26. +0
    -621
      src/git/remotes/richRemoteProvider.ts
  27. +2
    -2
      src/hovers/hovers.ts
  28. +2
    -2
      src/plus/focus/focusService.ts
  29. +0
    -116
      src/plus/integrationAuthentication.ts
  30. +123
    -0
      src/plus/integrations/authentication/azureDevOps.ts
  31. +137
    -0
      src/plus/integrations/authentication/bitbucket.ts
  32. +81
    -0
      src/plus/integrations/authentication/github.ts
  33. +82
    -0
      src/plus/integrations/authentication/gitlab.ts
  34. +166
    -0
      src/plus/integrations/authentication/integrationAuthentication.ts
  35. +177
    -0
      src/plus/integrations/integrationService.ts
  36. +925
    -0
      src/plus/integrations/providerIntegration.ts
  37. +129
    -0
      src/plus/integrations/providers/azureDevOps.ts
  38. +110
    -0
      src/plus/integrations/providers/bitbucket.ts
  39. +196
    -0
      src/plus/integrations/providers/github.ts
  40. +41
    -41
      src/plus/integrations/providers/github/github.ts
  41. +63
    -56
      src/plus/integrations/providers/github/githubGitProvider.ts
  42. +9
    -12
      src/plus/integrations/providers/github/models.ts
  43. +190
    -0
      src/plus/integrations/providers/gitlab.ts
  44. +34
    -34
      src/plus/integrations/providers/gitlab/gitlab.ts
  45. +4
    -4
      src/plus/integrations/providers/gitlab/models.ts
  46. +283
    -0
      src/plus/integrations/providers/models.ts
  47. +609
    -0
      src/plus/integrations/providers/providersApi.ts
  48. +7
    -5
      src/plus/webviews/focus/focusWebview.ts
  49. +2
    -2
      src/plus/webviews/focus/protocol.ts
  50. +3
    -2
      src/quickpicks/remoteProviderPicker.ts
  51. +1
    -1
      src/statusbar/statusBarController.ts
  52. +3
    -3
      src/views/nodes/commitNode.ts
  53. +1
    -1
      src/views/nodes/fileRevisionAsCommitNode.ts
  54. +3
    -2
      src/views/nodes/remoteNode.ts
  55. +2
    -2
      src/webviews/commitDetails/commitDetailsWebview.ts
  56. +8
    -1
      yarn.lock

+ 1
- 0
package.json Vedi File

@ -15774,6 +15774,7 @@
},
"dependencies": {
"@gitkraken/gitkraken-components": "10.2.5",
"@gitkraken/provider-apis": "0.10.0",
"@gitkraken/shared-web-components": "0.1.1-rc.15",
"@lit/react": "1.0.2",
"@microsoft/fast-element": "1.12.0",

+ 8
- 8
src/annotations/autolinks.ts Vedi File

@ -7,7 +7,7 @@ import type { IssueOrPullRequest } from '../git/models/issue';
import { getIssueOrPullRequestHtmlIcon, getIssueOrPullRequestMarkdownIcon } from '../git/models/issue';
import type { GitRemote } from '../git/models/remote';
import type { RemoteProviderReference } from '../git/models/remoteProvider';
import type { RepositoryDescriptor, RichRemoteProvider } from '../git/remotes/richRemoteProvider';
import type { RepositoryDescriptor } from '../plus/integrations/providerIntegration';
import type { MaybePausedResult } from '../system/cancellation';
import { configuration } from '../system/configuration';
import { fromNow } from '../system/date';
@ -223,10 +223,9 @@ export class Autolinks implements Disposable {
}
if (messageOrAutolinks.size === 0) return undefined;
let provider: RichRemoteProvider | undefined;
if (remote?.hasRichIntegration()) {
({ provider } = remote);
const connected = remote.provider.maybeConnected ?? (await remote.provider.isConnected());
let provider = remote?.getIntegration();
if (provider != null) {
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (!connected) {
provider = undefined;
}
@ -239,10 +238,11 @@ export class Autolinks implements Disposable {
[
id,
[
remote?.provider != null &&
provider != null &&
link.provider?.id === provider.id &&
link.provider?.domain === provider.domain
? provider.getIssueOrPullRequest(id, link.descriptor)
? provider.getIssueOrPullRequest(link.descriptor ?? remote.provider.repoDesc, id)
: undefined,
link,
] satisfies EnrichedAutolink,
@ -284,8 +284,8 @@ export class Autolinks implements Disposable {
if (remotes != null && remotes.length !== 0) {
remotes = [...remotes].sort((a, b) => {
const aConnected = a.provider?.maybeConnected;
const bConnected = b.provider?.maybeConnected;
const aConnected = a.maybeIntegrationConnected;
const bConnected = b.maybeIntegrationConnected;
return aConnected !== bConnected ? (aConnected ? -1 : bConnected ? 1 : 0) : 0;
});
for (const r of remotes) {

+ 2
- 2
src/annotations/lineAnnotationController.ts Vedi File

@ -39,7 +39,7 @@ export class LineAnnotationController implements Disposable {
once(container.onReady)(this.onReady, this),
configuration.onDidChange(this.onConfigurationChanged, this),
container.fileAnnotations.onDidToggleAnnotations(this.onFileAnnotationsToggled, this),
container.richRemoteProviders.onAfterDidChangeConnectionState(
container.integrations.onDidChangeConnectionState(
debounce(() => void this.refresh(window.activeTextEditor), 250),
),
);
@ -158,7 +158,7 @@ export class LineAnnotationController implements Disposable {
const prs = new Map<string, Promise<PullRequest | undefined>>();
if (lines.size === 0) return prs;
const remotePromise = this.container.git.getBestRemoteWithRichProvider(repoPath);
const remotePromise = this.container.git.getBestRemoteWithIntegration(repoPath);
for (const [, state] of lines) {
if (state.commit.isUncommitted) continue;

+ 7
- 2
src/avatars.ts Vedi File

@ -220,8 +220,13 @@ async function getAvatarUriFromRemoteProvider(
// account = await remote?.provider.getAccountForEmail(email, { avatarSize: size });
// } else {
if (typeof repoPathOrCommit !== 'string') {
const remote = await Container.instance.git.getBestRemoteWithRichProvider(repoPathOrCommit.repoPath);
account = await remote?.provider.getAccountForCommit(repoPathOrCommit.ref, { avatarSize: size });
const remote = await Container.instance.git.getBestRemoteWithIntegration(repoPathOrCommit.repoPath);
if (remote?.hasIntegration()) {
const integration = remote.getIntegration();
account = await integration?.getAccountForCommit(remote.provider.repoDesc, repoPathOrCommit.ref, {
avatarSize: size,
});
}
}
if (account?.avatarUrl == null) {

+ 19
- 22
src/cache.ts Vedi File

@ -4,10 +4,8 @@ import type { Container } from './container';
import type { DefaultBranch } from './git/models/defaultBranch';
import type { IssueOrPullRequest } from './git/models/issue';
import type { PullRequest } from './git/models/pullRequest';
import type { GitRemote } from './git/models/remote';
import type { RepositoryMetadata } from './git/models/repositoryMetadata';
import type { RemoteProvider } from './git/remotes/remoteProvider';
import type { RepositoryDescriptor, RichRemoteProvider } from './git/remotes/richRemoteProvider';
import type { ProviderIntegration, RepositoryDescriptor } from './plus/integrations/providerIntegration';
import { isPromise } from './system/promise';
type Caches = {
@ -74,11 +72,11 @@ export class CacheProvider implements Disposable {
getIssueOrPullRequest(
id: string,
repo: RepositoryDescriptor | undefined,
remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
repo: RepositoryDescriptor,
integration: ProviderIntegration | undefined,
cacheable: Cacheable<IssueOrPullRequest>,
): CacheResult<IssueOrPullRequest> {
const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
const { key, etag } = getRemoteKeyAndEtag(repo, integration);
if (repo == null) {
return this.get('issuesOrPrsById', `id:${id}:${key}`, etag, cacheable);
@ -88,7 +86,7 @@ export class CacheProvider implements Disposable {
// getEnrichedAutolinks(
// sha: string,
// remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
// remoteOrProvider: Integration,
// cacheable: Cacheable<Map<string, EnrichedAutolink>>,
// ): CacheResult<Map<string, EnrichedAutolink>> {
// const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
@ -97,39 +95,43 @@ export class CacheProvider implements Disposable {
getPullRequestForBranch(
branch: string,
remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
repo: RepositoryDescriptor,
integration: ProviderIntegration | undefined,
cacheable: Cacheable<PullRequest>,
): CacheResult<PullRequest> {
const cache = 'prByBranch';
const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
const { key, etag } = getRemoteKeyAndEtag(repo, integration);
// Wrap the cacheable so we can also add the result to the issuesOrPrsById cache
return this.get(cache, `branch:${branch}:${key}`, etag, this.wrapPullRequestCacheable(cacheable, key, etag));
}
getPullRequestForSha(
sha: string,
remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
repo: RepositoryDescriptor,
integration: ProviderIntegration | undefined,
cacheable: Cacheable<PullRequest>,
): CacheResult<PullRequest> {
const cache = 'prsBySha';
const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
const { key, etag } = getRemoteKeyAndEtag(repo, integration);
// Wrap the cacheable so we can also add the result to the issuesOrPrsById cache
return this.get(cache, `sha:${sha}:${key}`, etag, this.wrapPullRequestCacheable(cacheable, key, etag));
}
getRepositoryDefaultBranch(
remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
repo: RepositoryDescriptor,
integration: ProviderIntegration | undefined,
cacheable: Cacheable<DefaultBranch>,
): CacheResult<DefaultBranch> {
const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
const { key, etag } = getRemoteKeyAndEtag(repo, integration);
return this.get('defaultBranch', `repo:${key}`, etag, cacheable);
}
getRepositoryMetadata(
remoteOrProvider: RichRemoteProvider | GitRemote<RichRemoteProvider>,
repo: RepositoryDescriptor,
integration: ProviderIntegration | undefined,
cacheable: Cacheable<RepositoryMetadata>,
): CacheResult<RepositoryMetadata> {
const { key, etag } = getRemoteKeyAndEtag(remoteOrProvider);
const { key, etag } = getRemoteKeyAndEtag(repo, integration);
return this.get('repoMetadata', `repo:${key}`, etag, cacheable);
}
@ -218,11 +220,6 @@ function getExpiresAt(cache: T, value: CacheValue | undefine
}
}
function getRemoteKeyAndEtag(remoteOrProvider: RemoteProvider | GitRemote<RichRemoteProvider>) {
return {
key: remoteOrProvider.remoteKey,
etag: remoteOrProvider.hasRichIntegration()
? `${remoteOrProvider.remoteKey}:${remoteOrProvider.maybeConnected ?? false}`
: remoteOrProvider.remoteKey,
};
function getRemoteKeyAndEtag(repo: RepositoryDescriptor, integration?: ProviderIntegration) {
return { key: repo.key, etag: `${repo.key}:${integration?.maybeConnected ?? false}` };
}

+ 1
- 1
src/commands/openAssociatedPullRequestOnRemote.ts Vedi File

@ -36,7 +36,7 @@ export class OpenAssociatedPullRequestOnRemoteCommand extends ActiveEditorComman
} else {
try {
const repo = await getRepositoryOrShowPicker('Open Associated Pull Request', undefined, undefined, {
filter: async r => (await this.container.git.getBestRemoteWithRichProvider(r.uri)) != null,
filter: async r => (await this.container.git.getBestRemoteWithIntegration(r.uri)) != null,
});
if (repo == null) return;

+ 6
- 3
src/commands/openPullRequestOnRemote.ts Vedi File

@ -36,10 +36,13 @@ export class OpenPullRequestOnRemoteCommand extends Command {
if (args?.pr == null) {
if (args?.repoPath == null || args?.ref == null) return;
const remote = await this.container.git.getBestRemoteWithRichProvider(args.repoPath);
if (!remote?.hasRichIntegration()) return;
const remote = await this.container.git.getBestRemoteWithIntegration(args.repoPath);
if (remote == null) return;
const pr = await remote.provider.getPullRequestForCommit(args.ref);
const provider = this.container.integrations.getByRemote(remote);
if (provider == null) return;
const pr = await provider.getPullRequestForCommit(remote.provider.repoDesc, args.ref);
if (pr == null) {
void window.showInformationMessage(`No pull request associated with '${shortenRevision(args.ref)}'`);
return;

+ 18
- 16
src/commands/remoteProviders.ts Vedi File

@ -3,7 +3,7 @@ import type { Container } from '../container';
import type { GitCommit } from '../git/models/commit';
import { GitRemote } from '../git/models/remote';
import type { Repository } from '../git/models/repository';
import type { RichRemoteProvider } from '../git/remotes/richRemoteProvider';
import type { RemoteProvider } from '../git/remotes/remoteProvider';
import { showRepositoryPicker } from '../quickpicks/repositoryPicker';
import { command } from '../system/command';
import { first } from '../system/iterable';
@ -46,15 +46,15 @@ export class ConnectRemoteProviderCommand extends Command {
}
async execute(args?: ConnectRemoteProviderCommandArgs): Promise<any> {
let remote: GitRemote<RichRemoteProvider> | undefined;
let remote: GitRemote<RemoteProvider> | undefined;
let remotes: GitRemote[] | undefined;
let repoPath;
if (args?.repoPath == null) {
const repos = new Map<Repository, GitRemote<RichRemoteProvider>>();
const repos = new Map<Repository, GitRemote<RemoteProvider>>();
for (const repo of this.container.git.openRepositories) {
const remote = await repo.getRichRemote();
if (remote?.provider != null && !(await remote.provider.isConnected())) {
if (remote?.provider != null) {
repos.set(repo, remote);
}
}
@ -78,17 +78,20 @@ export class ConnectRemoteProviderCommand extends Command {
} else if (args?.remote == null) {
repoPath = args.repoPath;
remote = await this.container.git.getBestRemoteWithRichProvider(repoPath, { includeDisconnected: true });
remote = await this.container.git.getBestRemoteWithIntegration(repoPath, { includeDisconnected: true });
if (remote == null) return false;
} else {
repoPath = args.repoPath;
remotes = await this.container.git.getRemotesWithProviders(repoPath);
remote = remotes.find(r => r.name === args.remote) as GitRemote<RichRemoteProvider> | undefined;
if (!remote?.hasRichIntegration()) return false;
remote = remotes.find(r => r.name === args.remote) as GitRemote<RemoteProvider> | undefined;
if (!remote?.hasIntegration()) return false;
}
const connected = await remote.provider.connect();
const integration = this.container.integrations.getByRemote(remote);
if (integration == null) return false;
const connected = await integration.connect();
if (
connected &&
!(remotes ?? (await this.container.git.getRemotesWithProviders(repoPath))).some(r => r.default)
@ -138,10 +141,10 @@ export class DisconnectRemoteProviderCommand extends Command {
}
async execute(args?: DisconnectRemoteProviderCommandArgs): Promise<any> {
let remote: GitRemote<RichRemoteProvider> | undefined;
let remote: GitRemote<RemoteProvider> | undefined;
let repoPath;
if (args?.repoPath == null) {
const repos = new Map<Repository, GitRemote<RichRemoteProvider>>();
const repos = new Map<Repository, GitRemote<RemoteProvider>>();
for (const repo of this.container.git.openRepositories) {
const remote = await repo.getRichRemote(true);
@ -169,17 +172,16 @@ export class DisconnectRemoteProviderCommand extends Command {
} else if (args?.remote == null) {
repoPath = args.repoPath;
remote = await this.container.git.getBestRemoteWithRichProvider(repoPath, { includeDisconnected: false });
remote = await this.container.git.getBestRemoteWithIntegration(repoPath, { includeDisconnected: false });
if (remote == null) return undefined;
} else {
repoPath = args.repoPath;
remote = (await this.container.git.getRemotesWithProviders(repoPath)).find(r => r.name === args.remote) as
| GitRemote<RichRemoteProvider>
| undefined;
if (!remote?.hasRichIntegration()) return undefined;
remote = (await this.container.git.getRemotesWithProviders(repoPath)).find(r => r.name === args.remote);
if (!remote?.hasIntegration()) return undefined;
}
return remote.provider.disconnect();
const integration = this.container.integrations.getByRemote(remote);
return integration?.disconnect();
}
}

+ 19
- 22
src/container.ts Vedi File

@ -16,9 +16,6 @@ import { Commands, extensionPrefix } from './constants';
import { EventBus } from './eventBus';
import { GitFileSystemProvider } from './git/fsProvider';
import { GitProviderService } from './git/gitProviderService';
import { GitHubAuthenticationProvider } from './git/remotes/github';
import { GitLabAuthenticationProvider } from './git/remotes/gitlab';
import { RichRemoteProviderService } from './git/remotes/remoteProviderService';
import { LineHoverController } from './hovers/lineHoverController';
import type { RepositoryPathMappingProvider } from './pathMapping/repositoryPathMappingProvider';
import { DraftService } from './plus/drafts/draftsService';
@ -26,7 +23,8 @@ import { FocusService } from './plus/focus/focusService';
import { AccountAuthenticationProvider } from './plus/gk/account/authenticationProvider';
import { SubscriptionService } from './plus/gk/account/subscriptionService';
import { ServerConnection } from './plus/gk/serverConnection';
import { IntegrationAuthenticationService } from './plus/integrationAuthentication';
import { IntegrationAuthenticationService } from './plus/integrations/authentication/integrationAuthentication';
import { IntegrationService } from './plus/integrations/integrationService';
import { RepositoryIdentityService } from './plus/repos/repositoryIdentityService';
import { registerAccountWebviewView } from './plus/webviews/account/registration';
import { registerFocusWebviewCommands, registerFocusWebviewPanel } from './plus/webviews/focus/registration';
@ -495,7 +493,7 @@ export class Container {
return this._git;
}
private _github: Promise<import('./plus/github/github').GitHubApi | undefined> | undefined;
private _github: Promise<import('./plus/integrations/providers/github/github').GitHubApi | undefined> | undefined;
get github() {
if (this._github == null) {
this._github = this._loadGitHubApi();
@ -506,7 +504,9 @@ export class Container {
private async _loadGitHubApi() {
try {
const github = new (await import(/* webpackChunkName: "github" */ './plus/github/github')).GitHubApi(this);
const github = new (
await import(/* webpackChunkName: "github" */ './plus/integrations/providers/github/github')
).GitHubApi(this);
this._disposables.push(github);
return github;
} catch (ex) {
@ -515,7 +515,7 @@ export class Container {
}
}
private _gitlab: Promise<import('./plus/gitlab/gitlab').GitLabApi | undefined> | undefined;
private _gitlab: Promise<import('./plus/integrations/providers/gitlab/gitlab').GitLabApi | undefined> | undefined;
get gitlab() {
if (this._gitlab == null) {
this._gitlab = this._loadGitLabApi();
@ -526,7 +526,9 @@ export class Container {
private async _loadGitLabApi() {
try {
const gitlab = new (await import(/* webpackChunkName: "gitlab" */ './plus/gitlab/gitlab')).GitLabApi(this);
const gitlab = new (
await import(/* webpackChunkName: "gitlab" */ './plus/integrations/providers/gitlab/gitlab')
).GitLabApi(this);
this._disposables.push(gitlab);
return gitlab;
} catch (ex) {
@ -558,17 +560,20 @@ export class Container {
private _integrationAuthentication: IntegrationAuthenticationService | undefined;
get integrationAuthentication() {
if (this._integrationAuthentication == null) {
this._disposables.push(
(this._integrationAuthentication = new IntegrationAuthenticationService(this)),
// Register any integration authentication providers
new GitHubAuthenticationProvider(this),
new GitLabAuthenticationProvider(this),
);
this._disposables.push((this._integrationAuthentication = new IntegrationAuthenticationService(this)));
}
return this._integrationAuthentication;
}
private _integrations: IntegrationService | undefined;
get integrations(): IntegrationService {
if (this._integrations == null) {
this._disposables.push((this._integrations = new IntegrationService(this)));
}
return this._integrations;
}
private readonly _keyboard: Keyboard;
get keyboard() {
return this._keyboard;
@ -640,14 +645,6 @@ export class Container {
return this._repositoryPathMapping;
}
private _richRemoteProviders: RichRemoteProviderService | undefined;
get richRemoteProviders(): RichRemoteProviderService {
if (this._richRemoteProviders == null) {
this._richRemoteProviders = new RichRemoteProviderService(this);
}
return this._richRemoteProviders;
}
private readonly _searchAndCompareView: SearchAndCompareView;
get searchAndCompareView() {
return this._searchAndCompareView;

+ 1
- 1
src/env/browser/providers.ts Vedi File

@ -1,7 +1,7 @@
import { Container } from '../../container';
import { GitCommandOptions } from '../../git/commandOptions';
// Force import of GitHub since dynamic imports are not supported in the WebWorker ExtensionHost
import { GitHubGitProvider } from '../../plus/github/githubGitProvider';
import { GitHubGitProvider } from '../../plus/integrations/providers/github/githubGitProvider';
import { GitProvider } from '../../git/gitProvider';
import { RepositoryWebPathMappingProvider } from './pathMapping/repositoryWebPathMappingProvider';
import { WorkspacesWebPathMappingProvider } from './pathMapping/workspacesWebPathMappingProvider';

+ 1
- 16
src/env/node/git/localGitProvider.ts Vedi File

@ -314,8 +314,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
if (e.changed(RepositoryChange.Remotes, RepositoryChange.RemoteProviders, RepositoryChangeComparisonMode.Any)) {
const remotes = this._remotesCache.get(repo.path);
void disposeRemotes([remotes]);
this._remotesCache.delete(repo.path);
}
@ -1329,8 +1327,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
if (caches.length === 0 || caches.includes('remotes')) {
const remotes = this._remotesCache.get(repoPath);
void disposeRemotes([remotes]);
this._remotesCache.delete(repoPath);
}
@ -1364,7 +1360,6 @@ export class LocalGitProvider implements GitProvider, Disposable {
}
if (caches.length === 0 || caches.includes('remotes')) {
void disposeRemotes([...this._remotesCache.values()]);
this._remotesCache.clear();
}
@ -4536,6 +4531,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
try {
const data = await this.git.remote(repoPath!);
const remotes = parseGitRemotes(
this.container,
data,
repoPath!,
getRemoteProviderMatcher(this.container, providers),
@ -5696,14 +5692,3 @@ async function getEncoding(uri: Uri): Promise {
const encodingExists = (await import(/* webpackChunkName: "encoding" */ 'iconv-lite')).encodingExists;
return encodingExists(encoding) ? encoding : 'utf8';
}
async function disposeRemotes(remotes: (Promise<GitRemote[]> | undefined)[]) {
const remotesResults = await Promise.allSettled(remotes);
for (const remotes of remotesResults) {
for (const remote of getSettledValue(remotes) ?? []) {
if (remote.hasRichIntegration()) {
remote.provider?.dispose();
}
}
}
}

+ 3
- 2
src/env/node/providers.ts Vedi File

@ -40,8 +40,9 @@ export async function getSupportedGitProviders(container: Container): Promise
];
if (configuration.get('virtualRepositories.enabled')) {
const GitHubGitProvider = (await import(/* webpackChunkName: "github" */ '../../plus/github/githubGitProvider'))
.GitHubGitProvider;
const GitHubGitProvider = (
await import(/* webpackChunkName: "github" */ '../../plus/integrations/providers/github/githubGitProvider')
).GitHubGitProvider;
providers.push(new GitHubGitProvider(container));
}

+ 2
- 2
src/git/formatters/commitFormatter.ts Vedi File

@ -472,8 +472,8 @@ export class CommitFormatter extends Formatter {
} else if (remotes != null) {
const [remote] = remotes;
if (
remote?.hasRichIntegration() &&
!remote.provider.maybeConnected &&
remote?.hasIntegration() &&
!remote.maybeIntegrationConnected &&
configuration.get('integrations.enabled')
) {
commands += `${separator}[$(plug) Connect to ${remote?.provider.name}${

+ 25
- 104
src/git/gitProviderService.ts Vedi File

@ -35,15 +35,7 @@ import { Logger } from '../system/logger';
import { getLogScope, setLogScopeExit } from '../system/logger.scope';
import { getBestPath, getScheme, isAbsolute, maybeUri, normalizePath } from '../system/path';
import type { Deferred } from '../system/promise';
import {
asSettled,
cancellable,
defer,
getDeferredPromiseIfPending,
getSettledValue,
isPromise,
PromiseCancelledError,
} from '../system/promise';
import { asSettled, defer, getDeferredPromiseIfPending, getSettledValue } from '../system/promise';
import { sortCompare } from '../system/string';
import { VisitedPathsTrie } from '../system/trie';
import type {
@ -69,15 +61,14 @@ import type { GitContributor } from './models/contributor';
import type { GitDiff, GitDiffFile, GitDiffFiles, GitDiffFilter, GitDiffLine, GitDiffShortStat } from './models/diff';
import type { GitFile } from './models/file';
import type { GitGraph } from './models/graph';
import type { SearchedIssue } from './models/issue';
import type { GitLog } from './models/log';
import type { GitMergeStatus } from './models/merge';
import type { SearchedPullRequest } from './models/pullRequest';
import type { GitRebaseStatus } from './models/rebase';
import type { GitBranchReference, GitReference } from './models/reference';
import { createRevisionRange, isSha, isUncommitted, isUncommittedParent } from './models/reference';
import type { GitReflog } from './models/reflog';
import { getVisibilityCacheKey, GitRemote } from './models/remote';
import type { GitRemote } from './models/remote';
import { getVisibilityCacheKey } from './models/remote';
import type { RepositoryChangeEvent } from './models/repository';
import { Repository, RepositoryChange, RepositoryChangeComparisonMode } from './models/repository';
import type { GitStash } from './models/stash';
@ -88,7 +79,6 @@ import type { GitUser } from './models/user';
import type { GitWorktree } from './models/worktree';
import { parseGitRemoteUrl } from './parsers/remoteParser';
import type { RemoteProvider } from './remotes/remoteProvider';
import type { RichRemoteProvider } from './remotes/richRemoteProvider';
import type { GitSearch, SearchQuery } from './search';
const emptyArray = Object.freeze([]) as unknown as any[];
@ -217,10 +207,7 @@ export class GitProviderService implements Disposable {
readonly supportedSchemes = new Set<string>();
private readonly _bestRemotesCache = new Map<
RepoComparisonKey,
Promise<GitRemote<RemoteProvider | RichRemoteProvider>[]>
>();
private readonly _bestRemotesCache = new Map<RepoComparisonKey, Promise<GitRemote<RemoteProvider>[]>>();
private readonly _disposable: Disposable;
private _initializing: Deferred<number> | undefined;
private readonly _pendingRepositories = new Map<RepoComparisonKey, Promise<Repository | undefined>>();
@ -235,7 +222,7 @@ export class GitProviderService implements Disposable {
window.onDidChangeWindowState(this.onWindowStateChanged, this),
workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this),
configuration.onDidChange(this.onConfigurationChanged, this),
container.richRemoteProviders.onAfterDidChangeConnectionState(e => {
container.integrations.onDidChangeConnectionState(e => {
if (e.reason === 'connected') {
resetAvatarCache('failed');
}
@ -2048,78 +2035,6 @@ export class GitProviderService implements Disposable {
return provider.getPreviousComparisonUrisForLine(path, uri, editorLine, ref, skip);
}
@debug<GitProviderService['getMyPullRequests']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyPullRequests(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedPullRequest[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyPullRequests();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@debug<GitProviderService['getMyIssues']>({ args: { 0: remoteOrProvider => remoteOrProvider.name } })
async getMyIssues(
remoteOrProvider: GitRemote | RichRemoteProvider,
options?: { timeout?: number },
): Promise<SearchedIssue[] | undefined> {
let provider;
if (GitRemote.is(remoteOrProvider)) {
({ provider } = remoteOrProvider);
if (!provider?.hasRichIntegration()) return undefined;
} else {
provider = remoteOrProvider;
}
let timeout;
if (options != null) {
({ timeout, ...options } = options);
}
let promiseOrPRs = provider.searchMyIssues();
if (promiseOrPRs == null || !isPromise(promiseOrPRs)) {
return promiseOrPRs;
}
if (timeout != null && timeout > 0) {
promiseOrPRs = cancellable(promiseOrPRs, timeout);
}
try {
return await promiseOrPRs;
} catch (ex) {
if (ex instanceof PromiseCancelledError) throw ex;
return undefined;
}
}
@log()
async getIncomingActivity(
repoPath: string | Uri,
@ -2192,15 +2107,18 @@ export class GitProviderService implements Disposable {
// Only check remotes that have extra weighting and less than the default
if (weight > 0 && weight < 1000 && !originalFound) {
const p = remote.provider;
const integration = remote.getIntegration();
if (
p.hasRichIntegration() &&
(p.maybeConnected ||
(p.maybeConnected === undefined && p.shouldConnect && (await p.isConnected())))
integration != null &&
(integration.maybeConnected ||
(integration.maybeConnected === undefined && (await integration.isConnected())))
) {
if (cancellation?.isCancellationRequested) throw new CancellationError();
const repo = await p.getRepositoryMetadata(cancellation);
const repo = await integration.getRepositoryMetadata(
remote.provider.repoDesc,
cancellation,
);
if (cancellation?.isCancellationRequested) throw new CancellationError();
@ -2228,19 +2146,22 @@ export class GitProviderService implements Disposable {
}
@log()
async getBestRemoteWithRichProvider(
async getBestRemoteWithIntegration(
repoPath: string | Uri,
options?: { includeDisconnected?: boolean },
cancellation?: CancellationToken,
): Promise<GitRemote<RichRemoteProvider> | undefined> {
): Promise<GitRemote<RemoteProvider> | undefined> {
const remotes = await this.getBestRemotesWithProviders(repoPath, cancellation);
const includeDisconnected = options?.includeDisconnected ?? false;
for (const r of remotes) {
if (r.hasRichIntegration()) {
if (includeDisconnected || r.provider.maybeConnected === true) return r;
if (r.provider.maybeConnected === undefined && r.default) {
if (await r.provider.isConnected()) return r;
if (r.hasIntegration()) {
const provider = this.container.integrations.getByRemote(r);
if (provider != null) {
if (includeDisconnected || provider.maybeConnected === true) return r;
if (provider.maybeConnected === undefined && r.default) {
if (await provider.isConnected()) return r;
}
}
}
}
@ -2271,13 +2192,13 @@ export class GitProviderService implements Disposable {
}
@log()
async getRemotesWithRichProviders(
async getRemotesWithIntegrations(
repoPath: string | Uri,
options?: { sort?: boolean },
cancellation?: CancellationToken,
): Promise<GitRemote<RichRemoteProvider>[]> {
): Promise<GitRemote<RemoteProvider>[]> {
const remotes = await this.getRemotes(repoPath, options, cancellation);
return remotes.filter((r: GitRemote): r is GitRemote<RichRemoteProvider> => r.hasRichIntegration());
return remotes.filter((r: GitRemote): r is GitRemote<RemoteProvider> => r.hasIntegration());
}
getBestRepository(): Repository | undefined;

+ 9
- 6
src/git/models/branch.ts Vedi File

@ -106,12 +106,15 @@ export class GitBranch implements GitBranchReference {
include?: PullRequestState[];
}): Promise<PullRequest | undefined> {
const remote = await this.getRemote();
return remote?.hasRichIntegration()
? remote.provider.getPullRequestForBranch(
this.getTrackingWithoutRemote() ?? this.getNameWithoutRemote(),
options,
)
: undefined;
if (remote?.provider == null) return undefined;
return this.container.integrations
.getByRemote(remote)
?.getPullRequestForBranch(
remote.provider.repoDesc,
this.getTrackingWithoutRemote() ?? this.getNameWithoutRemote(),
options,
);
}
@memoize()

+ 7
- 4
src/git/models/commit.ts Vedi File

@ -416,15 +416,18 @@ export class GitCommit implements GitRevisionReference {
}
async getAssociatedPullRequest(remote?: GitRemote<RemoteProvider>): Promise<PullRequest | undefined> {
remote ??= await this.container.git.getBestRemoteWithRichProvider(this.repoPath);
return remote?.hasRichIntegration() ? remote.provider.getPullRequestForCommit(this.ref) : undefined;
remote ??= await this.container.git.getBestRemoteWithIntegration(this.repoPath);
if (!remote?.hasIntegration()) return undefined;
const provider = this.container.integrations.getByRemote(remote);
return provider?.getPullRequestForCommit(remote.provider.repoDesc, this.ref);
}
async getEnrichedAutolinks(remote?: GitRemote<RemoteProvider>): Promise<Map<string, EnrichedAutolink> | undefined> {
if (this.isUncommitted) return undefined;
remote ??= await this.container.git.getBestRemoteWithRichProvider(this.repoPath);
if (!remote?.hasRichIntegration()) return undefined;
remote ??= await this.container.git.getBestRemoteWithIntegration(this.repoPath);
if (remote?.provider == null) return undefined;
// TODO@eamodio should we cache these? Seems like we would use more memory than it's worth
// async function getCore(this: GitCommit): Promise<Map<string, EnrichedAutolink> | undefined> {

+ 15
- 8
src/git/models/remote.ts Vedi File

@ -2,17 +2,17 @@ import type { ColorTheme } from 'vscode';
import { Uri, window } from 'vscode';
import { GlyphChars } from '../../constants';
import { Container } from '../../container';
import type { ProviderIntegration } from '../../plus/integrations/providerIntegration';
import { memoize } from '../../system/decorators/memoize';
import { equalsIgnoreCase, sortCompare } from '../../system/string';
import { isLightTheme } from '../../system/utils';
import { parseGitRemoteUrl } from '../parsers/remoteParser';
import type { RemoteProvider } from '../remotes/remoteProvider';
import type { RichRemoteProvider } from '../remotes/richRemoteProvider';
export type GitRemoteType = 'fetch' | 'push';
export class GitRemote<TProvider extends RemoteProvider | undefined = RemoteProvider | RichRemoteProvider | undefined> {
static getHighlanderProviders(remotes: GitRemote<RemoteProvider | RichRemoteProvider>[]) {
export class GitRemote<TProvider extends RemoteProvider | undefined = RemoteProvider | undefined> {
static getHighlanderProviders(remotes: GitRemote<RemoteProvider>[]) {
if (remotes.length === 0) return undefined;
const remote = remotes.length === 1 ? remotes[0] : remotes.find(r => r.default);
@ -24,7 +24,7 @@ export class GitRemote
return undefined;
}
static getHighlanderProviderName(remotes: GitRemote<RemoteProvider | RichRemoteProvider>[]) {
static getHighlanderProviderName(remotes: GitRemote<RemoteProvider>[]) {
if (remotes.length === 0) return undefined;
const remote = remotes.length === 1 ? remotes[0] : remotes.find(r => r.default);
@ -52,6 +52,7 @@ export class GitRemote
}
constructor(
private readonly container: Container,
public readonly repoPath: string,
public readonly name: string,
public readonly scheme: string,
@ -77,6 +78,12 @@ export class GitRemote
return `${this.name}/${this.remoteKey}`;
}
get maybeIntegrationConnected(): boolean | undefined {
return this.provider == null || !this.container.integrations.supports(this.provider.id)
? false
: this.getIntegration()?.maybeConnected;
}
@memoize()
get path() {
return this.provider?.path ?? this._path;
@ -103,12 +110,12 @@ export class GitRemote
return bestUrl!;
}
hasRichIntegration(): this is GitRemote<RichRemoteProvider> {
return this.provider?.hasRichIntegration() ?? false;
getIntegration(): ProviderIntegration | undefined {
return this.provider != null ? this.container.integrations.getByRemote(this) : undefined;
}
get maybeConnected(): boolean | undefined {
return this.provider == null ? false : this.provider.maybeConnected;
hasIntegration(): this is GitRemote<RemoteProvider> {
return this.provider != null && this.container.integrations.supports(this.provider.id);
}
matches(url: string): boolean;

+ 6
- 0
src/git/models/remoteProvider.ts Vedi File

@ -4,3 +4,9 @@ export interface RemoteProviderReference {
readonly domain: string;
readonly icon: string;
}
export interface Provider extends RemoteProviderReference {
getIgnoreSSLErrors(): boolean | 'force';
reauthenticate(): Promise<void>;
trackRequestException(): void;
}

+ 3
- 3
src/git/models/repository.ts Vedi File

@ -25,7 +25,7 @@ import { updateRecordValue } from '../../system/object';
import { basename, normalizePath } from '../../system/path';
import { sortCompare } from '../../system/string';
import type { GitDir, GitProviderDescriptor, GitRepositoryCaches } from '../gitProvider';
import type { RichRemoteProvider } from '../remotes/richRemoteProvider';
import type { RemoteProvider } from '../remotes/remoteProvider';
import type { GitSearch, SearchQuery } from '../search';
import type { BranchSortOptions, GitBranch } from './branch';
import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from './branch';
@ -739,8 +739,8 @@ export class Repository implements Disposable {
return options?.filter != null ? remotes.filter(options.filter) : remotes;
}
async getRichRemote(connectedOnly: boolean = false): Promise<GitRemote<RichRemoteProvider> | undefined> {
return this.container.git.getBestRemoteWithRichProvider(this.uri, { includeDisconnected: !connectedOnly });
async getRichRemote(connectedOnly: boolean = false): Promise<GitRemote<RemoteProvider> | undefined> {
return this.container.git.getBestRemoteWithIntegration(this.uri, { includeDisconnected: !connectedOnly });
}
getStash(): Promise<GitStash | undefined> {

+ 13
- 8
src/git/parsers/remoteParser.ts Vedi File

@ -1,3 +1,4 @@
import type { Container } from '../../container';
import { maybeStopWatch } from '../../system/stopwatch';
import type { GitRemoteType } from '../models/remote';
import { GitRemote } from '../models/remote';
@ -8,6 +9,7 @@ const emptyStr = '';
const remoteRegex = /^(.*)\t(.*)\s\((.*)\)$/gm;
export function parseGitRemotes(
container: Container,
data: string,
repoPath: string,
remoteProviderMatcher: ReturnType<typeof getRemoteProviderMatcher>,
@ -45,22 +47,25 @@ export function parseGitRemotes(
remote = remotes.get(name);
if (remote == null) {
remote = new GitRemote(repoPath, name, scheme, domain, path, remoteProviderMatcher(url, domain, path), [
{ url: url, type: type as GitRemoteType },
]);
remote = new GitRemote(
container,
repoPath,
name,
scheme,
domain,
path,
remoteProviderMatcher(url, domain, path),
[{ url: url, type: type as GitRemoteType }],
);
remotes.set(name, remote);
} else {
remote.urls.push({ url: url, type: type as GitRemoteType });
if (remote.provider != null && type !== 'push') continue;
if (remote.provider?.hasRichIntegration()) {
remote.provider.dispose();
}
const provider = remoteProviderMatcher(url, domain, path);
if (provider == null) continue;
remote = new GitRemote(repoPath, name, scheme, domain, path, provider, remote.urls);
remote = new GitRemote(container, repoPath, name, scheme, domain, path, provider, remote.urls);
remotes.set(name, remote);
}
} while (true);

+ 16
- 245
src/git/remotes/github.ts Vedi File

@ -1,65 +1,32 @@
import type { AuthenticationSession, Disposable, QuickInputButton, Range } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import type { Range } from 'vscode';
import { Uri } from 'vscode';
import type { Autolink, DynamicAutolinkReference, MaybeEnrichedAutolink } from '../../annotations/autolinks';
import type { AutolinkReference } from '../../config';
import { GlyphChars } from '../../constants';
import type { Container } from '../../container';
import type { GkProviderId } from '../../gk/models/repositoryIdentities';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from '../../plus/integrationAuthentication';
import type { GitHubRepositoryDescriptor } from '../../plus/integrations/providers/github';
import type { Brand, Unbrand } from '../../system/brand';
import { fromNow } from '../../system/date';
import { log } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase, escapeMarkdown, unescapeMarkdown } from '../../system/string';
import { supportedInVSCodeVersion } from '../../system/utils';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import { getIssueOrPullRequestMarkdownIcon } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import type { RemoteProviderId } from './remoteProvider';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
import { RemoteProvider } from './remoteProvider';
const autolinkFullIssuesRegex = /\b([^/\s]+\/[^/\s]+?)(?:\\)?#([0-9]+)\b(?!]\()/g;
const fileRegex = /^\/([^/]+)\/([^/]+?)\/blob(.+)$/i;
const rangeRegex = /^L(\d+)(?:-L(\d+))?$/;
const authProvider = Object.freeze({ id: 'github', scopes: ['repo', 'read:user', 'user:email'] });
const enterpriseAuthProvider = Object.freeze({ id: 'github-enterprise', scopes: ['repo', 'read:user', 'user:email'] });
function isGitHubDotCom(domain: string): boolean {
return equalsIgnoreCase(domain, 'github.com');
}
type GitHubRepositoryDescriptor =
| {
owner: string;
name: string;
}
| Record<string, never>;
export class GitHubRemote extends RichRemoteProvider<GitHubRepositoryDescriptor> {
@memoize()
protected get authProvider() {
return isGitHubDotCom(this.domain) ? authProvider : enterpriseAuthProvider;
}
constructor(
container: Container,
domain: string,
path: string,
protocol?: string,
name?: string,
custom: boolean = false,
) {
super(container, domain, path, protocol, name, custom);
export class GitHubRemote extends RemoteProvider<GitHubRepositoryDescriptor> {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
get apiBaseUrl() {
@ -174,7 +141,11 @@ export class GitHubRemote extends RichRemoteProvider
description: `${this.name} Issue or Pull Request ${ownerAndRepo}#${num}`,
descriptor: { owner: owner, name: repo } satisfies GitHubRepositoryDescriptor,
descriptor: {
key: this.remoteKey,
owner: owner,
name: repo,
} satisfies GitHubRepositoryDescriptor,
});
} while (true);
},
@ -207,15 +178,10 @@ export class GitHubRemote extends RichRemoteProvider
return this.formatName('GitHub');
}
@log()
override async connect(): Promise<boolean> {
if (!isGitHubDotCom(this.domain)) {
if (!(await ensurePaidPlan('GitHub Enterprise instance', this.container))) {
return false;
}
}
return super.connect();
@memoize()
override get repoDesc(): GitHubRepositoryDescriptor {
const [owner, repo] = this.splitPath();
return { key: this.remoteKey, owner: owner, name: repo };
}
async getLocalInfoFromRemoteUri(
@ -327,117 +293,6 @@ export class GitHubRemote extends RichRemoteProvider
if (branch) return `${this.encodeUrl(`${this.baseUrl}/blob/${branch}/${fileName}`)}${line}`;
return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`;
}
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getAccountForCommit(this, accessToken, owner, repo, ref, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getAccountForEmail(this, accessToken, owner, repo, email, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderDefaultBranch({
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getDefaultBranch(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
id: string,
descriptor: GitHubRepositoryDescriptor | undefined,
): Promise<IssueOrPullRequest | undefined> {
let owner;
let repo;
if (descriptor != null) {
({ owner, name: repo } = descriptor);
} else {
[owner, repo] = this.splitPath();
}
return (await this.container.github)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
const { include, ...opts } = options ?? {};
const toGitHubPullRequestState = (await import(/* webpackChunkName: "github" */ '../../plus/github/models'))
.toGitHubPullRequestState;
return (await this.container.github)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, {
...opts,
include: include?.map(s => toGitHubPullRequestState(s)),
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.github)?.getRepositoryMetadata(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyPullRequests({
accessToken,
}: AuthenticationSession): Promise<SearchedPullRequest[] | undefined> {
return (await this.container.github)?.searchMyPullRequests(this, accessToken, {
repos: [this.path],
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyIssues({
accessToken,
}: AuthenticationSession): Promise<SearchedIssue[] | undefined> {
return (await this.container.github)?.searchMyIssues(this, accessToken, {
repos: [this.path],
baseUrl: this.apiBaseUrl,
});
}
}
const gitHubNoReplyAddressRegex = /^(?:(\d+)\+)?([a-zA-Z\d-]{1,39})@users\.noreply\.(.*)$/i;
@ -451,87 +306,3 @@ export function getGitHubNoReplyAddressParts(
const [, userId, login, authority] = match;
return { userId: userId, login: login, authority: authority };
}
export class GitHubAuthenticationProvider implements Disposable, IntegrationAuthenticationProvider {
private readonly _disposable: Disposable;
constructor(container: Container) {
this._disposable = container.integrationAuthentication.registerProvider('github-enterprise', this);
}
dispose() {
this._disposable.dispose();
}
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
input.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the GitHub Access Tokens Page',
};
token = await new Promise<string | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve(undefined)),
input.onDidChangeValue(() => (input.validationMessage = undefined)),
input.onDidAccept(() => {
const value = input.value.trim();
if (!value) {
input.validationMessage = 'A personal access token is required';
return;
}
resolve(value);
}),
input.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(`https://${descriptor?.domain ?? 'github.com'}/settings/tokens`),
);
}
}),
);
input.password = true;
input.title = `GitHub Authentication${descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''}`;
input.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
input.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [GitHub Personal Access Token](https://${
descriptor?.domain ?? 'github.com'
}/settings/tokens "Get your GitHub Access Token")`
: 'Paste your GitHub Personal Access Token';
input.buttons = [infoButton];
input.show();
});
} finally {
input.dispose();
disposables.forEach(d => void d.dispose());
}
if (!token) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: token,
scopes: [],
account: {
id: '',
label: '',
},
};
}
}

+ 21
- 239
src/git/remotes/gitlab.ts Vedi File

@ -1,63 +1,32 @@
import type { AuthenticationSession, Disposable, QuickInputButton, Range } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import type { Range, Uri } from 'vscode';
import type { Autolink, DynamicAutolinkReference, MaybeEnrichedAutolink } from '../../annotations/autolinks';
import type { AutolinkReference } from '../../config';
import { GlyphChars } from '../../constants';
import type { Container } from '../../container';
import type { GkProviderId } from '../../gk/models/repositoryIdentities';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from '../../plus/integrationAuthentication';
import type { GitLabRepositoryDescriptor } from '../../plus/integrations/providers/gitlab';
import type { Brand, Unbrand } from '../../system/brand';
import { fromNow } from '../../system/date';
import { log } from '../../system/decorators/log';
import { memoize } from '../../system/decorators/memoize';
import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase, escapeMarkdown, unescapeMarkdown } from '../../system/string';
import { supportedInVSCodeVersion } from '../../system/utils';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import { getIssueOrPullRequestMarkdownIcon } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import type { RemoteProviderId } from './remoteProvider';
import { ensurePaidPlan, RichRemoteProvider } from './richRemoteProvider';
import { RemoteProvider } from './remoteProvider';
const autolinkFullIssuesRegex = /\b([^/\s]+\/[^/\s]+?)(?:\\)?#([0-9]+)\b(?!]\()/g;
const autolinkFullMergeRequestsRegex = /\b([^/\s]+\/[^/\s]+?)(?:\\)?!([0-9]+)\b(?!]\()/g;
const fileRegex = /^\/([^/]+)\/([^/]+?)\/-\/blob(.+)$/i;
const rangeRegex = /^L(\d+)(?:-(\d+))?$/;
const authProvider = Object.freeze({ id: 'gitlab', scopes: ['read_api', 'read_user', 'read_repository'] });
function isGitLabDotCom(domain: string): boolean {
return equalsIgnoreCase(domain, 'gitlab.com');
}
type GitLabRepositoryDescriptor =
| {
owner: string;
name: string;
}
| Record<string, never>;
export class GitLabRemote extends RichRemoteProvider<GitLabRepositoryDescriptor> {
protected get authProvider() {
return authProvider;
}
constructor(
container: Container,
domain: string,
path: string,
protocol?: string,
name?: string,
custom: boolean = false,
) {
super(container, domain, path, protocol, name, custom);
export class GitLabRemote extends RemoteProvider<GitLabRepositoryDescriptor> {
constructor(domain: string, path: string, protocol?: string, name?: string, custom: boolean = false) {
super(domain, path, protocol, name, custom);
}
get apiBaseUrl() {
@ -169,7 +138,11 @@ export class GitLabRemote extends RichRemoteProvider
type: 'issue',
description: `${this.name} Issue ${ownerAndRepo}#${num}`,
descriptor: { owner: owner, name: repo } satisfies GitLabRepositoryDescriptor,
descriptor: {
key: this.remoteKey,
owner: owner,
name: repo,
} satisfies GitLabRepositoryDescriptor,
});
} while (true);
},
@ -266,7 +239,11 @@ export class GitLabRemote extends RichRemoteProvider
type: 'pullrequest',
description: `${this.name} Merge Request !${num} from ${ownerAndRepo}`,
descriptor: { owner: owner, name: repo } satisfies GitLabRepositoryDescriptor,
descriptor: {
key: this.remoteKey,
owner: owner,
name: repo,
} satisfies GitLabRepositoryDescriptor,
});
} while (true);
},
@ -294,15 +271,10 @@ export class GitLabRemote extends RichRemoteProvider
return this.formatName('GitLab');
}
@log()
override async connect(): Promise<boolean> {
if (!equalsIgnoreCase(this.domain, 'gitlab.com')) {
if (!(await ensurePaidPlan('GitLab self-managed instance', this.container))) {
return false;
}
}
return super.connect();
@memoize()
override get repoDesc(): GitLabRepositoryDescriptor {
const [owner, repo] = this.splitPath();
return { key: this.remoteKey, owner: owner, name: repo };
}
async getLocalInfoFromRemoteUri(
@ -402,194 +374,4 @@ export class GitLabRemote extends RichRemoteProvider
if (branch) return `${this.encodeUrl(`${this.baseUrl}/-/blob/${branch}/${fileName}`)}${line}`;
return `${this.encodeUrl(`${this.baseUrl}?path=${fileName}`)}${line}`;
}
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getAccountForCommit(this, accessToken, owner, repo, ref, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getAccountForEmail(this, accessToken, owner, repo, email, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderDefaultBranch({
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getDefaultBranch(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
id: string,
descriptor: GitLabRepositoryDescriptor | undefined,
): Promise<IssueOrPullRequest | undefined> {
let owner;
let repo;
if (descriptor != null) {
({ owner, name: repo } = descriptor);
} else {
[owner, repo] = this.splitPath();
}
return (await this.container.gitlab)?.getIssueOrPullRequest(this, accessToken, owner, repo, Number(id), {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
const { include, ...opts } = options ?? {};
const toGitLabMergeRequestState = (await import(/* webpackChunkName: "gitlab" */ '../../plus/gitlab/models'))
.toGitLabMergeRequestState;
return (await this.container.gitlab)?.getPullRequestForBranch(this, accessToken, owner, repo, branch, {
...opts,
include: include?.map(s => toGitLabMergeRequestState(s)),
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
ref: string,
): Promise<PullRequest | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getPullRequestForCommit(this, accessToken, owner, repo, ref, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined> {
const [owner, repo] = this.splitPath();
return (await this.container.gitlab)?.getRepositoryMetadata(this, accessToken, owner, repo, {
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyPullRequests(
_session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected override async searchProviderMyIssues(
_session: AuthenticationSession,
): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}
export class GitLabAuthenticationProvider implements Disposable, IntegrationAuthenticationProvider {
private readonly _disposable: Disposable;
constructor(container: Container) {
this._disposable = container.integrationAuthentication.registerProvider('gitlab', this);
}
dispose() {
this._disposable.dispose();
}
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
input.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the GitLab Access Tokens Page',
};
token = await new Promise<string | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve(undefined)),
input.onDidChangeValue(() => (input.validationMessage = undefined)),
input.onDidAccept(() => {
const value = input.value.trim();
if (!value) {
input.validationMessage = 'A personal access token is required';
return;
}
resolve(value);
}),
input.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(
`https://${descriptor?.domain ?? 'gitlab.com'}/-/profile/personal_access_tokens`,
),
);
}
}),
);
input.password = true;
input.title = `GitLab Authentication${descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''}`;
input.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
input.prompt = input.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [GitLab Personal Access Token](https://${
descriptor?.domain ?? 'gitlab.com'
}/-/profile/personal_access_tokens "Get your GitLab Access Token")`
: 'Paste your GitLab Personal Access Token';
input.buttons = [infoButton];
input.show();
});
} finally {
input.dispose();
disposables.forEach(d => void d.dispose());
}
if (!token) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: token,
scopes: [],
account: {
id: '',
label: '',
},
};
}
}

+ 13
- 16
src/git/remotes/remoteProvider.ts Vedi File

@ -3,13 +3,13 @@ import { env } from 'vscode';
import type { DynamicAutolinkReference } from '../../annotations/autolinks';
import type { AutolinkReference } from '../../config';
import type { GkProviderId } from '../../gk/models/repositoryIdentities';
import type { RepositoryDescriptor } from '../../plus/integrations/providerIntegration';
import { memoize } from '../../system/decorators/memoize';
import { encodeUrl } from '../../system/encoding';
import type { RemoteProviderReference } from '../models/remoteProvider';
import type { RemoteResource } from '../models/remoteResource';
import { RemoteResourceType } from '../models/remoteResource';
import type { Repository } from '../models/repository';
import type { RichRemoteProvider } from './richRemoteProvider';
export type RemoteProviderId =
| 'azure-devops'
@ -22,8 +22,9 @@ export type RemoteProviderId =
| 'gitlab'
| 'google-source';
export abstract class RemoteProvider implements RemoteProviderReference {
readonly type: 'simple' | 'rich' = 'simple';
export abstract class RemoteProvider<T extends RepositoryDescriptor = RepositoryDescriptor>
implements RemoteProviderReference
{
protected readonly _name: string | undefined;
constructor(
@ -36,11 +37,6 @@ export abstract class RemoteProvider implements RemoteProviderReference {
this._name = name;
}
@memoize()
get remoteKey() {
return this.domain ? `${this.domain}/${this.path}` : this.path;
}
get autolinks(): (AutolinkReference | DynamicAutolinkReference)[] {
return [];
}
@ -61,6 +57,15 @@ export abstract class RemoteProvider implements RemoteProviderReference {
return this.splitPath()[0];
}
@memoize()
get remoteKey() {
return this.domain ? `${this.domain}/${this.path}` : this.path;
}
get repoDesc(): T {
return { owner: this.owner, name: this.repoName } as unknown as T;
}
get repoName(): string | undefined {
return this.splitPath()[1];
}
@ -78,14 +83,6 @@ export abstract class RemoteProvider implements RemoteProviderReference {
await env.clipboard.writeText(url);
}
hasRichIntegration(): this is RichRemoteProvider {
return this.type === 'rich';
}
get maybeConnected(): boolean | undefined {
return false;
}
abstract getLocalInfoFromRemoteUri(
repository: Repository,
uri: Uri,

+ 0
- 49
src/git/remotes/remoteProviderService.ts Vedi File

@ -1,49 +0,0 @@
import type { Event } from 'vscode';
import { EventEmitter } from 'vscode';
import type { Container } from '../../container';
export interface ConnectionStateChangeEvent {
key: string;
reason: 'connected' | 'disconnected';
}
export class RichRemoteProviderService {
private readonly _onDidChangeConnectionState = new EventEmitter<ConnectionStateChangeEvent>();
get onDidChangeConnectionState(): Event<ConnectionStateChangeEvent> {
return this._onDidChangeConnectionState.event;
}
private readonly _onAfterDidChangeConnectionState = new EventEmitter<ConnectionStateChangeEvent>();
get onAfterDidChangeConnectionState(): Event<ConnectionStateChangeEvent> {
return this._onAfterDidChangeConnectionState.event;
}
private readonly _connectedCache = new Set<string>();
constructor(private readonly container: Container) {}
connected(key: string): void {
// Only fire events if the key is being connected for the first time
if (this._connectedCache.has(key)) return;
this._connectedCache.add(key);
this.container.telemetry.sendEvent('remoteProviders/connected', { 'remoteProviders.key': key });
this._onDidChangeConnectionState.fire({ key: key, reason: 'connected' });
setTimeout(() => this._onAfterDidChangeConnectionState.fire({ key: key, reason: 'connected' }), 250);
}
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;
this._connectedCache.delete(key);
this.container.telemetry.sendEvent('remoteProviders/disconnected', { 'remoteProviders.key': key });
this._onDidChangeConnectionState.fire({ key: key, reason: 'disconnected' });
setTimeout(() => this._onAfterDidChangeConnectionState.fire({ key: key, reason: 'disconnected' }), 250);
}
isConnected(key?: string): boolean {
return key == null ? this._connectedCache.size !== 0 : this._connectedCache.has(key);
}
}

+ 5
- 5
src/git/remotes/remoteProviders.ts Vedi File

@ -28,12 +28,12 @@ const builtInProviders: RemoteProviders = [
{
custom: false,
matcher: 'github.com',
creator: (container: Container, domain: string, path: string) => new GitHubRemote(container, domain, path),
creator: (container: Container, domain: string, path: string) => new GitHubRemote(domain, path),
},
{
custom: false,
matcher: 'gitlab.com',
creator: (container: Container, domain: string, path: string) => new GitLabRemote(container, domain, path),
creator: (container: Container, domain: string, path: string) => new GitLabRemote(domain, path),
},
{
custom: false,
@ -48,7 +48,7 @@ const builtInProviders: RemoteProviders = [
{
custom: false,
matcher: /\bgitlab\b/i,
creator: (container: Container, domain: string, path: string) => new GitLabRemote(container, domain, path),
creator: (container: Container, domain: string, path: string) => new GitLabRemote(domain, path),
},
{
custom: false,
@ -127,10 +127,10 @@ function getCustomProviderCreator(cfg: RemotesConfig) {
new GiteaRemote(domain, path, cfg.protocol, cfg.name, true);
case 'GitHub':
return (container: Container, domain: string, path: string) =>
new GitHubRemote(container, domain, path, cfg.protocol, cfg.name, true);
new GitHubRemote(domain, path, cfg.protocol, cfg.name, true);
case 'GitLab':
return (container: Container, domain: string, path: string) =>
new GitLabRemote(container, domain, path, cfg.protocol, cfg.name, true);
new GitLabRemote(domain, path, cfg.protocol, cfg.name, true);
default:
return undefined;
}

+ 0
- 621
src/git/remotes/richRemoteProvider.ts Vedi File

@ -1,621 +0,0 @@
/* eslint-disable @typescript-eslint/no-confusing-void-expression */
import type {
AuthenticationSession,
AuthenticationSessionsChangeEvent,
CancellationToken,
Event,
MessageItem,
} from 'vscode';
import { authentication, CancellationError, Disposable, EventEmitter, window } from 'vscode';
import { wrapForForcedInsecureSSL } from '@env/fetch';
import { isWeb } from '@env/platform';
import type { Container } from '../../container';
import { AuthenticationError, ProviderRequestClientError } from '../../errors';
import { showIntegrationDisconnectedTooManyFailedRequestsWarningMessage } from '../../messages';
import { isSubscriptionPaidPlan, isSubscriptionPreviewTrialExpired } from '../../plus/gk/account/subscription';
import type { IntegrationAuthenticationSessionDescriptor } from '../../plus/integrationAuthentication';
import { configuration } from '../../system/configuration';
import { gate } from '../../system/decorators/gate';
import { debug, log, logName } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import type { LogScope } from '../../system/logger.scope';
import { getLogScope } from '../../system/logger.scope';
import type { Account } from '../models/author';
import type { DefaultBranch } from '../models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../models/pullRequest';
import type { RepositoryMetadata } from '../models/repositoryMetadata';
import { RemoteProvider } from './remoteProvider';
// TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart
export type RepositoryDescriptor = Record<string, string>;
@logName<RichRemoteProvider>((c, name) => `${name}(${c.remoteKey})`)
export abstract class RichRemoteProvider<T extends RepositoryDescriptor = RepositoryDescriptor>
extends RemoteProvider
implements Disposable
{
override readonly type: 'simple' | 'rich' = 'rich';
private readonly _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> {
return this._onDidChange.event;
}
private readonly _disposable: Disposable;
constructor(
protected readonly container: Container,
domain: string,
path: string,
protocol?: string,
name?: string,
custom?: boolean,
) {
super(domain, path, protocol, name, custom);
this._disposable = Disposable.from(
configuration.onDidChange(e => {
if (configuration.changed(e, 'remotes')) {
this._ignoreSSLErrors.clear();
}
}),
// TODO@eamodio revisit how connections are linked or not
container.richRemoteProviders.onDidChangeConnectionState(e => {
if (e.key !== this.key) return;
if (e.reason === 'disconnected') {
void this.disconnect({ silent: true });
} else if (e.reason === 'connected') {
void this.ensureSession(false);
}
}),
authentication.onDidChangeSessions(this.onAuthenticationSessionsChanged, this),
);
container.context.subscriptions.push(this._disposable);
// If we think we should be connected, try to
if (this.shouldConnect) {
void this.isConnected();
}
}
disposed = false;
dispose() {
this._disposable.dispose();
this.disposed = true;
}
abstract get apiBaseUrl(): string;
protected abstract get authProvider(): { id: string; scopes: string[] };
protected get authProviderDescriptor(): IntegrationAuthenticationSessionDescriptor {
return { domain: this.domain, scopes: this.authProvider.scopes };
}
private get key() {
return this.custom ? `${this.name}:${this.domain}` : this.name;
}
private get connectedKey(): `connected:${string}` {
return `connected:${this.key}`;
}
override get maybeConnected(): boolean | undefined {
return this._session === undefined ? undefined : this._session !== null;
}
// This is a hack for now, since providers come and go with remotes
get shouldConnect(): boolean {
return this.container.richRemoteProviders.isConnected(this.key);
}
protected _session: AuthenticationSession | null | undefined;
protected session() {
if (this._session === undefined) {
return this.ensureSession(false);
}
return this._session ?? undefined;
}
private onAuthenticationSessionsChanged(e: AuthenticationSessionsChangeEvent) {
if (e.provider.id === this.authProvider.id) {
void this.ensureSession(false);
}
}
@log()
async connect(): Promise<boolean> {
try {
const session = await this.ensureSession(true);
return Boolean(session);
} catch (ex) {
return false;
}
}
@gate()
@log()
async disconnect(options?: { silent?: boolean; currentSessionOnly?: boolean }): Promise<void> {
if (options?.currentSessionOnly && this._session === null) return;
const connected = this._session != null;
if (connected && !options?.silent) {
if (options?.currentSessionOnly) {
void showIntegrationDisconnectedTooManyFailedRequestsWarningMessage(this.name);
} else {
const disable = { title: 'Disable' };
const signout = { title: 'Disable & Sign Out' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
let result: MessageItem | undefined;
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 },
disable,
signout,
cancel,
);
} else {
result = await window.showWarningMessage(
`Are you sure you want to disable the rich integration with ${this.name}?`,
{ modal: true },
disable,
cancel,
);
}
if (result == null || result === cancel) return;
if (result === signout) {
void this.container.integrationAuthentication.deleteSession(
this.authProvider.id,
this.authProviderDescriptor,
);
}
}
}
this.resetRequestExceptionCount();
this._session = null;
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 this.container.storage.storeWorkspace(this.connectedKey, false);
}
this._onDidChange.fire();
if (!options?.silent && !options?.currentSessionOnly) {
this.container.richRemoteProviders.disconnected(this.key);
}
}
}
@log()
async reauthenticate(): Promise<void> {
if (this._session === undefined) return;
this._session = undefined;
void (await this.ensureSession(true, true));
}
private requestExceptionCount = 0;
resetRequestExceptionCount() {
this.requestExceptionCount = 0;
}
private handleProviderException<T>(ex: Error, scope: LogScope | undefined, defaultValue: T): T {
if (ex instanceof CancellationError) return defaultValue;
Logger.error(ex, scope);
if (ex instanceof AuthenticationError || ex instanceof ProviderRequestClientError) {
this.trackRequestException();
}
return defaultValue;
}
@debug()
trackRequestException() {
this.requestExceptionCount++;
if (this.requestExceptionCount >= 5 && this._session !== null) {
void this.disconnect({ currentSessionOnly: true });
}
}
@gate()
@debug({ exit: true })
async isConnected(): Promise<boolean> {
return (await this.session()) != null;
}
@gate()
@debug()
async getAccountForCommit(
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.getProviderAccountForCommit(this._session!, ref, options);
this.resetRequestExceptionCount();
return author;
} catch (ex) {
return this.handleProviderException(ex, scope, undefined);
}
}
protected abstract getProviderAccountForCommit(
session: AuthenticationSession,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
@gate()
@debug()
async getAccountForEmail(
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.getProviderAccountForEmail(this._session!, email, options);
this.resetRequestExceptionCount();
return author;
} catch (ex) {
return this.handleProviderException(ex, scope, undefined);
}
}
protected abstract getProviderAccountForEmail(
session: AuthenticationSession,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
@debug()
async getDefaultBranch(): Promise<DefaultBranch | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const defaultBranch = this.container.cache.getRepositoryDefaultBranch(this, () => ({
value: (async () => {
try {
const result = await this.getProviderDefaultBranch(this._session!);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<DefaultBranch | undefined>(ex, scope, undefined);
}
})(),
}));
return defaultBranch;
}
protected abstract getProviderDefaultBranch({
accessToken,
}: AuthenticationSession): Promise<DefaultBranch | undefined>;
private _ignoreSSLErrors = new Map<string, boolean | 'force'>();
getIgnoreSSLErrors(): boolean | 'force' {
if (isWeb) return false;
let ignoreSSLErrors = this._ignoreSSLErrors.get(this.id);
if (ignoreSSLErrors === undefined) {
const cfg = configuration
.get('remotes')
?.find(remote => remote.type.toLowerCase() === this.id && remote.domain === this.domain);
ignoreSSLErrors = cfg?.ignoreSSLErrors ?? false;
this._ignoreSSLErrors.set(this.id, ignoreSSLErrors);
}
return ignoreSSLErrors;
}
@debug()
async getRepositoryMetadata(_cancellation?: CancellationToken): Promise<RepositoryMetadata | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const metadata = this.container.cache.getRepositoryMetadata(this, () => ({
value: (async () => {
try {
const result = await this.getProviderRepositoryMetadata(this._session!);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<RepositoryMetadata | undefined>(ex, scope, undefined);
}
})(),
}));
return metadata;
}
protected abstract getProviderRepositoryMetadata({
accessToken,
}: AuthenticationSession): Promise<RepositoryMetadata | undefined>;
@debug()
async getIssueOrPullRequest(id: string, repo: T | undefined): Promise<IssueOrPullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const issueOrPR = this.container.cache.getIssueOrPullRequest(id, repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderIssueOrPullRequest(this._session!, id, repo);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<IssueOrPullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return issueOrPR;
}
protected abstract getProviderIssueOrPullRequest(
session: AuthenticationSession,
id: string,
repo: T | undefined,
): Promise<IssueOrPullRequest | undefined>;
@debug()
async getPullRequestForBranch(
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const pr = this.container.cache.getPullRequestForBranch(branch, this, () => ({
value: (async () => {
try {
const result = await this.getProviderPullRequestForBranch(this._session!, branch, options);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<PullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return pr;
}
protected abstract getProviderPullRequestForBranch(
session: AuthenticationSession,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined>;
@debug()
async getPullRequestForCommit(ref: string): Promise<PullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const pr = this.container.cache.getPullRequestForSha(ref, this, () => ({
value: (async () => {
try {
const result = await this.getProviderPullRequestForCommit(this._session!, ref);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<PullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return pr;
}
protected abstract getProviderPullRequestForCommit(
session: AuthenticationSession,
ref: string,
): Promise<PullRequest | undefined>;
@gate()
@debug()
async searchMyIssues(): Promise<SearchedIssue[] | undefined> {
const scope = getLogScope();
try {
const issues = await this.searchProviderMyIssues(this._session!);
this.resetRequestExceptionCount();
return issues;
} catch (ex) {
return this.handleProviderException(ex, scope, undefined);
}
}
protected abstract searchProviderMyIssues(session: AuthenticationSession): Promise<SearchedIssue[] | undefined>;
@gate()
@debug()
async searchMyPullRequests(): Promise<SearchedPullRequest[] | undefined> {
const scope = getLogScope();
try {
const pullRequests = await this.searchProviderMyPullRequests(this._session!);
this.resetRequestExceptionCount();
return pullRequests;
} catch (ex) {
return this.handleProviderException(ex, scope, undefined);
}
}
protected abstract searchProviderMyPullRequests(
session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined>;
@gate()
private async ensureSession(
createIfNeeded: boolean,
forceNewSession: boolean = false,
): Promise<AuthenticationSession | undefined> {
if (this._session != null) return this._session;
if (!configuration.get('integrations.enabled')) return undefined;
if (createIfNeeded) {
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 (this.container.integrationAuthentication.hasProvider(this.authProvider.id)) {
session = await this.container.integrationAuthentication.getSession(
this.authProvider.id,
this.authProviderDescriptor,
{ createIfNeeded: createIfNeeded, forceNewSession: forceNewSession },
);
} else {
session = await wrapForForcedInsecureSSL(this.getIgnoreSSLErrors(), () =>
authentication.getSession(this.authProvider.id, this.authProvider.scopes, {
createIfNone: forceNewSession ? undefined : createIfNeeded,
silent: !createIfNeeded && !forceNewSession ? true : undefined,
forceNewSession: forceNewSession ? true : undefined,
}),
);
}
} catch (ex) {
await this.container.storage.deleteWorkspace(this.connectedKey);
if (ex instanceof Error && ex.message.includes('User did not consent')) {
return undefined;
}
session = null;
}
if (session === undefined && !createIfNeeded) {
await this.container.storage.deleteWorkspace(this.connectedKey);
}
this._session = session ?? null;
this.resetRequestExceptionCount();
if (session != null) {
await this.container.storage.storeWorkspace(this.connectedKey, true);
queueMicrotask(() => {
this._onDidChange.fire();
this.container.richRemoteProviders.connected(this.key);
});
}
return session ?? undefined;
}
}
export async function ensurePaidPlan(providerName: string, container: Container): Promise<boolean> {
const title = `Connecting to a ${providerName} instance for rich integration features requires a trial or paid plan.`;
while (true) {
const subscription = await container.subscription.getSubscription();
if (subscription.account?.verified === false) {
const resend = { title: 'Resend Verification' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nYou must verify your email before you can continue.`,
{ modal: true },
resend,
cancel,
);
if (result === resend) {
if (await container.subscription.resendVerification()) {
continue;
}
}
return false;
}
const plan = subscription.plan.effective.id;
if (isSubscriptionPaidPlan(plan)) break;
if (subscription.account == null && !isSubscriptionPreviewTrialExpired(subscription)) {
const startTrial = { title: 'Preview Pro' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to preview ✨ features for 3 days?`,
{ modal: true },
startTrial,
cancel,
);
if (result !== startTrial) return false;
void container.subscription.startPreviewTrial();
break;
} else if (subscription.account == null) {
const signIn = { title: 'Start Free GitKraken Trial' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to continue to use ✨ features on privately hosted repos, free for an additional 7 days?`,
{ modal: true },
signIn,
cancel,
);
if (result === signIn) {
if (await container.subscription.loginOrSignUp()) {
continue;
}
}
} else {
const upgrade = { title: 'Upgrade to Pro' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to continue to use ✨ features on privately hosted repos?`,
{ modal: true },
upgrade,
cancel,
);
if (result === upgrade) {
void container.subscription.purchase();
}
}
return false;
}
return true;
}

+ 2
- 2
src/hovers/hovers.ts Vedi File

@ -229,8 +229,8 @@ export async function detailsMessage(
(options?.autolinks || (options?.autolinks !== false && cfg.autolinks.enabled && cfg.autolinks.enhanced)) &&
CommitFormatter.has(cfg.detailsMarkdownFormat, 'message');
const prs =
remote?.hasRichIntegration() &&
remote.provider.maybeConnected !== false &&
remote?.hasIntegration() &&
remote.maybeIntegrationConnected !== false &&
(options?.pullRequests || (options?.pullRequests !== false && cfg.pullRequests.enabled)) &&
CommitFormatter.has(
options.format,

+ 2
- 2
src/plus/focus/focusService.ts Vedi File

@ -1,7 +1,7 @@
import type { Disposable } from 'vscode';
import type { Container } from '../../container';
import type { GitRemote } from '../../git/models/remote';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import type { RemoteProvider } from '../../git/remotes/remoteProvider';
import { log } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import { getLogScope } from '../../system/logger.scope';
@ -11,7 +11,7 @@ import { ensureAccount, ensurePaidPlan } from '../utils';
export interface FocusItem {
type: EnrichedItemResponse['entityType'];
id: string;
remote: GitRemote<RichRemoteProvider>;
remote: GitRemote<RemoteProvider>;
url: string;
}

+ 0
- 116
src/plus/integrationAuthentication.ts Vedi File

@ -1,116 +0,0 @@
import type { AuthenticationSession, Disposable } from 'vscode';
import type { Container } from '../container';
import { debug } from '../system/decorators/log';
interface StoredSession {
id: string;
accessToken: string;
account?: {
label?: string;
displayName?: string;
id: string;
};
scopes: string[];
}
export interface IntegrationAuthenticationSessionDescriptor {
domain: string;
scopes: string[];
[key: string]: unknown;
}
export interface IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string;
createSession(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<AuthenticationSession | undefined>;
}
export class IntegrationAuthenticationService implements Disposable {
private readonly providers = new Map<string, IntegrationAuthenticationProvider>();
constructor(private readonly container: Container) {}
dispose() {
this.providers.clear();
}
registerProvider(providerId: string, provider: IntegrationAuthenticationProvider): Disposable {
if (this.providers.has(providerId)) throw new Error(`Provider with id ${providerId} already registered`);
this.providers.set(providerId, provider);
return {
dispose: () => this.providers.delete(providerId),
};
}
hasProvider(providerId: string): boolean {
return this.providers.has(providerId);
}
@debug()
async createSession(
providerId: string,
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const provider = this.providers.get(providerId);
if (provider == null) throw new Error(`Provider with id ${providerId} not registered`);
const session = await provider?.createSession(descriptor);
if (session == null) return undefined;
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
await this.container.storage.storeSecret(key, JSON.stringify(session));
return session;
}
@debug()
async getSession(
providerId: string,
descriptor?: IntegrationAuthenticationSessionDescriptor,
options?: { createIfNeeded?: boolean; forceNewSession?: boolean },
): Promise<AuthenticationSession | undefined> {
const provider = this.providers.get(providerId);
if (provider == null) throw new Error(`Provider with id ${providerId} not registered`);
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
if (options?.forceNewSession) {
await this.container.storage.deleteSecret(key);
}
let storedSession: StoredSession | undefined;
try {
const sessionJSON = await this.container.storage.getSecret(key);
if (sessionJSON) {
storedSession = JSON.parse(sessionJSON);
}
} catch (ex) {
try {
await this.container.storage.deleteSecret(key);
} catch {}
if (!options?.createIfNeeded) {
throw ex;
}
}
if (options?.createIfNeeded && storedSession == null) {
return this.createSession(providerId, descriptor);
}
return storedSession as AuthenticationSession | undefined;
}
@debug()
async deleteSession(providerId: string, descriptor?: IntegrationAuthenticationSessionDescriptor) {
const provider = this.providers.get(providerId);
if (provider == null) throw new Error(`Provider with id ${providerId} not registered`);
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
await this.container.storage.deleteSecret(key);
}
private getSecretKey(providerId: string, id: string): `gitlens.integration.auth:${string}` {
return `gitlens.integration.auth:${providerId}|${id}`;
}
}

+ 123
- 0
src/plus/integrations/authentication/azureDevOps.ts Vedi File

@ -0,0 +1,123 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { base64 } from '../../../system/string';
import { supportedInVSCodeVersion } from '../../../system/utils';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
export class AzureDevOpsAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
let azureOrganization: string | undefined = descriptor?.organization as string | undefined;
if (!azureOrganization) {
const orgInput = window.createInputBox();
orgInput.ignoreFocusOut = true;
const orgInputDisposables: Disposable[] = [];
try {
azureOrganization = await new Promise<string | undefined>(resolve => {
orgInputDisposables.push(
orgInput.onDidHide(() => resolve(undefined)),
orgInput.onDidChangeValue(() => (orgInput.validationMessage = undefined)),
orgInput.onDidAccept(() => {
const value = orgInput.value.trim();
if (!value) {
orgInput.validationMessage = 'An organization is required';
return;
}
resolve(value);
}),
);
orgInput.title = `Azure DevOps Authentication${
descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''
}`;
orgInput.placeholder = 'Organization';
orgInput.prompt = 'Enter your Azure DevOps organization';
orgInput.show();
});
} finally {
orgInput.dispose();
orgInputDisposables.forEach(d => void d.dispose());
}
}
if (!azureOrganization) return undefined;
const tokenInput = window.createInputBox();
tokenInput.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the Azure DevOps Access Tokens Page',
};
token = await new Promise<string | undefined>(resolve => {
disposables.push(
tokenInput.onDidHide(() => resolve(undefined)),
tokenInput.onDidChangeValue(() => (tokenInput.validationMessage = undefined)),
tokenInput.onDidAccept(() => {
const value = tokenInput.value.trim();
if (!value) {
tokenInput.validationMessage = 'A personal access token is required';
return;
}
resolve(value);
}),
tokenInput.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(
`https://${
descriptor?.domain ?? 'dev.azure.com'
}/${azureOrganization}/_usersSettings/tokens`,
),
);
}
}),
);
tokenInput.password = true;
tokenInput.title = `Azure DevOps Authentication${
descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''
}`;
tokenInput.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
tokenInput.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [Azure DevOps Personal Access Token](https://${
descriptor?.domain ?? 'dev.azure.com'
}/${azureOrganization}/_usersSettings/tokens "Get your Azure DevOps Access Token")`
: 'Paste your Azure DevOps Personal Access Token';
tokenInput.buttons = [infoButton];
tokenInput.show();
});
} finally {
tokenInput.dispose();
disposables.forEach(d => void d.dispose());
}
if (!token) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: base64(`:${token}`),
scopes: descriptor?.scopes ?? [],
account: {
id: '',
label: '',
},
};
}
}

+ 137
- 0
src/plus/integrations/authentication/bitbucket.ts Vedi File

@ -0,0 +1,137 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { base64 } from '../../../system/string';
import { supportedInVSCodeVersion } from '../../../system/utils';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
export class BitbucketAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
let bitbucketUsername: string | undefined = descriptor?.username as string | undefined;
if (!bitbucketUsername) {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the Bitbucket Settings Page',
};
const usernameInput = window.createInputBox();
usernameInput.ignoreFocusOut = true;
const usernameInputDisposables: Disposable[] = [];
try {
bitbucketUsername = await new Promise<string | undefined>(resolve => {
usernameInputDisposables.push(
usernameInput.onDidHide(() => resolve(undefined)),
usernameInput.onDidChangeValue(() => (usernameInput.validationMessage = undefined)),
usernameInput.onDidAccept(() => {
const value = usernameInput.value.trim();
if (!value) {
usernameInput.validationMessage = 'A Bitbucket username is required';
return;
}
resolve(value);
}),
usernameInput.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(`https://${descriptor?.domain ?? 'bitbucket.org'}/account/settings/`),
);
}
}),
);
usernameInput.title = `Bitbucket Authentication${
descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''
}`;
usernameInput.placeholder = 'Username';
usernameInput.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Enter your [Bitbucket Username](https://${
descriptor?.domain ?? 'bitbucket.org'
}/account/settings/ "Get your Bitbucket App Password")`
: 'Enter your Bitbucket Username';
usernameInput.show();
});
} finally {
usernameInput.dispose();
usernameInputDisposables.forEach(d => void d.dispose());
}
}
if (!bitbucketUsername) return undefined;
const appPasswordInput = window.createInputBox();
appPasswordInput.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let appPassword;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the Bitbucket App Passwords Page',
};
appPassword = await new Promise<string | undefined>(resolve => {
disposables.push(
appPasswordInput.onDidHide(() => resolve(undefined)),
appPasswordInput.onDidChangeValue(() => (appPasswordInput.validationMessage = undefined)),
appPasswordInput.onDidAccept(() => {
const value = appPasswordInput.value.trim();
if (!value) {
appPasswordInput.validationMessage = 'An app password is required';
return;
}
resolve(value);
}),
appPasswordInput.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(
`https://${descriptor?.domain ?? 'bitbucket.org'}/account/settings/app-passwords/`,
),
);
}
}),
);
appPasswordInput.password = true;
appPasswordInput.title = `Bitbucket Authentication${
descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''
}`;
appPasswordInput.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
appPasswordInput.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [Bitbucket App Password](https://${
descriptor?.domain ?? 'bitbucket.org'
}/account/settings/app-passwords/ "Get your Bitbucket App Password")`
: 'Paste your Bitbucket App Password';
appPasswordInput.buttons = [infoButton];
appPasswordInput.show();
});
} finally {
appPasswordInput.dispose();
disposables.forEach(d => void d.dispose());
}
if (!appPassword) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: base64(`${bitbucketUsername}:${appPassword}`),
scopes: descriptor?.scopes ?? [],
account: {
id: '',
label: '',
},
};
}
}

+ 81
- 0
src/plus/integrations/authentication/github.ts Vedi File

@ -0,0 +1,81 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { supportedInVSCodeVersion } from '../../../system/utils';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
export class GitHubEnterpriseAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
input.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the GitHub Access Tokens Page',
};
token = await new Promise<string | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve(undefined)),
input.onDidChangeValue(() => (input.validationMessage = undefined)),
input.onDidAccept(() => {
const value = input.value.trim();
if (!value) {
input.validationMessage = 'A personal access token is required';
return;
}
resolve(value);
}),
input.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(`https://${descriptor?.domain ?? 'github.com'}/settings/tokens`),
);
}
}),
);
input.password = true;
input.title = `GitHub Authentication${descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''}`;
input.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
input.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [GitHub Personal Access Token](https://${
descriptor?.domain ?? 'github.com'
}/settings/tokens "Get your GitHub Access Token")`
: 'Paste your GitHub Personal Access Token';
input.buttons = [infoButton];
input.show();
});
} finally {
input.dispose();
disposables.forEach(d => void d.dispose());
}
if (!token) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: token,
scopes: descriptor?.scopes ?? [],
account: {
id: '',
label: '',
},
};
}
}

+ 82
- 0
src/plus/integrations/authentication/gitlab.ts Vedi File

@ -0,0 +1,82 @@
import type { AuthenticationSession, Disposable, QuickInputButton } from 'vscode';
import { env, ThemeIcon, Uri, window } from 'vscode';
import { supportedInVSCodeVersion } from '../../../system/utils';
import type {
IntegrationAuthenticationProvider,
IntegrationAuthenticationSessionDescriptor,
} from './integrationAuthentication';
export class GitLabAuthenticationProvider implements IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string {
return descriptor?.domain ?? '';
}
async createSession(
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const input = window.createInputBox();
input.ignoreFocusOut = true;
const disposables: Disposable[] = [];
let token;
try {
const infoButton: QuickInputButton = {
iconPath: new ThemeIcon(`link-external`),
tooltip: 'Open the GitLab Access Tokens Page',
};
token = await new Promise<string | undefined>(resolve => {
disposables.push(
input.onDidHide(() => resolve(undefined)),
input.onDidChangeValue(() => (input.validationMessage = undefined)),
input.onDidAccept(() => {
const value = input.value.trim();
if (!value) {
input.validationMessage = 'A personal access token is required';
return;
}
resolve(value);
}),
input.onDidTriggerButton(e => {
if (e === infoButton) {
void env.openExternal(
Uri.parse(
`https://${descriptor?.domain ?? 'gitlab.com'}/-/profile/personal_access_tokens`,
),
);
}
}),
);
input.password = true;
input.title = `GitLab Authentication${descriptor?.domain ? ` \u2022 ${descriptor.domain}` : ''}`;
input.placeholder = `Requires ${descriptor?.scopes.join(', ') ?? 'all'} scopes`;
input.prompt = supportedInVSCodeVersion('input-prompt-links')
? `Paste your [GitLab Personal Access Token](https://${
descriptor?.domain ?? 'gitlab.com'
}/-/profile/personal_access_tokens "Get your GitLab Access Token")`
: 'Paste your GitLab Personal Access Token';
input.buttons = [infoButton];
input.show();
});
} finally {
input.dispose();
disposables.forEach(d => void d.dispose());
}
if (!token) return undefined;
return {
id: this.getSessionId(descriptor),
accessToken: token,
scopes: descriptor?.scopes ?? [],
account: {
id: '',
label: '',
},
};
}
}

+ 166
- 0
src/plus/integrations/authentication/integrationAuthentication.ts Vedi File

@ -0,0 +1,166 @@
import type { AuthenticationSession, Disposable } from 'vscode';
import { authentication } from 'vscode';
import { wrapForForcedInsecureSSL } from '@env/fetch';
import type { Container } from '../../../container';
import { debug } from '../../../system/decorators/log';
import { ProviderId } from '../providers/models';
import { AzureDevOpsAuthenticationProvider } from './azureDevOps';
import { BitbucketAuthenticationProvider } from './bitbucket';
import { GitHubEnterpriseAuthenticationProvider } from './github';
import { GitLabAuthenticationProvider } from './gitlab';
interface StoredSession {
id: string;
accessToken: string;
account?: {
label?: string;
displayName?: string;
id: string;
};
scopes: string[];
}
export interface IntegrationAuthenticationProviderDescriptor {
id: ProviderId;
scopes: string[];
}
export interface IntegrationAuthenticationSessionDescriptor {
domain: string;
scopes: string[];
[key: string]: unknown;
}
export interface IntegrationAuthenticationProvider {
getSessionId(descriptor?: IntegrationAuthenticationSessionDescriptor): string;
createSession(descriptor?: IntegrationAuthenticationSessionDescriptor): Promise<AuthenticationSession | undefined>;
}
export class IntegrationAuthenticationService implements Disposable {
private readonly providers = new Map<ProviderId, IntegrationAuthenticationProvider>();
constructor(private readonly container: Container) {}
dispose() {
this.providers.clear();
}
@debug()
async createSession(
providerId: ProviderId,
descriptor?: IntegrationAuthenticationSessionDescriptor,
): Promise<AuthenticationSession | undefined> {
const provider = this.ensureProvider(providerId);
const session = await provider.createSession(descriptor);
if (session == null) return undefined;
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
await this.container.storage.storeSecret(key, JSON.stringify(session));
return session;
}
@debug()
async getSession(
providerId: ProviderId,
descriptor?: IntegrationAuthenticationSessionDescriptor,
options?: { createIfNeeded?: boolean; forceNewSession?: boolean },
): Promise<AuthenticationSession | undefined> {
if (this.supports(providerId)) {
const provider = this.ensureProvider(providerId);
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
if (options?.forceNewSession) {
await this.container.storage.deleteSecret(key);
}
let storedSession: StoredSession | undefined;
try {
const sessionJSON = await this.container.storage.getSecret(key);
if (sessionJSON) {
storedSession = JSON.parse(sessionJSON);
}
} catch (ex) {
try {
await this.container.storage.deleteSecret(key);
} catch {}
if (!options?.createIfNeeded) {
throw ex;
}
}
if (options?.createIfNeeded && storedSession == null) {
return this.createSession(providerId, descriptor);
}
return storedSession as AuthenticationSession | undefined;
}
if (descriptor == null) return undefined;
const { createIfNeeded, forceNewSession } = options ?? {};
return wrapForForcedInsecureSSL(
this.container.integrations.ignoreSSLErrors({ id: providerId, domain: descriptor?.domain }),
() =>
authentication.getSession(providerId, descriptor.scopes, {
createIfNone: forceNewSession ? undefined : createIfNeeded,
silent: !createIfNeeded && !forceNewSession ? true : undefined,
forceNewSession: forceNewSession ? true : undefined,
}),
);
}
@debug()
async deleteSession(providerId: ProviderId, descriptor?: IntegrationAuthenticationSessionDescriptor) {
const provider = this.ensureProvider(providerId);
const key = this.getSecretKey(providerId, provider.getSessionId(descriptor));
await this.container.storage.deleteSecret(key);
}
supports(providerId: string): boolean {
switch (providerId) {
case ProviderId.AzureDevOps:
case ProviderId.Bitbucket:
case ProviderId.GitHubEnterprise:
case ProviderId.GitLab:
case ProviderId.GitLabSelfHosted:
return true;
default:
return false;
}
}
private getSecretKey(providerId: string, id: string): `gitlens.integration.auth:${string}` {
return `gitlens.integration.auth:${providerId}|${id}`;
}
private ensureProvider(providerId: ProviderId): IntegrationAuthenticationProvider {
let provider = this.providers.get(providerId);
if (provider == null) {
switch (providerId) {
case ProviderId.AzureDevOps:
provider = new AzureDevOpsAuthenticationProvider();
break;
case ProviderId.Bitbucket:
provider = new BitbucketAuthenticationProvider();
break;
case ProviderId.GitHubEnterprise:
provider = new GitHubEnterpriseAuthenticationProvider();
break;
case ProviderId.GitLab:
case ProviderId.GitLabSelfHosted:
provider = new GitLabAuthenticationProvider();
break;
default:
throw new Error(`Provider '${providerId}' is not supported`);
}
this.providers.set(providerId, provider);
}
return provider;
}
}

+ 177
- 0
src/plus/integrations/integrationService.ts Vedi File

@ -0,0 +1,177 @@
import type { AuthenticationSessionsChangeEvent, Event } from 'vscode';
import { authentication, Disposable, EventEmitter } from 'vscode';
import { isWeb } from '@env/platform';
import type { Container } from '../../container';
import type { SearchedIssue } from '../../git/models/issue';
import type { SearchedPullRequest } from '../../git/models/pullRequest';
import type { GitRemote } from '../../git/models/remote';
import type { RemoteProviderId } from '../../git/remotes/remoteProvider';
import { configuration } from '../../system/configuration';
import { debug } from '../../system/decorators/log';
import type { ProviderIntegration, ProviderKey, SupportedProviderIds } from './providerIntegration';
import { AzureDevOpsIntegration } from './providers/azureDevOps';
import { BitbucketIntegration } from './providers/bitbucket';
import { GitHubEnterpriseIntegration, GitHubIntegration } from './providers/github';
import { GitLabIntegration, GitLabSelfHostedIntegration } from './providers/gitlab';
import { ProviderId } from './providers/models';
import { ProvidersApi } from './providers/providersApi';
export interface ConnectionStateChangeEvent {
key: string;
reason: 'connected' | 'disconnected';
}
export class IntegrationService implements Disposable {
private readonly _onDidChangeConnectionState = new EventEmitter<ConnectionStateChangeEvent>();
get onDidChangeConnectionState(): Event<ConnectionStateChangeEvent> {
return this._onDidChangeConnectionState.event;
}
private readonly _connectedCache = new Set<string>();
private readonly _disposable: Disposable;
private _integrations = new Map<ProviderKey, ProviderIntegration>();
private _providersApi: ProvidersApi;
constructor(private readonly container: Container) {
this._providersApi = new ProvidersApi(container);
this._disposable = Disposable.from(
configuration.onDidChange(e => {
if (configuration.changed(e, 'remotes')) {
this._ignoreSSLErrors.clear();
}
}),
authentication.onDidChangeSessions(this.onAuthenticationSessionsChanged, this),
);
}
dispose() {
this._disposable?.dispose();
}
private onAuthenticationSessionsChanged(e: AuthenticationSessionsChangeEvent) {
for (const provider of this._integrations.values()) {
if (e.provider.id === provider.authProvider.id) {
provider.refresh();
}
}
}
connected(key: string): void {
// Only fire events if the key is being connected for the first time
if (this._connectedCache.has(key)) return;
this._connectedCache.add(key);
this.container.telemetry.sendEvent('remoteProviders/connected', { 'remoteProviders.key': key });
setTimeout(() => this._onDidChangeConnectionState.fire({ key: key, reason: 'connected' }), 250);
}
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;
this._connectedCache.delete(key);
this.container.telemetry.sendEvent('remoteProviders/disconnected', { 'remoteProviders.key': key });
setTimeout(() => this._onDidChangeConnectionState.fire({ key: key, reason: 'disconnected' }), 250);
}
isConnected(key?: string): boolean {
return key == null ? this._connectedCache.size !== 0 : this._connectedCache.has(key);
}
get(id: SupportedProviderIds, domain?: string): ProviderIntegration {
const key: ProviderKey = `${id}|${domain}`;
let provider = this._integrations.get(key);
if (provider == null) {
switch (id) {
case ProviderId.GitHub:
provider = new GitHubIntegration(this.container, this._providersApi);
break;
case ProviderId.GitHubEnterprise:
if (domain == null) throw new Error(`Domain is required for '${id}' integration`);
provider = new GitHubEnterpriseIntegration(this.container, this._providersApi, domain);
break;
case ProviderId.GitLab:
provider = new GitLabIntegration(this.container, this._providersApi);
break;
case ProviderId.GitLabSelfHosted:
if (domain == null) throw new Error(`Domain is required for '${id}' integration`);
provider = new GitLabSelfHostedIntegration(this.container, this._providersApi, domain);
break;
case ProviderId.Bitbucket:
provider = new BitbucketIntegration(this.container, this._providersApi);
break;
case ProviderId.AzureDevOps:
provider = new AzureDevOpsIntegration(this.container, this._providersApi);
break;
default:
throw new Error(`Provider '${id}' is not supported`);
}
this._integrations.set(key, provider);
}
return provider;
}
getByRemote(remote: GitRemote): ProviderIntegration | undefined {
if (remote?.provider == null) return undefined;
const id = convertRemoteIdToProviderId(remote.provider.id);
return id != null ? this.get(id, remote.domain) : undefined;
}
@debug<IntegrationService['getMyIssues']>({ args: { 0: r => r.name } })
async getMyIssues(remote: GitRemote): Promise<SearchedIssue[] | undefined> {
if (remote?.provider == null) return undefined;
const provider = this.getByRemote(remote);
return provider?.searchMyIssues();
}
@debug<IntegrationService['getMyPullRequests']>({ args: { 0: r => r.name } })
async getMyPullRequests(remote: GitRemote): Promise<SearchedPullRequest[] | undefined> {
if (remote?.provider == null) return undefined;
const provider = this.getByRemote(remote);
return provider?.searchMyPullRequests();
}
supports(remoteId: RemoteProviderId): boolean {
return convertRemoteIdToProviderId(remoteId) != null;
}
private _ignoreSSLErrors = new Map<string, boolean | 'force'>();
ignoreSSLErrors(
integration: ProviderIntegration | { id: SupportedProviderIds; domain: string },
): boolean | 'force' {
if (isWeb) return false;
let ignoreSSLErrors = this._ignoreSSLErrors.get(integration.id);
if (ignoreSSLErrors === undefined) {
const cfg = configuration
.get('remotes')
?.find(remote => remote.type.toLowerCase() === integration.id && remote.domain === integration.domain);
ignoreSSLErrors = cfg?.ignoreSSLErrors ?? false;
this._ignoreSSLErrors.set(integration.id, ignoreSSLErrors);
}
return ignoreSSLErrors;
}
}
function convertRemoteIdToProviderId(remoteId: RemoteProviderId): SupportedProviderIds | undefined {
switch (remoteId) {
case 'azure-devops':
return ProviderId.AzureDevOps;
case 'bitbucket':
case 'bitbucket-server':
return ProviderId.Bitbucket;
case 'github':
return ProviderId.GitHub;
case 'gitlab':
return ProviderId.GitLab;
default:
return undefined;
}
}

+ 925
- 0
src/plus/integrations/providerIntegration.ts Vedi File

@ -0,0 +1,925 @@
import type { AuthenticationSession, CancellationToken, Event, MessageItem } from 'vscode';
import { CancellationError, EventEmitter, window } from 'vscode';
import type { Container } from '../../container';
import { AuthenticationError, ProviderRequestClientError } from '../../errors';
import type { PagedResult } from '../../git/gitProvider';
import type { Account } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import { showIntegrationDisconnectedTooManyFailedRequestsWarningMessage } from '../../messages';
import { configuration } from '../../system/configuration';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import type { LogScope } from '../../system/logger.scope';
import { getLogScope } from '../../system/logger.scope';
import { isSubscriptionPaidPlan, isSubscriptionPreviewTrialExpired } from '../gk/account/subscription';
import type {
IntegrationAuthenticationProviderDescriptor,
IntegrationAuthenticationSessionDescriptor,
} from './authentication/integrationAuthentication';
import type {
GetIssuesOptions,
GetPullRequestsOptions,
PagedProjectInput,
PagedRepoInput,
ProviderAccount,
ProviderIssue,
ProviderPullRequest,
ProviderRepoInput,
ProviderReposInput,
} from './providers/models';
import { IssueFilter, PagingMode, ProviderId, PullRequestFilter } from './providers/models';
import type { ProvidersApi } from './providers/providersApi';
// TODO@eamodio revisit how once authenticated, all remotes are always connected, even after a restart
export type SupportedProviderIds = ProviderId;
export type ProviderKey = `${SupportedProviderIds}|${string}`;
export type RepositoryDescriptor = { key: string } & Record<string, unknown>;
export abstract class ProviderIntegration<T extends RepositoryDescriptor = RepositoryDescriptor> {
private readonly _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> {
return this._onDidChange.event;
}
constructor(
protected readonly container: Container,
protected readonly api: ProvidersApi,
) {}
abstract get authProvider(): IntegrationAuthenticationProviderDescriptor;
abstract get id(): SupportedProviderIds;
abstract get name(): string;
abstract get domain(): string;
protected get authProviderDescriptor(): IntegrationAuthenticationSessionDescriptor {
return { domain: this.domain, scopes: this.authProvider.scopes };
}
get icon(): string {
return this.id;
}
protected get key(): `${SupportedProviderIds}` | `${SupportedProviderIds}:${string}` {
return this.id;
}
private get connectedKey(): `connected:${ProviderIntegration['key']}` {
return `connected:${this.key}`;
}
get maybeConnected(): boolean | undefined {
return this._session === undefined ? undefined : this._session !== null;
}
protected _session: AuthenticationSession | null | undefined;
protected session() {
if (this._session === undefined) {
return this.ensureSession(false);
}
return this._session ?? undefined;
}
@log()
async connect(): Promise<boolean> {
try {
const session = await this.ensureSession(true);
return Boolean(session);
} catch (ex) {
return false;
}
}
@gate()
@log()
async disconnect(options?: { silent?: boolean; currentSessionOnly?: boolean }): Promise<void> {
if (options?.currentSessionOnly && this._session === null) return;
const connected = this._session != null;
if (connected && !options?.silent) {
if (options?.currentSessionOnly) {
void showIntegrationDisconnectedTooManyFailedRequestsWarningMessage(this.name);
} else {
const disable = { title: 'Disable' };
const signout = { title: 'Disable & Sign Out' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
let result: MessageItem | undefined;
if (this.container.integrationAuthentication.supports(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 },
disable,
signout,
cancel,
);
} else {
result = await window.showWarningMessage(
`Are you sure you want to disable the rich integration with ${this.name}?`,
{ modal: true },
disable,
cancel,
);
}
if (result == null || result === cancel) return;
if (result === signout) {
void this.container.integrationAuthentication.deleteSession(
this.authProvider.id,
this.authProviderDescriptor,
);
}
}
}
this.resetRequestExceptionCount();
this._session = null;
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 this.container.storage.storeWorkspace(this.connectedKey, false);
}
this._onDidChange.fire();
if (!options?.silent && !options?.currentSessionOnly) {
this.container.integrations.disconnected(this.key);
}
}
}
@log()
async reauthenticate(): Promise<void> {
if (this._session === undefined) return;
this._session = undefined;
void (await this.ensureSession(true, true));
}
refresh() {
void this.ensureSession(false);
}
private requestExceptionCount = 0;
resetRequestExceptionCount(): void {
this.requestExceptionCount = 0;
}
private handleProviderException<T>(ex: Error, scope: LogScope | undefined, defaultValue: T): T {
if (ex instanceof CancellationError) return defaultValue;
Logger.error(ex, scope);
if (ex instanceof AuthenticationError || ex instanceof ProviderRequestClientError) {
this.trackRequestException();
}
return defaultValue;
}
@debug()
trackRequestException(): void {
this.requestExceptionCount++;
if (this.requestExceptionCount >= 5 && this._session !== null) {
void this.disconnect({ currentSessionOnly: true });
}
}
@gate()
@debug({ exit: true })
async isConnected(): Promise<boolean> {
return (await this.session()) != null;
}
@gate()
@debug()
async getAccountForCommit(
repo: T,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.getProviderAccountForCommit(this._session!, repo, ref, options);
this.resetRequestExceptionCount();
return author;
} catch (ex) {
return this.handleProviderException<Account | undefined>(ex, scope, undefined);
}
}
protected abstract getProviderAccountForCommit(
session: AuthenticationSession,
repo: T,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
@gate()
@debug()
async getAccountForEmail(
repo: T,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
const author = await this.getProviderAccountForEmail(this._session!, repo, email, options);
this.resetRequestExceptionCount();
return author;
} catch (ex) {
return this.handleProviderException<Account | undefined>(ex, scope, undefined);
}
}
protected abstract getProviderAccountForEmail(
session: AuthenticationSession,
repo: T,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined>;
@debug()
async getDefaultBranch(repo: T): Promise<DefaultBranch | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const defaultBranch = this.container.cache.getRepositoryDefaultBranch(repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderDefaultBranch(this._session!, repo);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<DefaultBranch | undefined>(ex, scope, undefined);
}
})(),
}));
return defaultBranch;
}
protected abstract getProviderDefaultBranch(
{ accessToken }: AuthenticationSession,
repo: T,
): Promise<DefaultBranch | undefined>;
@debug()
async getRepositoryMetadata(repo: T, _cancellation?: CancellationToken): Promise<RepositoryMetadata | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const metadata = this.container.cache.getRepositoryMetadata(repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderRepositoryMetadata(this._session!, repo);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<RepositoryMetadata | undefined>(ex, scope, undefined);
}
})(),
}));
return metadata;
}
protected abstract getProviderRepositoryMetadata(
session: AuthenticationSession,
repo: T,
): Promise<RepositoryMetadata | undefined>;
@debug()
async getIssueOrPullRequest(repo: T, id: string): Promise<IssueOrPullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const issueOrPR = this.container.cache.getIssueOrPullRequest(id, repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderIssueOrPullRequest(this._session!, repo, id);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<IssueOrPullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return issueOrPR;
}
protected abstract getProviderIssueOrPullRequest(
session: AuthenticationSession,
repo: T,
id: string,
): Promise<IssueOrPullRequest | undefined>;
@debug()
async getPullRequestForBranch(
repo: T,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const pr = this.container.cache.getPullRequestForBranch(branch, repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderPullRequestForBranch(this._session!, repo, branch, options);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<PullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return pr;
}
protected abstract getProviderPullRequestForBranch(
session: AuthenticationSession,
repo: T,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined>;
@debug()
async getPullRequestForCommit(repo: T, ref: string): Promise<PullRequest | undefined> {
const scope = getLogScope();
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
const pr = this.container.cache.getPullRequestForSha(ref, repo, this, () => ({
value: (async () => {
try {
const result = await this.getProviderPullRequestForCommit(this._session!, repo, ref);
this.resetRequestExceptionCount();
return result;
} catch (ex) {
return this.handleProviderException<PullRequest | undefined>(ex, scope, undefined);
}
})(),
}));
return pr;
}
protected abstract getProviderPullRequestForCommit(
session: AuthenticationSession,
repo: T,
ref: string,
): Promise<PullRequest | undefined>;
async getMyIssuesForRepos(
reposOrRepoIds: ProviderReposInput,
options?: {
filters?: IssueFilter[];
cursor?: string;
customUrl?: string;
},
): Promise<PagedResult<ProviderIssue> | undefined> {
const providerId = this.authProvider.id;
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
if (
providerId !== ProviderId.GitLab &&
(this.api.isRepoIdsInput(reposOrRepoIds) ||
(providerId === ProviderId.AzureDevOps &&
!reposOrRepoIds.every(repo => repo.project != null && repo.namespace != null)))
) {
Logger.warn(`Unsupported input for provider ${providerId}`, 'getIssuesForRepos');
return undefined;
}
let getIssuesOptions: GetIssuesOptions | undefined;
if (providerId === ProviderId.AzureDevOps) {
const organizations = new Set<string>();
const projects = new Set<string>();
for (const repo of reposOrRepoIds as ProviderRepoInput[]) {
organizations.add(repo.namespace);
projects.add(repo.project!);
}
if (organizations.size > 1) {
Logger.warn(`Multiple organizations not supported for provider ${providerId}`, 'getIssuesForRepos');
return undefined;
} else if (organizations.size === 0) {
Logger.warn(`No organizations found for provider ${providerId}`, 'getIssuesForRepos');
return undefined;
}
const organization: string = organizations.values().next().value;
if (options?.filters != null) {
if (!this.api.providerSupportsIssueFilters(providerId, options.filters)) {
Logger.warn(`Unsupported filters for provider ${providerId}`, 'getIssuesForRepos');
return undefined;
}
let userAccount: ProviderAccount | undefined;
try {
userAccount = await this.api.getCurrentUserForInstance(providerId, organization);
} catch (ex) {
Logger.error(ex, 'getIssuesForRepos');
return undefined;
}
if (userAccount == null) {
Logger.warn(`Unable to get current user for ${providerId}`, 'getIssuesForRepos');
return undefined;
}
const userFilterProperty = userAccount.name;
if (userFilterProperty == null) {
Logger.warn(`Unable to get user property for filter for ${providerId}`, 'getIssuesForRepos');
return undefined;
}
getIssuesOptions = {
authorLogin: options.filters.includes(IssueFilter.Author) ? userFilterProperty : undefined,
assigneeLogins: options.filters.includes(IssueFilter.Assignee) ? [userFilterProperty] : undefined,
mentionLogin: options.filters.includes(IssueFilter.Mention) ? userFilterProperty : undefined,
};
}
const cursorInfo = JSON.parse(options?.cursor ?? '{}');
const cursors: PagedProjectInput[] = cursorInfo.cursors ?? [];
let projectInputs: PagedProjectInput[] = Array.from(projects.values()).map(project => ({
namespace: organization,
project: project,
cursor: undefined,
}));
if (cursors.length > 0) {
projectInputs = cursors;
}
try {
const cursor: { cursors: PagedProjectInput[] } = { cursors: [] };
let hasMore = false;
const data: ProviderIssue[] = [];
await Promise.all(
projectInputs.map(async projectInput => {
const results = await this.api.getIssuesForAzureProject(
projectInput.namespace,
projectInput.project,
{
...getIssuesOptions,
cursor: projectInput.cursor,
},
);
data.push(...results.values);
if (results.paging?.more) {
hasMore = true;
cursor.cursors.push({
namespace: projectInput.namespace,
project: projectInput.project,
cursor: results.paging.cursor,
});
}
}),
);
return {
values: data,
paging: {
more: hasMore,
cursor: JSON.stringify(cursor),
},
};
} catch (ex) {
Logger.error(ex, 'getIssuesForRepos');
return undefined;
}
}
if (options?.filters != null) {
let userAccount: ProviderAccount | undefined;
try {
userAccount = await this.api.getCurrentUser(providerId);
} catch (ex) {
Logger.error(ex, 'getIssuesForRepos');
return undefined;
}
if (userAccount == null) {
Logger.warn(`Unable to get current user for ${providerId}`, 'getIssuesForRepos');
return undefined;
}
const userFilterProperty = userAccount.username;
if (userFilterProperty == null) {
Logger.warn(`Unable to get user property for filter for ${providerId}`, 'getIssuesForRepos');
return undefined;
}
getIssuesOptions = {
authorLogin: options.filters.includes(IssueFilter.Author) ? userFilterProperty : undefined,
assigneeLogins: options.filters.includes(IssueFilter.Assignee) ? [userFilterProperty] : undefined,
mentionLogin: options.filters.includes(IssueFilter.Mention) ? userFilterProperty : undefined,
};
}
if (
this.api.getProviderIssuesPagingMode(providerId) === PagingMode.Repo &&
!this.api.isRepoIdsInput(reposOrRepoIds)
) {
const cursorInfo = JSON.parse(options?.cursor ?? '{}');
const cursors: PagedRepoInput[] = cursorInfo.cursors ?? [];
let repoInputs: PagedRepoInput[] = reposOrRepoIds.map(repo => ({ repo: repo, cursor: undefined }));
if (cursors.length > 0) {
repoInputs = cursors;
}
try {
const cursor: { cursors: PagedRepoInput[] } = { cursors: [] };
let hasMore = false;
const data: ProviderIssue[] = [];
await Promise.all(
repoInputs.map(async repoInput => {
const results = await this.api.getIssuesForRepo(providerId, repoInput.repo, {
...getIssuesOptions,
cursor: repoInput.cursor,
baseUrl: options?.customUrl,
});
data.push(...results.values);
if (results.paging?.more) {
hasMore = true;
cursor.cursors.push({ repo: repoInput.repo, cursor: results.paging.cursor });
}
}),
);
return {
values: data,
paging: {
more: hasMore,
cursor: JSON.stringify(cursor),
},
};
} catch (ex) {
Logger.error(ex, 'getIssuesForRepos');
return undefined;
}
}
try {
return await this.api.getIssuesForRepos(providerId, reposOrRepoIds, {
...getIssuesOptions,
cursor: options?.cursor,
baseUrl: options?.customUrl,
});
} catch (ex) {
Logger.error(ex, 'getIssuesForRepos');
return undefined;
}
}
async getMyPullRequestsForRepos(
reposOrRepoIds: ProviderReposInput,
options?: {
filters?: PullRequestFilter[];
cursor?: string;
customUrl?: string;
},
): Promise<PagedResult<ProviderPullRequest> | undefined> {
const providerId = this.authProvider.id;
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
if (
providerId !== ProviderId.GitLab &&
(this.api.isRepoIdsInput(reposOrRepoIds) ||
(providerId === ProviderId.AzureDevOps &&
!reposOrRepoIds.every(repo => repo.project != null && repo.namespace != null)))
) {
Logger.warn(`Unsupported input for provider ${providerId}`);
return undefined;
}
let getPullRequestsOptions: GetPullRequestsOptions | undefined;
if (options?.filters != null) {
if (!this.api.providerSupportsPullRequestFilters(providerId, options.filters)) {
Logger.warn(`Unsupported filters for provider ${providerId}`, 'getPullRequestsForRepos');
return undefined;
}
let userAccount: ProviderAccount | undefined;
if (providerId === ProviderId.AzureDevOps) {
const organizations = new Set<string>();
for (const repo of reposOrRepoIds as ProviderRepoInput[]) {
organizations.add(repo.namespace);
}
if (organizations.size > 1) {
Logger.warn(
`Multiple organizations not supported for provider ${providerId}`,
'getPullRequestsForRepos',
);
return undefined;
} else if (organizations.size === 0) {
Logger.warn(`No organizations found for provider ${providerId}`, 'getPullRequestsForRepos');
return undefined;
}
const organization: string = organizations.values().next().value;
try {
userAccount = await this.api.getCurrentUserForInstance(providerId, organization);
} catch (ex) {
Logger.error(ex, 'getPullRequestsForRepos');
return undefined;
}
} else {
try {
userAccount = await this.api.getCurrentUser(providerId);
} catch (ex) {
Logger.error(ex, 'getPullRequestsForRepos');
return undefined;
}
}
if (userAccount == null) {
Logger.warn(`Unable to get current user for ${providerId}`, 'getPullRequestsForRepos');
return undefined;
}
let userFilterProperty: string | null;
switch (providerId) {
case ProviderId.Bitbucket:
case ProviderId.AzureDevOps:
userFilterProperty = userAccount.id;
break;
default:
userFilterProperty = userAccount.username;
break;
}
if (userFilterProperty == null) {
Logger.warn(`Unable to get user property for filter for ${providerId}`, 'getPullRequestsForRepos');
return undefined;
}
getPullRequestsOptions = {
authorLogin: options.filters.includes(PullRequestFilter.Author) ? userFilterProperty : undefined,
assigneeLogins: options.filters.includes(PullRequestFilter.Assignee) ? [userFilterProperty] : undefined,
reviewRequestedLogin: options.filters.includes(PullRequestFilter.ReviewRequested)
? userFilterProperty
: undefined,
mentionLogin: options.filters.includes(PullRequestFilter.Mention) ? userFilterProperty : undefined,
};
}
if (
this.api.getProviderPullRequestsPagingMode(providerId) === PagingMode.Repo &&
!this.api.isRepoIdsInput(reposOrRepoIds)
) {
const cursorInfo = JSON.parse(options?.cursor ?? '{}');
const cursors: PagedRepoInput[] = cursorInfo.cursors ?? [];
let repoInputs: PagedRepoInput[] = reposOrRepoIds.map(repo => ({ repo: repo, cursor: undefined }));
if (cursors.length > 0) {
repoInputs = cursors;
}
try {
const cursor: { cursors: PagedRepoInput[] } = { cursors: [] };
let hasMore = false;
const data: ProviderPullRequest[] = [];
await Promise.all(
repoInputs.map(async repoInput => {
const results = await this.api.getPullRequestsForRepo(providerId, repoInput.repo, {
...getPullRequestsOptions,
cursor: repoInput.cursor,
baseUrl: options?.customUrl,
});
data.push(...results.values);
if (results.paging?.more) {
hasMore = true;
cursor.cursors.push({ repo: repoInput.repo, cursor: results.paging.cursor });
}
}),
);
return {
values: data,
paging: {
more: hasMore,
cursor: JSON.stringify(cursor),
},
};
} catch (ex) {
Logger.error(ex, 'getPullRequestsForRepos');
return undefined;
}
}
try {
return this.api.getPullRequestsForRepos(providerId, reposOrRepoIds, {
...getPullRequestsOptions,
cursor: options?.cursor,
baseUrl: options?.customUrl,
});
} catch (ex) {
Logger.error(ex, 'getPullRequestsForRepos');
return undefined;
}
}
@debug()
async searchMyIssues(): Promise<SearchedIssue[] | undefined> {
const scope = getLogScope();
try {
const issues = await this.searchProviderMyIssues(this._session!);
this.resetRequestExceptionCount();
return issues;
} catch (ex) {
return this.handleProviderException<SearchedIssue[] | undefined>(ex, scope, undefined);
}
}
protected abstract searchProviderMyIssues(session: AuthenticationSession): Promise<SearchedIssue[] | undefined>;
@debug()
async searchMyPullRequests(): Promise<SearchedPullRequest[] | undefined> {
const scope = getLogScope();
try {
const pullRequests = await this.searchProviderMyPullRequests(this._session!);
this.resetRequestExceptionCount();
return pullRequests;
} catch (ex) {
return this.handleProviderException<SearchedPullRequest[] | undefined>(ex, scope, undefined);
}
}
protected abstract searchProviderMyPullRequests(
session: AuthenticationSession,
): Promise<SearchedPullRequest[] | undefined>;
@gate()
private async ensureSession(
createIfNeeded: boolean,
forceNewSession: boolean = false,
): Promise<AuthenticationSession | undefined> {
if (this._session != null) return this._session;
if (!configuration.get('integrations.enabled')) return undefined;
if (createIfNeeded) {
await this.container.storage.deleteWorkspace(this.connectedKey);
} else if (this.container.storage.getWorkspace(this.connectedKey) === false) {
return undefined;
}
let session: AuthenticationSession | undefined | null;
try {
session = await this.container.integrationAuthentication.getSession(
this.authProvider.id,
this.authProviderDescriptor,
{ createIfNeeded: createIfNeeded, forceNewSession: forceNewSession },
);
} catch (ex) {
await this.container.storage.deleteWorkspace(this.connectedKey);
if (ex instanceof Error && ex.message.includes('User did not consent')) {
return undefined;
}
session = null;
}
if (session === undefined && !createIfNeeded) {
await this.container.storage.deleteWorkspace(this.connectedKey);
}
this._session = session ?? null;
this.resetRequestExceptionCount();
if (session != null) {
await this.container.storage.storeWorkspace(this.connectedKey, true);
queueMicrotask(() => {
this._onDidChange.fire();
this.container.integrations.connected(this.key);
});
}
return session ?? undefined;
}
getIgnoreSSLErrors(): boolean | 'force' {
return this.container.integrations.ignoreSSLErrors(this);
}
}
export async function ensurePaidPlan(providerName: string, container: Container): Promise<boolean> {
const title = `Connecting to a ${providerName} instance for rich integration features requires a trial or paid plan.`;
while (true) {
const subscription = await container.subscription.getSubscription();
if (subscription.account?.verified === false) {
const resend = { title: 'Resend Verification' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nYou must verify your email before you can continue.`,
{ modal: true },
resend,
cancel,
);
if (result === resend) {
if (await container.subscription.resendVerification()) {
continue;
}
}
return false;
}
const plan = subscription.plan.effective.id;
if (isSubscriptionPaidPlan(plan)) break;
if (subscription.account == null && !isSubscriptionPreviewTrialExpired(subscription)) {
const startTrial = { title: 'Preview Pro' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to preview ✨ features for 3 days?`,
{ modal: true },
startTrial,
cancel,
);
if (result !== startTrial) return false;
void container.subscription.startPreviewTrial();
break;
} else if (subscription.account == null) {
const signIn = { title: 'Start Free GitKraken Trial' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to continue to use ✨ features on privately hosted repos, free for an additional 7 days?`,
{ modal: true },
signIn,
cancel,
);
if (result === signIn) {
if (await container.subscription.loginOrSignUp()) {
continue;
}
}
} else {
const upgrade = { title: 'Upgrade to Pro' };
const cancel = { title: 'Cancel', isCloseAffordance: true };
const result = await window.showWarningMessage(
`${title}\n\nDo you want to continue to use ✨ features on privately hosted repos?`,
{ modal: true },
upgrade,
cancel,
);
if (result === upgrade) {
void container.subscription.purchase();
}
}
return false;
}
return true;
}

+ 129
- 0
src/plus/integrations/providers/azureDevOps.ts Vedi File

@ -0,0 +1,129 @@
import type { AuthenticationSession } from 'vscode';
import type { PagedResult } from '../../../git/gitProvider';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
import { Logger } from '../../../system/logger';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthentication';
import type { RepositoryDescriptor, SupportedProviderIds } from '../providerIntegration';
import { ProviderIntegration } from '../providerIntegration';
import type { ProviderRepository } from './models';
import { ProviderId, providersMetadata } from './models';
const metadata = providersMetadata[ProviderId.AzureDevOps];
const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
interface AzureRepositoryDescriptor extends RepositoryDescriptor {
owner: string;
name: string;
}
export class AzureDevOpsIntegration extends ProviderIntegration<AzureRepositoryDescriptor> {
readonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;
readonly id: SupportedProviderIds = ProviderId.AzureDevOps;
readonly name: string = 'Azure DevOps';
get domain(): string {
return metadata.domain;
}
protected get apiBaseUrl(): string {
return 'https://dev.azure.com';
}
async getReposForAzureProject(
namespace: string,
project: string,
options?: { cursor?: string },
): Promise<PagedResult<ProviderRepository> | undefined> {
const connected = this.maybeConnected ?? (await this.isConnected());
if (!connected) return undefined;
try {
return await this.api.getReposForAzureProject(namespace, project, { cursor: options?.cursor });
} catch (ex) {
Logger.error(ex, 'getReposForAzureProject');
return undefined;
}
}
// TODO: implement
protected override async getProviderAccountForCommit(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_ref: string,
_options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderAccountForEmail(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_email: string,
_options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderDefaultBranch(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
): Promise<DefaultBranch | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderIssueOrPullRequest(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_id: string,
): Promise<IssueOrPullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderPullRequestForBranch(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_branch: string,
_options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderPullRequestForCommit(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
_ref: string,
): Promise<PullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderRepositoryMetadata(
_session: AuthenticationSession,
_repo: AzureRepositoryDescriptor,
): Promise<RepositoryMetadata | undefined> {
return Promise.resolve(undefined);
}
protected override async searchProviderMyPullRequests(
_session: AuthenticationSession,
_repo?: AzureRepositoryDescriptor,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected override async searchProviderMyIssues(
_session: AuthenticationSession,
_repo?: AzureRepositoryDescriptor,
): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}

+ 110
- 0
src/plus/integrations/providers/bitbucket.ts Vedi File

@ -0,0 +1,110 @@
import type { AuthenticationSession } from 'vscode';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthentication';
import type { RepositoryDescriptor, SupportedProviderIds } from '../providerIntegration';
import { ProviderIntegration } from '../providerIntegration';
import { ProviderId, providersMetadata } from './models';
const metadata = providersMetadata[ProviderId.Bitbucket];
const authProvider = Object.freeze({ id: metadata.id, scopes: metadata.scopes });
interface BitbucketRepositoryDescriptor extends RepositoryDescriptor {
owner: string;
name: string;
}
export class BitbucketIntegration extends ProviderIntegration<BitbucketRepositoryDescriptor> {
readonly authProvider: IntegrationAuthenticationProviderDescriptor = authProvider;
readonly id: SupportedProviderIds = ProviderId.Bitbucket;
readonly name: string = 'Bitbucket';
get domain(): string {
return metadata.domain;
}
protected get apiBaseUrl(): string {
return 'https://api.bitbucket.org/2.0';
}
// TODO: implement
protected override async getProviderAccountForCommit(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_ref: string,
_options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderAccountForEmail(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_email: string,
_options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderDefaultBranch(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
): Promise<DefaultBranch | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderIssueOrPullRequest(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_id: string,
): Promise<IssueOrPullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderPullRequestForBranch(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_branch: string,
_options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderPullRequestForCommit(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
_ref: string,
): Promise<PullRequest | undefined> {
return Promise.resolve(undefined);
}
protected override async getProviderRepositoryMetadata(
_session: AuthenticationSession,
_repo: BitbucketRepositoryDescriptor,
): Promise<RepositoryMetadata | undefined> {
return Promise.resolve(undefined);
}
protected override async searchProviderMyPullRequests(
_session: AuthenticationSession,
_repo?: BitbucketRepositoryDescriptor,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected override async searchProviderMyIssues(
_session: AuthenticationSession,
_repo?: BitbucketRepositoryDescriptor,
): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}

+ 196
- 0
src/plus/integrations/providers/github.ts Vedi File

@ -0,0 +1,196 @@
import type { AuthenticationSession } from 'vscode';
import type { Container } from '../../../container';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
import { log } from '../../../system/decorators/log';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthentication';
import type { SupportedProviderIds } from '../providerIntegration';
import { ensurePaidPlan, ProviderIntegration } from '../providerIntegration';
import { ProviderId, providersMetadata } from './models';
import type { ProvidersApi } from './providersApi';
const metadata = providersMetadata[ProviderId.GitHub];
const authProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
id: metadata.id,
scopes: metadata.scopes,
});
const enterpriseMetadata = providersMetadata[ProviderId.GitHubEnterprise];
const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
id: enterpriseMetadata.id,
scopes: enterpriseMetadata.scopes,
});
export type GitHubRepositoryDescriptor = {
key: string;
owner: string;
name: string;
};
export class GitHubIntegration extends ProviderIntegration<GitHubRepositoryDescriptor> {
readonly authProvider = authProvider;
readonly id: SupportedProviderIds = ProviderId.GitHub;
readonly name: string = 'GitHub';
get domain(): string {
return metadata.domain;
}
protected get apiBaseUrl(): string {
return 'https://api.github.com';
}
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return (await this.container.github)?.getAccountForCommit(this, accessToken, repo.owner, repo.name, ref, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return (await this.container.github)?.getAccountForEmail(this, accessToken, repo.owner, repo.name, email, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderDefaultBranch(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
): Promise<DefaultBranch | undefined> {
return (await this.container.github)?.getDefaultBranch(this, accessToken, repo.owner, repo.name, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
id: string,
): Promise<IssueOrPullRequest | undefined> {
return (await this.container.github)?.getIssueOrPullRequest(
this,
accessToken,
repo.owner,
repo.name,
Number(id),
{
baseUrl: this.apiBaseUrl,
},
);
}
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const { include, ...opts } = options ?? {};
const toGitHubPullRequestState = (await import(/* webpackChunkName: "github" */ './github/models'))
.toGitHubPullRequestState;
return (await this.container.github)?.getPullRequestForBranch(
this,
accessToken,
repo.owner,
repo.name,
branch,
{
...opts,
include: include?.map(s => toGitHubPullRequestState(s)),
baseUrl: this.apiBaseUrl,
},
);
}
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
ref: string,
): Promise<PullRequest | undefined> {
return (await this.container.github)?.getPullRequestForCommit(this, accessToken, repo.owner, repo.name, ref, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderRepositoryMetadata(
{ accessToken }: AuthenticationSession,
repo: GitHubRepositoryDescriptor,
): Promise<RepositoryMetadata | undefined> {
return (await this.container.github)?.getRepositoryMetadata(this, accessToken, repo.owner, repo.name, {
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyPullRequests(
{ accessToken }: AuthenticationSession,
repo?: GitHubRepositoryDescriptor,
): Promise<SearchedPullRequest[] | undefined> {
return (await this.container.github)?.searchMyPullRequests(this, accessToken, {
repos: repo != null ? [`${repo.owner}/${repo.name}`] : undefined,
baseUrl: this.apiBaseUrl,
});
}
protected override async searchProviderMyIssues(
{ accessToken }: AuthenticationSession,
repo?: GitHubRepositoryDescriptor,
): Promise<SearchedIssue[] | undefined> {
return (await this.container.github)?.searchMyIssues(this, accessToken, {
repos: repo != null ? [`${repo.owner}/${repo.name}`] : undefined,
baseUrl: this.apiBaseUrl,
});
}
}
export class GitHubEnterpriseIntegration extends GitHubIntegration {
override readonly authProvider = enterpriseAuthProvider;
override readonly id = ProviderId.GitHubEnterprise;
override readonly name = 'GitHub Enterprise';
override get domain(): string {
return this._domain;
}
protected override get apiBaseUrl(): string {
return `https://${this._domain}/api/v3`;
}
protected override get key(): `${SupportedProviderIds}:${string}` {
return `${this.id}:${this.domain}`;
}
constructor(
container: Container,
override readonly api: ProvidersApi,
private readonly _domain: string,
) {
super(container, api);
}
@log()
override async connect(): Promise<boolean> {
if (!(await ensurePaidPlan(`${this.name} instance`, this.container))) {
return false;
}
return super.connect();
}
}

src/plus/github/github.ts → src/plus/integrations/providers/github/github.ts Vedi File

@ -7,8 +7,8 @@ import type { CancellationToken, Disposable, Event } from 'vscode';
import { EventEmitter, Uri, window } from 'vscode';
import { fetch, getProxyAgent, wrapForForcedInsecureSSL } from '@env/fetch';
import { isWeb } from '@env/platform';
import type { CoreConfiguration } from '../../constants';
import type { Container } from '../../container';
import type { CoreConfiguration } from '../../../../constants';
import type { Container } from '../../../../container';
import {
AuthenticationError,
AuthenticationErrorReason,
@ -16,31 +16,31 @@ import {
ProviderRequestClientError,
ProviderRequestNotFoundError,
ProviderRequestRateLimitError,
} from '../../errors';
import type { PagedResult, RepositoryVisibility } from '../../git/gitProvider';
import type { Account } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../git/models/issue';
import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequest';
import { isSha } from '../../git/models/reference';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import type { GitUser } from '../../git/models/user';
import { getGitHubNoReplyAddressParts } from '../../git/remotes/github';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
} from '../../../../errors';
import type { PagedResult, RepositoryVisibility } from '../../../../git/gitProvider';
import type { Account } from '../../../../git/models/author';
import type { DefaultBranch } from '../../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../../git/models/issue';
import type { PullRequest, SearchedPullRequest } from '../../../../git/models/pullRequest';
import { isSha } from '../../../../git/models/reference';
import type { Provider } from '../../../../git/models/remoteProvider';
import type { RepositoryMetadata } from '../../../../git/models/repositoryMetadata';
import type { GitUser } from '../../../../git/models/user';
import { getGitHubNoReplyAddressParts } from '../../../../git/remotes/github';
import {
showIntegrationRequestFailed500WarningMessage,
showIntegrationRequestTimedOutWarningMessage,
} from '../../messages';
import { uniqueBy } from '../../system/array';
import { configuration } from '../../system/configuration';
import { debug } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import type { LogScope } from '../../system/logger.scope';
import { getLogScope } from '../../system/logger.scope';
import { maybeStopWatch } from '../../system/stopwatch';
import { base64 } from '../../system/string';
import type { Version } from '../../system/version';
import { fromString, satisfies } from '../../system/version';
} from '../../../../messages';
import { uniqueBy } from '../../../../system/array';
import { configuration } from '../../../../system/configuration';
import { debug } from '../../../../system/decorators/log';
import { Logger } from '../../../../system/logger';
import type { LogScope } from '../../../../system/logger.scope';
import { getLogScope } from '../../../../system/logger.scope';
import { maybeStopWatch } from '../../../../system/stopwatch';
import { base64 } from '../../../../system/string';
import type { Version } from '../../../../system/version';
import { fromString, satisfies } from '../../../../system/version';
import type {
GitHubBlame,
GitHubBlameRange,
@ -223,7 +223,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getAccountForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForCommit(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -316,7 +316,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getAccountForEmail']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForEmail(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -403,7 +403,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getDefaultBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getDefaultBranch(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -462,7 +462,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getIssueOrPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
async getIssueOrPullRequest(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -545,7 +545,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getPullRequestForBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForBranch(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -651,7 +651,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getPullRequestForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForCommit(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -753,7 +753,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getRepositoryMetadata']>({ args: { 0: p => p.name, 1: '<token>' } })
async getRepositoryMetadata(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -2272,7 +2272,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['getEnterpriseVersion']>({ args: { 0: p => p?.name, 1: '<token>' } })
private async getEnterpriseVersion(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
options?: { baseUrl?: string },
): Promise<Version | undefined> {
@ -2296,7 +2296,7 @@ export class GitHubApi implements Disposable {
}
private async graphql<T>(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
query: string,
variables: RequestParameters,
@ -2356,7 +2356,7 @@ export class GitHubApi implements Disposable {
}
private async request<R extends string>(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
route: keyof Endpoints | R,
options:
@ -2458,7 +2458,7 @@ export class GitHubApi implements Disposable {
}
private handleRequestError(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
ex: RequestError | (Error & { name: 'AbortError' }),
scope: LogScope | undefined,
@ -2494,7 +2494,7 @@ export class GitHubApi implements Disposable {
provider?.trackRequestException();
void showIntegrationRequestFailed500WarningMessage(
`${provider?.name ?? 'GitHub'} failed to respond and might be experiencing issues.${
!provider?.custom
provider == null || provider.id === 'github'
? ' Please visit the [GitHub status page](https://githubstatus.com) for more information.'
: ''
}`,
@ -2523,7 +2523,7 @@ export class GitHubApi implements Disposable {
}
}
private handleException(ex: Error, provider: RichRemoteProvider | undefined, scope: LogScope | undefined): Error {
private handleException(ex: Error, provider: Provider | undefined, scope: LogScope | undefined): Error {
Logger.error(ex, scope);
// debugger;
@ -2533,7 +2533,7 @@ export class GitHubApi implements Disposable {
return ex;
}
private async showAuthenticationErrorMessage(ex: AuthenticationError, provider: RichRemoteProvider | undefined) {
private async showAuthenticationErrorMessage(ex: AuthenticationError, provider: Provider | undefined) {
if (ex.reason === AuthenticationErrorReason.Unauthorized || ex.reason === AuthenticationErrorReason.Forbidden) {
const confirm = 'Reauthenticate';
const result = await window.showErrorMessage(
@ -2554,7 +2554,7 @@ export class GitHubApi implements Disposable {
}
private async createEnterpriseAvatarUrl(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
baseUrl: string,
email: string,
@ -2598,7 +2598,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['searchMyPullRequests']>({ args: { 0: p => p.name, 1: '<token>' } })
async searchMyPullRequests(
provider: RichRemoteProvider,
provider: Provider,
token: string,
options?: { search?: string; user?: string; repos?: string[]; baseUrl?: string },
): Promise<SearchedPullRequest[]> {
@ -2709,7 +2709,7 @@ export class GitHubApi implements Disposable {
@debug<GitHubApi['searchMyIssues']>({ args: { 0: p => p.name, 1: '<token>' } })
async searchMyIssues(
provider: RichRemoteProvider,
provider: Provider,
token: string,
options?: { search?: string; user?: string; repos?: string[]; baseUrl?: string },
): Promise<SearchedIssue[] | undefined> {

src/plus/github/githubGitProvider.ts → src/plus/integrations/providers/github/githubGitProvider.ts Vedi File

@ -11,18 +11,18 @@ import type {
} from 'vscode';
import { authentication, EventEmitter, FileType, Uri, window, workspace } from 'vscode';
import { encodeUtf8Hex } from '@env/hex';
import { CharCode, Schemes } from '../../constants';
import type { Container } from '../../container';
import { emojify } from '../../emojis';
import { CharCode, Schemes } from '../../../../constants';
import type { Container } from '../../../../container';
import { emojify } from '../../../../emojis';
import {
AuthenticationError,
AuthenticationErrorReason,
ExtensionNotFoundError,
OpenVirtualRepositoryError,
OpenVirtualRepositoryErrorReason,
} from '../../errors';
import { Features } from '../../features';
import { GitSearchError } from '../../git/errors';
} from '../../../../errors';
import { Features } from '../../../../features';
import { GitSearchError } from '../../../../git/errors';
import type {
GitCaches,
GitProvider,
@ -34,18 +34,18 @@ import type {
RepositoryOpenEvent,
RepositoryVisibility,
ScmRepository,
} from '../../git/gitProvider';
import { GitUri } from '../../git/gitUri';
import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../git/models/blame';
import type { BranchSortOptions } from '../../git/models/branch';
import { getBranchId, getBranchNameWithoutRemote, GitBranch, sortBranches } from '../../git/models/branch';
import type { GitCommitLine } from '../../git/models/commit';
import { getChangedFilesCount, GitCommit, GitCommitIdentity } from '../../git/models/commit';
import { deletedOrMissing, uncommitted } from '../../git/models/constants';
import { GitContributor } from '../../git/models/contributor';
import type { GitDiffFile, GitDiffFilter, GitDiffLine, GitDiffShortStat } from '../../git/models/diff';
import type { GitFile } from '../../git/models/file';
import { GitFileChange, GitFileIndexStatus } from '../../git/models/file';
} from '../../../../git/gitProvider';
import { GitUri } from '../../../../git/gitUri';
import type { GitBlame, GitBlameAuthor, GitBlameLine, GitBlameLines } from '../../../../git/models/blame';
import type { BranchSortOptions } from '../../../../git/models/branch';
import { getBranchId, getBranchNameWithoutRemote, GitBranch, sortBranches } from '../../../../git/models/branch';
import type { GitCommitLine } from '../../../../git/models/commit';
import { getChangedFilesCount, GitCommit, GitCommitIdentity } from '../../../../git/models/commit';
import { deletedOrMissing, uncommitted } from '../../../../git/models/constants';
import { GitContributor } from '../../../../git/models/contributor';
import type { GitDiffFile, GitDiffFilter, GitDiffLine, GitDiffShortStat } from '../../../../git/models/diff';
import type { GitFile } from '../../../../git/models/file';
import { GitFileChange, GitFileIndexStatus } from '../../../../git/models/file';
import type {
GitGraph,
GitGraphHostingServiceType,
@ -56,49 +56,55 @@ import type {
GitGraphRowsStats,
GitGraphRowStats,
GitGraphRowTag,
} from '../../git/models/graph';
import type { GitLog } from '../../git/models/log';
import type { GitMergeStatus } from '../../git/models/merge';
import type { GitRebaseStatus } from '../../git/models/rebase';
import type { GitBranchReference, GitReference } from '../../git/models/reference';
import { createReference, isRevisionRange, isSha, isShaLike, isUncommitted } from '../../git/models/reference';
import type { GitReflog } from '../../git/models/reflog';
import { getRemoteIconUri, getVisibilityCacheKey, GitRemote } from '../../git/models/remote';
import type { RepositoryChangeEvent } from '../../git/models/repository';
import { Repository } from '../../git/models/repository';
import type { GitStash } from '../../git/models/stash';
import type { GitStatusFile } from '../../git/models/status';
import { GitStatus } from '../../git/models/status';
import type { TagSortOptions } from '../../git/models/tag';
import { getTagId, GitTag, sortTags } from '../../git/models/tag';
import type { GitTreeEntry } from '../../git/models/tree';
import type { GitUser } from '../../git/models/user';
import { isUserMatch } from '../../git/models/user';
import { getRemoteProviderMatcher, loadRemoteProviders } from '../../git/remotes/remoteProviders';
import type { GitSearch, GitSearchResultData, GitSearchResults, SearchOperators, SearchQuery } from '../../git/search';
import { getSearchQueryComparisonKey, parseSearchQuery } from '../../git/search';
import { configuration } from '../../system/configuration';
import { setContext } from '../../system/context';
import { gate } from '../../system/decorators/gate';
import { debug, log } from '../../system/decorators/log';
import { filterMap, first, last, map, some } from '../../system/iterable';
import { Logger } from '../../system/logger';
import type { LogScope } from '../../system/logger.scope';
import { getLogScope } from '../../system/logger.scope';
import { isAbsolute, isFolderGlob, maybeUri, normalizePath, relative } from '../../system/path';
import { asSettled, getSettledValue } from '../../system/promise';
import { serializeWebviewItemContext } from '../../system/webview';
import type { CachedBlame, CachedLog } from '../../trackers/gitDocumentTracker';
import { GitDocumentState } from '../../trackers/gitDocumentTracker';
import type { TrackedDocument } from '../../trackers/trackedDocument';
import type { GitHubAuthorityMetadata, Metadata, RemoteHubApi } from '../remotehub';
import { getRemoteHubApi, HeadType } from '../remotehub';
} from '../../../../git/models/graph';
import type { GitLog } from '../../../../git/models/log';
import type { GitMergeStatus } from '../../../../git/models/merge';
import type { GitRebaseStatus } from '../../../../git/models/rebase';
import type { GitBranchReference, GitReference } from '../../../../git/models/reference';
import { createReference, isRevisionRange, isSha, isShaLike, isUncommitted } from '../../../../git/models/reference';
import type { GitReflog } from '../../../../git/models/reflog';
import { getRemoteIconUri, getVisibilityCacheKey, GitRemote } from '../../../../git/models/remote';
import type { RepositoryChangeEvent } from '../../../../git/models/repository';
import { Repository } from '../../../../git/models/repository';
import type { GitStash } from '../../../../git/models/stash';
import type { GitStatusFile } from '../../../../git/models/status';
import { GitStatus } from '../../../../git/models/status';
import type { TagSortOptions } from '../../../../git/models/tag';
import { getTagId, GitTag, sortTags } from '../../../../git/models/tag';
import type { GitTreeEntry } from '../../../../git/models/tree';
import type { GitUser } from '../../../../git/models/user';
import { isUserMatch } from '../../../../git/models/user';
import { getRemoteProviderMatcher, loadRemoteProviders } from '../../../../git/remotes/remoteProviders';
import type {
GitSearch,
GitSearchResultData,
GitSearchResults,
SearchOperators,
SearchQuery,
} from '../../../../git/search';
import { getSearchQueryComparisonKey, parseSearchQuery } from '../../../../git/search';
import { configuration } from '../../../../system/configuration';
import { setContext } from '../../../../system/context';
import { gate } from '../../../../system/decorators/gate';
import { debug, log } from '../../../../system/decorators/log';
import { filterMap, first, last, map, some } from '../../../../system/iterable';
import { Logger } from '../../../../system/logger';
import type { LogScope } from '../../../../system/logger.scope';
import { getLogScope } from '../../../../system/logger.scope';
import { isAbsolute, isFolderGlob, maybeUri, normalizePath, relative } from '../../../../system/path';
import { asSettled, getSettledValue } from '../../../../system/promise';
import { serializeWebviewItemContext } from '../../../../system/webview';
import type { CachedBlame, CachedLog } from '../../../../trackers/gitDocumentTracker';
import { GitDocumentState } from '../../../../trackers/gitDocumentTracker';
import type { TrackedDocument } from '../../../../trackers/trackedDocument';
import type { GitHubAuthorityMetadata, Metadata, RemoteHubApi } from '../../../remotehub';
import { getRemoteHubApi, HeadType } from '../../../remotehub';
import type {
GraphBranchContextValue,
GraphItemContext,
GraphItemRefContext,
GraphTagContextValue,
} from '../webviews/graph/protocol';
} from '../../../webviews/graph/protocol';
import type { GitHubApi } from './github';
import { fromCommitFileStatus } from './models';
@ -2574,6 +2580,7 @@ export class GitHubGitProvider implements GitProvider, Disposable {
return [
new GitRemote(
this.container,
repoPath,
'origin',
'https',

src/plus/github/models.ts → src/plus/integrations/providers/github/models.ts Vedi File

@ -1,10 +1,10 @@
import type { Endpoints } from '@octokit/types';
import { GitFileIndexStatus } from '../../git/models/file';
import type { IssueLabel, IssueOrPullRequestType } from '../../git/models/issue';
import { Issue } from '../../git/models/issue';
import type { PullRequestState } from '../../git/models/pullRequest';
import { PullRequest, PullRequestMergeableState, PullRequestReviewDecision } from '../../git/models/pullRequest';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import { GitFileIndexStatus } from '../../../../git/models/file';
import type { IssueLabel, IssueOrPullRequestType } from '../../../../git/models/issue';
import { Issue } from '../../../../git/models/issue';
import type { PullRequestState } from '../../../../git/models/pullRequest';
import { PullRequest, PullRequestMergeableState, PullRequestReviewDecision } from '../../../../git/models/pullRequest';
import type { Provider } from '../../../../git/models/remoteProvider';
export interface GitHubBlame {
ranges: GitHubBlameRange[];
@ -160,7 +160,7 @@ export interface GitHubDetailedPullRequest extends GitHubPullRequest {
};
}
export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: RichRemoteProvider): PullRequest {
export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider): PullRequest {
return new PullRequest(
provider,
{
@ -239,10 +239,7 @@ export function toGitHubPullRequestMergeableState(
}
}
export function fromGitHubPullRequestDetailed(
pr: GitHubDetailedPullRequest,
provider: RichRemoteProvider,
): PullRequest {
export function fromGitHubPullRequestDetailed(pr: GitHubDetailedPullRequest, provider: Provider): PullRequest {
return new PullRequest(
provider,
{
@ -299,7 +296,7 @@ export function fromGitHubPullRequestDetailed(
);
}
export function fromGitHubIssueDetailed(value: GitHubIssueDetailed, provider: RichRemoteProvider): Issue {
export function fromGitHubIssueDetailed(value: GitHubIssueDetailed, provider: Provider): Issue {
return new Issue(
{
id: provider.id,

+ 190
- 0
src/plus/integrations/providers/gitlab.ts Vedi File

@ -0,0 +1,190 @@
import type { AuthenticationSession } from 'vscode';
import type { Container } from '../../../container';
import type { Account } from '../../../git/models/author';
import type { DefaultBranch } from '../../../git/models/defaultBranch';
import type { IssueOrPullRequest, SearchedIssue } from '../../../git/models/issue';
import type { PullRequest, PullRequestState, SearchedPullRequest } from '../../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../../git/models/repositoryMetadata';
import { log } from '../../../system/decorators/log';
import type { IntegrationAuthenticationProviderDescriptor } from '../authentication/integrationAuthentication';
import type { SupportedProviderIds } from '../providerIntegration';
import { ensurePaidPlan, ProviderIntegration } from '../providerIntegration';
import { ProviderId, providersMetadata } from './models';
import type { ProvidersApi } from './providersApi';
const metadata = providersMetadata[ProviderId.GitLab];
const authProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
id: metadata.id,
scopes: metadata.scopes,
});
const enterpriseMetadata = providersMetadata[ProviderId.GitLabSelfHosted];
const enterpriseAuthProvider: IntegrationAuthenticationProviderDescriptor = Object.freeze({
id: enterpriseMetadata.id,
scopes: enterpriseMetadata.scopes,
});
export type GitLabRepositoryDescriptor = {
key: string;
owner: string;
name: string;
};
export class GitLabIntegration extends ProviderIntegration<GitLabRepositoryDescriptor> {
readonly authProvider = authProvider;
readonly id: SupportedProviderIds = ProviderId.GitLab;
readonly name: string = 'GitLab';
get domain(): string {
return metadata.domain;
}
protected get apiBaseUrl(): string {
return 'https://gitlab.com/api';
}
protected override async getProviderAccountForCommit(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
ref: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return (await this.container.gitlab)?.getAccountForCommit(this, accessToken, repo.owner, repo.name, ref, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderAccountForEmail(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
email: string,
options?: {
avatarSize?: number;
},
): Promise<Account | undefined> {
return (await this.container.gitlab)?.getAccountForEmail(this, accessToken, repo.owner, repo.name, email, {
...options,
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderDefaultBranch(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
): Promise<DefaultBranch | undefined> {
return (await this.container.gitlab)?.getDefaultBranch(this, accessToken, repo.owner, repo.name, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderIssueOrPullRequest(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
id: string,
): Promise<IssueOrPullRequest | undefined> {
return (await this.container.gitlab)?.getIssueOrPullRequest(
this,
accessToken,
repo.owner,
repo.name,
Number(id),
{
baseUrl: this.apiBaseUrl,
},
);
}
protected override async getProviderPullRequestForBranch(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
branch: string,
options?: {
avatarSize?: number;
include?: PullRequestState[];
},
): Promise<PullRequest | undefined> {
const { include, ...opts } = options ?? {};
const toGitLabMergeRequestState = (await import(/* webpackChunkName: "gitlab" */ './gitlab/models'))
.toGitLabMergeRequestState;
return (await this.container.gitlab)?.getPullRequestForBranch(
this,
accessToken,
repo.owner,
repo.name,
branch,
{
...opts,
include: include?.map(s => toGitLabMergeRequestState(s)),
baseUrl: this.apiBaseUrl,
},
);
}
protected override async getProviderPullRequestForCommit(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
ref: string,
): Promise<PullRequest | undefined> {
return (await this.container.gitlab)?.getPullRequestForCommit(this, accessToken, repo.owner, repo.name, ref, {
baseUrl: this.apiBaseUrl,
});
}
protected override async getProviderRepositoryMetadata(
{ accessToken }: AuthenticationSession,
repo: GitLabRepositoryDescriptor,
): Promise<RepositoryMetadata | undefined> {
return (await this.container.gitlab)?.getRepositoryMetadata(this, accessToken, repo.owner, repo.name, {
baseUrl: this.apiBaseUrl,
});
}
protected override searchProviderMyPullRequests(
_session: AuthenticationSession,
_repo?: GitLabRepositoryDescriptor,
): Promise<SearchedPullRequest[] | undefined> {
return Promise.resolve(undefined);
}
protected override searchProviderMyIssues(
_session: AuthenticationSession,
_repo?: GitLabRepositoryDescriptor,
): Promise<SearchedIssue[] | undefined> {
return Promise.resolve(undefined);
}
}
export class GitLabSelfHostedIntegration extends GitLabIntegration {
override readonly authProvider = enterpriseAuthProvider;
override readonly id = ProviderId.GitHubEnterprise;
override readonly name = 'GitLab Self-Hosted';
override get domain(): string {
return this._domain;
}
protected override get apiBaseUrl(): string {
return `https://${this._domain}/api`;
}
protected override get key(): `${SupportedProviderIds}:${string}` {
return `${this.id}:${this.domain}`;
}
constructor(
container: Container,
override readonly api: ProvidersApi,
private readonly _domain: string,
) {
super(container, api);
}
@log()
override async connect(): Promise<boolean> {
if (!(await ensurePaidPlan(`${this.name} instance`, this.container))) {
return false;
}
return super.connect();
}
}

src/plus/gitlab/gitlab.ts → src/plus/integrations/providers/gitlab/gitlab.ts Vedi File

@ -4,8 +4,8 @@ import { Uri, window } from 'vscode';
import type { RequestInit, Response } from '@env/fetch';
import { fetch, getProxyAgent, wrapForForcedInsecureSSL } from '@env/fetch';
import { isWeb } from '@env/platform';
import type { CoreConfiguration } from '../../constants';
import type { Container } from '../../container';
import type { CoreConfiguration } from '../../../../constants';
import type { Container } from '../../../../container';
import {
AuthenticationError,
AuthenticationErrorReason,
@ -14,24 +14,24 @@ import {
ProviderRequestClientError,
ProviderRequestNotFoundError,
ProviderRequestRateLimitError,
} from '../../errors';
import type { Account } from '../../git/models/author';
import type { DefaultBranch } from '../../git/models/defaultBranch';
import type { IssueOrPullRequest } from '../../git/models/issue';
import { PullRequest } from '../../git/models/pullRequest';
import type { RepositoryMetadata } from '../../git/models/repositoryMetadata';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
} from '../../../../errors';
import type { Account } from '../../../../git/models/author';
import type { DefaultBranch } from '../../../../git/models/defaultBranch';
import type { IssueOrPullRequest } from '../../../../git/models/issue';
import { PullRequest } from '../../../../git/models/pullRequest';
import type { Provider } from '../../../../git/models/remoteProvider';
import type { RepositoryMetadata } from '../../../../git/models/repositoryMetadata';
import {
showIntegrationRequestFailed500WarningMessage,
showIntegrationRequestTimedOutWarningMessage,
} from '../../messages';
import { configuration } from '../../system/configuration';
import { debug } from '../../system/decorators/log';
import { Logger } from '../../system/logger';
import type { LogScope } from '../../system/logger.scope';
import { getLogScope, setLogScopeExit } from '../../system/logger.scope';
import { maybeStopWatch } from '../../system/stopwatch';
import { equalsIgnoreCase } from '../../system/string';
} from '../../../../messages';
import { configuration } from '../../../../system/configuration';
import { debug } from '../../../../system/decorators/log';
import { Logger } from '../../../../system/logger';
import type { LogScope } from '../../../../system/logger.scope';
import { getLogScope, setLogScopeExit } from '../../../../system/logger.scope';
import { maybeStopWatch } from '../../../../system/stopwatch';
import { equalsIgnoreCase } from '../../../../system/string';
import type {
GitLabCommit,
GitLabIssue,
@ -68,7 +68,7 @@ export class GitLabApi implements Disposable {
}
private _proxyAgents = new Map<string, HttpsProxyAgent | null | undefined>();
private getProxyAgent(provider: RichRemoteProvider): HttpsProxyAgent | undefined {
private getProxyAgent(provider: Provider): HttpsProxyAgent | undefined {
if (isWeb) return undefined;
let proxyAgent = this._proxyAgents.get(provider.id);
@ -83,7 +83,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getAccountForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForCommit(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -150,7 +150,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getAccountForEmail']>({ args: { 0: p => p.name, 1: '<token>' } })
async getAccountForEmail(
provider: RichRemoteProvider,
provider: Provider,
token: string,
_owner: string,
_repo: string,
@ -181,7 +181,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getDefaultBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getDefaultBranch(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -241,7 +241,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getIssueOrPullRequest']>({ args: { 0: p => p.name, 1: '<token>' } })
async getIssueOrPullRequest(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -357,7 +357,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getPullRequestForBranch']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForBranch(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -507,7 +507,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getPullRequestForCommit']>({ args: { 0: p => p.name, 1: '<token>' } })
async getPullRequestForCommit(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -556,7 +556,7 @@ export class GitLabApi implements Disposable {
@debug<GitLabApi['getRepositoryMetadata']>({ args: { 0: p => p.name, 1: '<token>' } })
async getRepositoryMetadata(
provider: RichRemoteProvider,
provider: Provider,
token: string,
owner: string,
repo: string,
@ -606,7 +606,7 @@ export class GitLabApi implements Disposable {
}
private async findUser(
provider: RichRemoteProvider,
provider: Provider,
token: string,
search: string,
options?: {
@ -691,7 +691,7 @@ $search: String!
}
private getProjectId(
provider: RichRemoteProvider,
provider: Provider,
token: string,
group: string,
repo: string,
@ -710,7 +710,7 @@ $search: String!
}
private async getProjectIdCore(
provider: RichRemoteProvider,
provider: Provider,
token: string,
group: string,
repo: string,
@ -762,7 +762,7 @@ $search: String!
}
private async graphql<T extends object>(
provider: RichRemoteProvider,
provider: Provider,
token: string,
baseUrl: string | undefined,
query: string,
@ -820,7 +820,7 @@ $search: String!
}
private async request<T>(
provider: RichRemoteProvider,
provider: Provider,
token: string,
baseUrl: string | undefined,
route: string,
@ -874,7 +874,7 @@ $search: String!
}
private handleRequestError(
provider: RichRemoteProvider | undefined,
provider: Provider | undefined,
token: string,
ex: ProviderFetchError | (Error & { name: 'AbortError' }),
scope: LogScope | undefined,
@ -910,7 +910,7 @@ $search: String!
provider?.trackRequestException();
void showIntegrationRequestFailed500WarningMessage(
`${provider?.name ?? 'GitLab'} failed to respond and might be experiencing issues.${
!provider?.custom
provider == null || provider.id === 'gitlab'
? ' Please visit the [GitLab status page](https://status.gitlab.com) for more information.'
: ''
}`,
@ -939,7 +939,7 @@ $search: String!
}
}
private handleException(ex: Error, provider: RichRemoteProvider, scope: LogScope | undefined): Error {
private handleException(ex: Error, provider: Provider, scope: LogScope | undefined): Error {
Logger.error(ex, scope);
// debugger;
@ -949,7 +949,7 @@ $search: String!
return ex;
}
private async showAuthenticationErrorMessage(ex: AuthenticationError, provider: RichRemoteProvider) {
private async showAuthenticationErrorMessage(ex: AuthenticationError, provider: Provider) {
if (ex.reason === AuthenticationErrorReason.Unauthorized || ex.reason === AuthenticationErrorReason.Forbidden) {
const confirm = 'Reauthenticate';
const result = await window.showErrorMessage(

src/plus/gitlab/models.ts → src/plus/integrations/providers/gitlab/models.ts Vedi File

@ -1,6 +1,6 @@
import type { PullRequestState } from '../../git/models/pullRequest';
import { PullRequest } from '../../git/models/pullRequest';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import type { PullRequestState } from '../../../../git/models/pullRequest';
import { PullRequest } from '../../../../git/models/pullRequest';
import type { Provider } from '../../../../git/models/remoteProvider';
export interface GitLabUser {
id: number;
@ -89,7 +89,7 @@ export interface GitLabMergeRequestREST {
web_url: string;
}
export function fromGitLabMergeRequestREST(pr: GitLabMergeRequestREST, provider: RichRemoteProvider): PullRequest {
export function fromGitLabMergeRequestREST(pr: GitLabMergeRequestREST, provider: Provider): PullRequest {
return new PullRequest(
provider,
{

+ 283
- 0
src/plus/integrations/providers/models.ts Vedi File

@ -0,0 +1,283 @@
import type {
Account,
AzureDevOps,
Bitbucket,
EnterpriseOptions,
GetRepoInput,
GitHub,
GitLab,
GitPullRequest,
GitRepository,
Issue,
Jira,
Trello,
} from '@gitkraken/provider-apis';
export type ProviderAccount = Account;
export type ProviderReposInput = (string | number)[] | GetRepoInput[];
export type ProviderRepoInput = GetRepoInput;
export type ProviderPullRequest = GitPullRequest;
export type ProviderRepository = GitRepository;
export type ProviderIssue = Issue;
export enum ProviderId {
GitHub = 'github',
GitHubEnterprise = 'github-enterprise',
GitLab = 'gitlab',
GitLabSelfHosted = 'gitlab-self-hosted',
Bitbucket = 'bitbucket',
Jira = 'jira',
Trello = 'trello',
AzureDevOps = 'azureDevOps',
}
export enum PullRequestFilter {
Author = 'author',
Assignee = 'assignee',
ReviewRequested = 'review-requested',
Mention = 'mention',
}
export enum IssueFilter {
Author = 'author',
Assignee = 'assignee',
Mention = 'mention',
}
export enum PagingMode {
Project = 'project',
Repo = 'repo',
Repos = 'repos',
}
export interface PagingInput {
cursor?: string | null;
page?: number;
}
export interface PagedRepoInput {
repo: GetRepoInput;
cursor?: string;
}
export interface PagedProjectInput {
namespace: string;
project: string;
cursor?: string;
}
export interface GetPullRequestsOptions {
authorLogin?: string;
assigneeLogins?: string[];
reviewRequestedLogin?: string;
mentionLogin?: string;
cursor?: string; // stringified JSON object of type { type: 'cursor' | 'page'; value: string | number } | {}
baseUrl?: string;
}
export interface GetPullRequestsForRepoInput extends GetPullRequestsOptions {
repo: GetRepoInput;
}
export interface GetPullRequestsForReposInput extends GetPullRequestsOptions {
repos: GetRepoInput[];
}
export interface GetPullRequestsForRepoIdsInput extends GetPullRequestsOptions {
repoIds: (string | number)[];
}
export interface GetIssuesOptions {
authorLogin?: string;
assigneeLogins?: string[];
mentionLogin?: string;
cursor?: string; // stringified JSON object of type { type: 'cursor' | 'page'; value: string | number } | {}
baseUrl?: string;
}
export interface GetIssuesForRepoInput extends GetIssuesOptions {
repo: GetRepoInput;
}
export interface GetIssuesForReposInput extends GetIssuesOptions {
repos: GetRepoInput[];
}
export interface GetIssuesForRepoIdsInput extends GetIssuesOptions {
repoIds: (string | number)[];
}
export interface GetIssuesForAzureProjectInput extends GetIssuesOptions {
namespace: string;
project: string;
}
export interface GetReposOptions {
cursor?: string; // stringified JSON object of type { type: 'cursor' | 'page'; value: string | number } | {}
}
export interface GetReposForAzureProjectInput {
namespace: string;
project: string;
}
export interface PageInfo {
hasNextPage: boolean;
endCursor?: string | null;
nextPage?: number | null;
}
export type GetPullRequestsForReposFn = (
input: (GetPullRequestsForReposInput | GetPullRequestsForRepoIdsInput) & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: GitPullRequest[]; pageInfo?: PageInfo }>;
export type GetPullRequestsForRepoFn = (
input: GetPullRequestsForRepoInput & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: GitPullRequest[]; pageInfo?: PageInfo }>;
export type GetIssuesForReposFn = (
input: (GetIssuesForReposInput | GetIssuesForRepoIdsInput) & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: Issue[]; pageInfo?: PageInfo }>;
export type GetIssuesForRepoFn = (
input: GetIssuesForRepoInput & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: Issue[]; pageInfo?: PageInfo }>;
export type GetIssuesForAzureProjectFn = (
input: GetIssuesForAzureProjectInput & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: Issue[]; pageInfo?: PageInfo }>;
export type GetReposForAzureProjectFn = (
input: GetReposForAzureProjectInput & PagingInput,
options?: EnterpriseOptions,
) => Promise<{ data: GitRepository[]; pageInfo?: PageInfo }>;
export type getCurrentUserFn = (options?: EnterpriseOptions) => Promise<{ data: Account }>;
export type getCurrentUserForInstanceFn = (
input: { namespace: string },
options?: EnterpriseOptions,
) => Promise<{ data: Account }>;
export interface ProviderInfo extends ProviderMetadata {
provider: GitHub | GitLab | Bitbucket | Jira | Trello | AzureDevOps;
getPullRequestsForReposFn?: GetPullRequestsForReposFn;
getPullRequestsForRepoFn?: GetPullRequestsForRepoFn;
getIssuesForReposFn?: GetIssuesForReposFn;
getIssuesForRepoFn?: GetIssuesForRepoFn;
getIssuesForAzureProjectFn?: GetIssuesForAzureProjectFn;
getCurrentUserFn?: getCurrentUserFn;
getCurrentUserForInstanceFn?: getCurrentUserForInstanceFn;
getReposForAzureProjectFn?: GetReposForAzureProjectFn;
}
export interface ProviderMetadata {
domain: string;
id: ProviderId;
issuesPagingMode?: PagingMode;
pullRequestsPagingMode?: PagingMode;
scopes: string[];
supportedPullRequestFilters?: PullRequestFilter[];
supportedIssueFilters?: IssueFilter[];
}
export type Providers = Record<ProviderId, ProviderInfo>;
export type ProvidersMetadata = Record<ProviderId, ProviderMetadata>;
export const providersMetadata: ProvidersMetadata = {
[ProviderId.GitHub]: {
domain: 'github.com',
id: ProviderId.GitHub,
issuesPagingMode: PagingMode.Repos,
pullRequestsPagingMode: PagingMode.Repos,
// Use 'username' property on account for PR filters
supportedPullRequestFilters: [
PullRequestFilter.Author,
PullRequestFilter.Assignee,
PullRequestFilter.ReviewRequested,
PullRequestFilter.Mention,
],
// Use 'username' property on account for issue filters
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
scopes: ['repo', 'read:user', 'user:email'],
},
[ProviderId.GitHubEnterprise]: {
domain: '',
id: ProviderId.GitHubEnterprise,
issuesPagingMode: PagingMode.Repos,
pullRequestsPagingMode: PagingMode.Repos,
// Use 'username' property on account for PR filters
supportedPullRequestFilters: [
PullRequestFilter.Author,
PullRequestFilter.Assignee,
PullRequestFilter.ReviewRequested,
PullRequestFilter.Mention,
],
// Use 'username' property on account for issue filters
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
scopes: ['repo', 'read:user', 'user:email'],
},
[ProviderId.GitLab]: {
domain: 'gitlab.com',
id: ProviderId.GitLab,
issuesPagingMode: PagingMode.Repo,
pullRequestsPagingMode: PagingMode.Repo,
// Use 'username' property on account for PR filters
supportedPullRequestFilters: [
PullRequestFilter.Author,
PullRequestFilter.Assignee,
PullRequestFilter.ReviewRequested,
],
// Use 'username' property on account for issue filters
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee],
scopes: ['read_api', 'read_user', 'read_repository'],
},
[ProviderId.GitLabSelfHosted]: {
domain: '',
id: ProviderId.GitLabSelfHosted,
issuesPagingMode: PagingMode.Repo,
pullRequestsPagingMode: PagingMode.Repo,
// Use 'username' property on account for PR filters
supportedPullRequestFilters: [
PullRequestFilter.Author,
PullRequestFilter.Assignee,
PullRequestFilter.ReviewRequested,
],
// Use 'username' property on account for issue filters
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee],
scopes: ['read_api', 'read_user', 'read_repository'],
},
[ProviderId.Bitbucket]: {
domain: 'bitbucket.org',
id: ProviderId.Bitbucket,
pullRequestsPagingMode: PagingMode.Repo,
// Use 'id' property on account for PR filters
supportedPullRequestFilters: [PullRequestFilter.Author],
scopes: ['account:read', 'repository:read', 'pullrequest:read', 'issue:read'],
},
[ProviderId.AzureDevOps]: {
domain: 'dev.azure.com',
id: ProviderId.AzureDevOps,
issuesPagingMode: PagingMode.Project,
pullRequestsPagingMode: PagingMode.Repo,
// Use 'id' property on account for PR filters
supportedPullRequestFilters: [PullRequestFilter.Author, PullRequestFilter.Assignee],
// Use 'name' property on account for issue filters
supportedIssueFilters: [IssueFilter.Author, IssueFilter.Assignee, IssueFilter.Mention],
scopes: ['vso.code', 'vso.identity', 'vso.project', 'vso.profile', 'vso.work'],
},
[ProviderId.Jira]: {
domain: 'atlassian.net',
id: ProviderId.Jira,
scopes: [],
},
[ProviderId.Trello]: {
domain: 'trello.com',
id: ProviderId.Trello,
scopes: [],
},
};

+ 609
- 0
src/plus/integrations/providers/providersApi.ts Vedi File

@ -0,0 +1,609 @@
import ProviderApis from '@gitkraken/provider-apis';
import type { Container } from '../../../container';
import type { PagedResult } from '../../../git/gitProvider';
import type {
getCurrentUserFn,
getCurrentUserForInstanceFn,
GetIssuesForAzureProjectFn,
GetIssuesForRepoFn,
GetIssuesForReposFn,
GetIssuesOptions,
GetPullRequestsForRepoFn,
GetPullRequestsForReposFn,
GetPullRequestsOptions,
GetReposForAzureProjectFn,
GetReposOptions,
IssueFilter,
PagingMode,
ProviderAccount,
ProviderInfo,
ProviderIssue,
ProviderPullRequest,
ProviderRepoInput,
ProviderReposInput,
ProviderRepository,
Providers,
PullRequestFilter,
} from './models';
import { ProviderId, providersMetadata } from './models';
export class ProvidersApi {
private readonly providers: Providers;
constructor(private readonly container: Container) {
const providerApis = ProviderApis();
this.providers = {
[ProviderId.GitHub]: {
...providersMetadata[ProviderId.GitHub],
provider: providerApis.github,
getCurrentUserFn: providerApis.github.getCurrentUser.bind(providerApis.github) as getCurrentUserFn,
getPullRequestsForReposFn: providerApis.github.getPullRequestsForRepos.bind(
providerApis.github,
) as GetPullRequestsForReposFn,
getIssuesForReposFn: providerApis.github.getIssuesForRepos.bind(
providerApis.github,
) as GetIssuesForReposFn,
},
[ProviderId.GitHubEnterprise]: {
...providersMetadata[ProviderId.GitHubEnterprise],
provider: providerApis.github,
getCurrentUserFn: providerApis.github.getCurrentUser.bind(providerApis.github) as getCurrentUserFn,
getPullRequestsForReposFn: providerApis.github.getPullRequestsForRepos.bind(
providerApis.github,
) as GetPullRequestsForReposFn,
getIssuesForReposFn: providerApis.github.getIssuesForRepos.bind(
providerApis.github,
) as GetIssuesForReposFn,
},
[ProviderId.GitLab]: {
...providersMetadata[ProviderId.GitLab],
provider: providerApis.gitlab,
getCurrentUserFn: providerApis.gitlab.getCurrentUser.bind(providerApis.gitlab) as getCurrentUserFn,
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
providerApis.gitlab,
) as GetPullRequestsForReposFn,
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
providerApis.gitlab,
) as GetPullRequestsForRepoFn,
getIssuesForReposFn: providerApis.gitlab.getIssuesForRepos.bind(
providerApis.gitlab,
) as GetIssuesForReposFn,
getIssuesForRepoFn: providerApis.gitlab.getIssuesForRepo.bind(
providerApis.gitlab,
) as GetIssuesForRepoFn,
},
[ProviderId.GitLabSelfHosted]: {
...providersMetadata[ProviderId.GitLabSelfHosted],
provider: providerApis.gitlab,
getCurrentUserFn: providerApis.gitlab.getCurrentUser.bind(providerApis.gitlab) as getCurrentUserFn,
getPullRequestsForReposFn: providerApis.gitlab.getPullRequestsForRepos.bind(
providerApis.gitlab,
) as GetPullRequestsForReposFn,
getPullRequestsForRepoFn: providerApis.gitlab.getPullRequestsForRepo.bind(
providerApis.gitlab,
) as GetPullRequestsForRepoFn,
getIssuesForReposFn: providerApis.gitlab.getIssuesForRepos.bind(
providerApis.gitlab,
) as GetIssuesForReposFn,
getIssuesForRepoFn: providerApis.gitlab.getIssuesForRepo.bind(
providerApis.gitlab,
) as GetIssuesForRepoFn,
},
[ProviderId.Bitbucket]: {
...providersMetadata[ProviderId.Bitbucket],
provider: providerApis.bitbucket,
getCurrentUserFn: providerApis.bitbucket.getCurrentUser.bind(
providerApis.bitbucket,
) as getCurrentUserFn,
getPullRequestsForReposFn: providerApis.bitbucket.getPullRequestsForRepos.bind(
providerApis.bitbucket,
) as GetPullRequestsForReposFn,
getPullRequestsForRepoFn: providerApis.bitbucket.getPullRequestsForRepo.bind(
providerApis.bitbucket,
) as GetPullRequestsForRepoFn,
},
[ProviderId.AzureDevOps]: {
...providersMetadata[ProviderId.AzureDevOps],
provider: providerApis.azureDevOps,
getCurrentUserForInstanceFn: providerApis.azureDevOps.getCurrentUserForInstance.bind(
providerApis.azureDevOps,
) as getCurrentUserForInstanceFn,
getPullRequestsForReposFn: providerApis.azureDevOps.getPullRequestsForRepos.bind(
providerApis.azureDevOps,
) as GetPullRequestsForReposFn,
getPullRequestsForRepoFn: providerApis.azureDevOps.getPullRequestsForRepo.bind(
providerApis.azureDevOps,
) as GetPullRequestsForRepoFn,
getIssuesForAzureProjectFn: providerApis.azureDevOps.getIssuesForAzureProject.bind(
providerApis.azureDevOps,
) as GetIssuesForAzureProjectFn,
getReposForAzureProjectFn: providerApis.azureDevOps.getReposForAzureProject.bind(
providerApis.azureDevOps,
) as GetReposForAzureProjectFn,
},
[ProviderId.Jira]: {
...providersMetadata[ProviderId.Jira],
provider: providerApis.jira,
},
[ProviderId.Trello]: {
...providersMetadata[ProviderId.Trello],
provider: providerApis.trello,
},
};
}
getScopesForProvider(providerId: ProviderId): string[] | undefined {
return this.providers[providerId]?.scopes;
}
getProviderDomain(providerId: ProviderId): string | undefined {
return this.providers[providerId]?.domain;
}
getProviderPullRequestsPagingMode(providerId: ProviderId): PagingMode | undefined {
return this.providers[providerId]?.pullRequestsPagingMode;
}
getProviderIssuesPagingMode(providerId: ProviderId): PagingMode | undefined {
return this.providers[providerId]?.issuesPagingMode;
}
providerSupportsPullRequestFilters(providerId: ProviderId, filters: PullRequestFilter[]): boolean {
return (
this.providers[providerId]?.supportedPullRequestFilters != null &&
filters.every(filter => this.providers[providerId]?.supportedPullRequestFilters?.includes(filter))
);
}
providerSupportsIssueFilters(providerId: ProviderId, filters: IssueFilter[]): boolean {
return (
this.providers[providerId]?.supportedIssueFilters != null &&
filters.every(filter => this.providers[providerId]?.supportedIssueFilters?.includes(filter))
);
}
isRepoIdsInput(input: any): input is (string | number)[] {
return (
input != null &&
Array.isArray(input) &&
input.every((id: any) => typeof id === 'string' || typeof id === 'number')
);
}
async getProviderToken(
provider: ProviderInfo,
options?: { createSessionIfNeeded?: boolean },
): Promise<string | undefined> {
const providerDescriptor =
provider.domain == null || provider.scopes == null
? undefined
: { domain: provider.domain, scopes: provider.scopes };
try {
return (
await this.container.integrationAuthentication.getSession(provider.id, providerDescriptor, {
createIfNeeded: options?.createSessionIfNeeded,
})
)?.accessToken;
} catch {
return undefined;
}
}
async getPullRequestsForRepos(
providerId: ProviderId,
reposOrIds: ProviderReposInput,
options?: GetPullRequestsOptions,
): Promise<PagedResult<ProviderPullRequest>> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getPullRequestsForReposFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting pull requests for repositories`);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const input = {
...(this.isRepoIdsInput(reposOrIds) ? { repoIds: reposOrIds } : { repos: reposOrIds }),
...options,
...cursorOrPage,
};
const result = await provider.getPullRequestsForReposFn(input, { token: token, isPAT: true });
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getPullRequestsForRepo(
providerId: ProviderId,
repo: ProviderRepoInput,
options?: GetPullRequestsOptions,
): Promise<PagedResult<ProviderPullRequest>> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getPullRequestsForRepoFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting pull requests for a repository`);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const result = await provider.getPullRequestsForRepoFn(
{
repo: repo,
...options,
...cursorOrPage,
},
{ token: token, isPAT: true },
);
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getIssuesForRepos(
providerId: ProviderId,
reposOrIds: ProviderReposInput,
options?: GetIssuesOptions,
): Promise<PagedResult<ProviderIssue>> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getIssuesForReposFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting issues for repositories`);
}
if (provider.id === ProviderId.AzureDevOps) {
throw new Error(
`Provider with id ${providerId} does not support getting issues for repositories; use getIssuesForAzureProject instead`,
);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const input = {
...(this.isRepoIdsInput(reposOrIds) ? { repoIds: reposOrIds } : { repos: reposOrIds }),
...options,
...cursorOrPage,
};
const result = await provider.getIssuesForReposFn(input, { token: token, isPAT: true });
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getIssuesForRepo(
providerId: ProviderId,
repo: ProviderRepoInput,
options?: GetIssuesOptions,
): Promise<PagedResult<ProviderIssue>> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getIssuesForRepoFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting issues for a repository`);
}
if (provider.id === ProviderId.AzureDevOps) {
throw new Error(
`Provider with id ${providerId} does not support getting issues for a repository; use getIssuesForAzureProject instead`,
);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const result = await provider.getIssuesForRepoFn(
{
repo: repo,
...options,
...cursorOrPage,
},
{ token: token, isPAT: true },
);
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getIssuesForAzureProject(
namespace: string,
project: string,
options?: GetIssuesOptions,
): Promise<PagedResult<ProviderIssue>> {
const provider = this.providers[ProviderId.AzureDevOps];
if (provider == null) {
throw new Error(`Provider with id ${ProviderId.AzureDevOps} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${ProviderId.AzureDevOps}`);
}
if (provider.getIssuesForAzureProjectFn == null) {
throw new Error(
`Provider with id ${ProviderId.AzureDevOps} does not support getting issues for an Azure project`,
);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const result = await provider.getIssuesForAzureProjectFn(
{
namespace: namespace,
project: project,
...options,
...cursorOrPage,
},
{ token: token, isPAT: true },
);
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getReposForAzureProject(
namespace: string,
project: string,
options?: GetReposOptions,
): Promise<PagedResult<ProviderRepository>> {
const provider = this.providers[ProviderId.AzureDevOps];
if (provider == null) {
throw new Error(`Provider with id ${ProviderId.AzureDevOps} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${ProviderId.AzureDevOps}`);
}
if (provider.getReposForAzureProjectFn == null) {
throw new Error(
`Provider with id ${ProviderId.AzureDevOps} does not support getting repositories for Azure projects`,
);
}
let cursorInfo;
try {
cursorInfo = JSON.parse(options?.cursor ?? '{}');
} catch {
cursorInfo = {};
}
const cursorValue = cursorInfo.value;
const cursorType = cursorInfo.type;
let cursorOrPage = {};
if (cursorType === 'page') {
cursorOrPage = { page: cursorValue };
} else if (cursorType === 'cursor') {
cursorOrPage = { cursor: cursorValue };
}
const result = await provider.getReposForAzureProjectFn(
{
namespace: namespace,
project: project,
...cursorOrPage,
},
{ token: token, isPAT: true },
);
const hasMore = result.pageInfo?.hasNextPage ?? false;
let nextCursor = '{}';
if (result.pageInfo?.endCursor != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.endCursor, type: 'cursor' });
} else if (result.pageInfo?.nextPage != null) {
nextCursor = JSON.stringify({ value: result.pageInfo?.nextPage, type: 'page' });
}
return {
values: result.data,
paging: {
cursor: nextCursor,
more: hasMore,
},
};
}
async getCurrentUser(providerId: ProviderId): Promise<ProviderAccount> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getCurrentUserFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting current user`);
}
const { data: account } = await provider.getCurrentUserFn({ token: token, isPAT: true });
return account;
}
async getCurrentUserForInstance(providerId: ProviderId, namespace: string): Promise<ProviderAccount> {
const provider = this.providers[providerId];
if (provider == null) {
throw new Error(`Provider with id ${providerId} not registered`);
}
const token = await this.getProviderToken(provider);
if (token == null) {
throw new Error(`Not connected to provider ${providerId}`);
}
if (provider.getCurrentUserForInstanceFn == null) {
throw new Error(`Provider with id ${providerId} does not support getting current user for an instance`);
}
const { data: account } = await provider.getCurrentUserForInstanceFn(
{ namespace: namespace },
{ token: token, isPAT: true },
);
return account;
}
}

+ 7
- 5
src/plus/webviews/focus/focusWebview.ts Vedi File

@ -23,7 +23,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode } from '../../../git/m
import type { GitWorktree } from '../../../git/models/worktree';
import { getWorktreeForBranch } from '../../../git/models/worktree';
import { parseGitRemoteUrl } from '../../../git/parsers/remoteParser';
import type { RichRemoteProvider } from '../../../git/remotes/richRemoteProvider';
import type { RemoteProvider } from '../../../git/remotes/remoteProvider';
import { executeCommand } from '../../../system/command';
import { debug } from '../../../system/decorators/log';
import { Logger } from '../../../system/logger';
@ -59,7 +59,7 @@ import {
interface RepoWithRichRemote {
repo: Repository;
remote: GitRemote<RichRemoteProvider>;
remote: GitRemote<RemoteProvider>;
isConnected: boolean;
isGitHub: boolean;
}
@ -528,10 +528,12 @@ export class FocusWebviewProvider implements WebviewProvider {
disposables.push(repo.onDidChange(this.onRepositoryChanged, this));
const provider = this.container.integrations.getByRemote(richRemote);
repos.push({
repo: repo,
remote: richRemote,
isConnected: await richRemote.provider.isConnected(),
isConnected: provider?.maybeConnected ?? (await provider?.isConnected()) ?? false,
isGitHub: richRemote.provider.id === 'github',
});
}
@ -565,7 +567,7 @@ export class FocusWebviewProvider implements WebviewProvider {
const branchesByRepo = new Map<Repository, PageableResult<GitBranch>>();
const worktreesByRepo = new Map<Repository, GitWorktree[]>();
const queries = richRepos.map(r => [r, this.container.git.getMyPullRequests(r.remote)] as const);
const queries = richRepos.map(r => [r, this.container.integrations.getMyPullRequests(r.remote)] as const);
for (const [r, query] of queries) {
let prs;
try {
@ -647,7 +649,7 @@ export class FocusWebviewProvider implements WebviewProvider {
if (force || this._pullRequests == null) {
const allIssues = [];
const queries = richRepos.map(r => [r, this.container.git.getMyIssues(r.remote)] as const);
const queries = richRepos.map(r => [r, this.container.integrations.getMyIssues(r.remote)] as const);
for (const [r, query] of queries) {
let issues;
try {

+ 2
- 2
src/plus/webviews/focus/protocol.ts Vedi File

@ -9,7 +9,7 @@ export interface State extends WebviewState {
access: FeatureAccess;
pullRequests?: PullRequestResult[];
issues?: IssueResult[];
repos?: RepoWithRichProvider[];
repos?: RepoWithIntegration[];
}
export interface SearchResultBase {
@ -35,7 +35,7 @@ export interface PullRequestResult extends SearchResultBase {
hasLocalBranch: boolean;
}
export interface RepoWithRichProvider {
export interface RepoWithIntegration {
repo: string;
isGitHub: boolean;
isConnected: boolean;

+ 3
- 2
src/quickpicks/remoteProviderPicker.ts Vedi File

@ -54,8 +54,9 @@ export class CopyOrOpenRemoteCommandQuickPickItem extends CommandQuickPickItem {
let branch = resource.base.branch;
if (branch == null) {
branch = await Container.instance.git.getDefaultBranchName(this.remote.repoPath, this.remote.name);
if (branch == null && this.remote.hasRichIntegration()) {
const defaultBranch = await this.remote.provider.getDefaultBranch?.();
if (branch == null && this.remote.hasIntegration()) {
const provider = Container.instance.integrations.getByRemote(this.remote);
const defaultBranch = await provider?.getDefaultBranch?.(this.remote.provider.repoDesc);
branch = defaultBranch?.name;
}
}

+ 1
- 1
src/statusbar/statusBarController.ts Vedi File

@ -306,7 +306,7 @@ export class StatusBarController implements Disposable {
const showPullRequests =
!commit.isUncommitted &&
remote?.hasRichIntegration() &&
remote?.hasIntegration() &&
cfg.pullRequests.enabled &&
(CommitFormatter.has(
cfg.format,

+ 3
- 3
src/views/nodes/commitNode.ts Vedi File

@ -9,7 +9,7 @@ import type { GitCommit } from '../../git/models/commit';
import type { PullRequest } from '../../git/models/pullRequest';
import type { GitRevisionReference } from '../../git/models/reference';
import type { GitRemote } from '../../git/models/remote';
import type { RichRemoteProvider } from '../../git/remotes/richRemoteProvider';
import type { RemoteProvider } from '../../git/remotes/remoteProvider';
import { makeHierarchical } from '../../system/array';
import { pauseOnCancelOrTimeoutMapTuplePromise } from '../../system/cancellation';
import { configuration } from '../../system/configuration';
@ -232,7 +232,7 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis
private async getAssociatedPullRequest(
commit: GitCommit,
remote?: GitRemote<RichRemoteProvider>,
remote?: GitRemote<RemoteProvider>,
): Promise<PullRequest | undefined> {
let pullRequest = this.getState('pullRequest');
if (pullRequest !== undefined) return Promise.resolve(pullRequest ?? undefined);
@ -264,7 +264,7 @@ export class CommitNode extends ViewRefNode<'commit', ViewsWithCommits | FileHis
let enrichedAutolinks;
let pr;
if (remote?.hasRichIntegration()) {
if (remote?.hasIntegration()) {
const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
pauseOnCancelOrTimeoutMapTuplePromise(this.commit.getEnrichedAutolinks(remote)),
this.getAssociatedPullRequest(this.commit, remote),

+ 1
- 1
src/views/nodes/fileRevisionAsCommitNode.ts Vedi File

@ -246,7 +246,7 @@ export async function getFileRevisionAsCommitTooltip(
let enrichedAutolinks;
let pr;
if (remote?.hasRichIntegration()) {
if (remote?.hasIntegration()) {
const [enrichedAutolinksResult, prResult] = await Promise.allSettled([
pauseOnCancelOrTimeoutMapTuplePromise(commit.getEnrichedAutolinks(remote)),
commit.getAssociatedPullRequest(remote),

+ 3
- 2
src/views/nodes/remoteNode.ts Vedi File

@ -102,8 +102,9 @@ export class RemoteNode extends ViewNode<'remote', ViewsWithRemotes> {
),
};
if (provider.hasRichIntegration()) {
const connected = provider.maybeConnected ?? (await provider.isConnected());
if (this.remote.hasIntegration()) {
const integration = this.view.container.integrations.getByRemote(this.remote);
const connected = integration?.maybeConnected ?? (await integration?.isConnected());
item.contextValue = `${ContextValues.Remote}${connected ? '+connected' : '+disconnected'}`;
item.tooltip = `${this.remote.name} (${provider.name} ${GlyphChars.Dash} ${

+ 2
- 2
src/webviews/commitDetails/commitDetailsWebview.ts Vedi File

@ -647,7 +647,7 @@ export class CommitDetailsWebviewProvider
const { commit } = current;
if (commit == null) return;
const remote = await this.container.git.getBestRemoteWithRichProvider(commit.repoPath);
const remote = await this.container.git.getBestRemoteWithIntegration(commit.repoPath);
if (cancellation.isCancellationRequested) return;
@ -966,7 +966,7 @@ export class CommitDetailsWebviewProvider
const [commitResult, avatarUriResult, remoteResult] = await Promise.allSettled([
!commit.hasFullDetails() ? commit.ensureFullDetails().then(() => commit) : commit,
commit.author.getAvatarUri(commit, { size: 32 }),
this.container.git.getBestRemoteWithRichProvider(commit.repoPath, { includeDisconnected: true }),
this.container.git.getBestRemoteWithIntegration(commit.repoPath, { includeDisconnected: true }),
]);
commit = getSettledValue(commitResult, commit);

+ 8
- 1
yarn.lock Vedi File

@ -242,6 +242,13 @@
react-dragula "1.1.17"
react-onclickoutside "^6.13.0"
"@gitkraken/provider-apis@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@gitkraken/provider-apis/-/provider-apis-0.10.0.tgz#75cc20d710f07f3eb28df89d83213833b6d40073"
integrity sha512-aXMNYROQ/7fCTk08/dsDVqdyvYkohgU39dIsbqdhdj5uRjTVmybqaS3F9mQXPSdnTqztAkz46w43t8JfIi+gTQ==
dependencies:
node-fetch "2.6.9"
"@gitkraken/shared-web-components@0.1.1-rc.15":
version "0.1.1-rc.15"
resolved "https://registry.yarnpkg.com/@gitkraken/shared-web-components/-/shared-web-components-0.1.1-rc.15.tgz#efd520083a7f5a32fe342108f447e8c77723cfd2"
@ -4920,7 +4927,7 @@ node-addon-api@^6.1.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
node-fetch@2.7.0:
node-fetch@2.6.9, node-fetch@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==

Caricamento…
Annulla
Salva