Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { createExperimentationService, ExperimentationTelemetry } from './experi
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
import { CredentialStore } from './github/credentials';
import { FolderRepositoryManager } from './github/folderRepositoryManager';
import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver';
import { OverviewRestorer } from './github/overviewRestorer';
import { RepositoriesManager } from './github/repositoriesManager';
import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api';
Expand Down Expand Up @@ -292,7 +293,9 @@ async function init(

context.subscriptions.push(new GitLensIntegration());

context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore));
const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry);
context.subscriptions.push(folderRepositoryManagerResolver);
context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore, folderRepositoryManagerResolver));

await vscode.commands.executeCommand('setContext', 'github:initialized', true);

Expand All @@ -302,7 +305,7 @@ async function init(
await resumePendingCheckout(reviewsManager, context, reposManager);

initChat(context, credentialStore, reposManager);
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git)));
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git, folderRepositoryManagerResolver)));

// Make sure any compare changes tabs, which come from the create flow, are closed.
CompareChanges.closeTabs();
Expand Down
52 changes: 6 additions & 46 deletions src/github/externalUriOpener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,20 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
import { Disposable } from '../common/lifecycle';
import { IThemeWatcher } from '../themeWatcher';
import { CredentialStore } from './credentials';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver';
import { IssueOverviewPanel } from './issueOverview';
import { PullRequestOverviewPanel } from './pullRequestOverview';
import { RepositoriesManager } from './repositoriesManager';
import { GitApiImpl } from '../api/api1';
import { getGitHubIssueOrPullRequestUriOpenerPriority, openWithDefaultExternalOpener, parseGitHubIssueOrPullRequestUri } from '../common/externalUri';
import { Disposable } from '../common/lifecycle';
import { OPEN_PULL_LINKS, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
import { ITelemetry } from '../common/telemetry';
import { EXTENSION_ID } from '../constants';
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
import { ThemeData } from '../view/theme';

class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vscode.ExternalUriOpener {
private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined;

constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _repositoriesManager: RepositoriesManager,
private readonly _credentialStore: CredentialStore,
private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
private readonly _telemetry: ITelemetry,
) {
super();
Expand All @@ -51,7 +42,7 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs
return;
}

const folderRepositoryManager = this.getFolderRepositoryManager(identity.owner, identity.repo);
const folderRepositoryManager = this._folderRepositoryManagerResolver.getManagerForRepository(identity.owner, identity.repo);
if (identity.kind === 'pullRequest') {
const pullRequest = await folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true);
if (token.isCancellationRequested) {
Expand Down Expand Up @@ -91,43 +82,12 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs
return vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(OPEN_PULL_LINKS, true);
}

private getFolderRepositoryManager(owner: string, repo: string): FolderRepositoryManager {
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo)
?? this._repositoriesManager.folderManagers[0];
if (existingManager) {
return existingManager;
}
if (this._remoteFolderRepositoryManager) {
return this._remoteFolderRepositoryManager;
}

const repository = this._register(new RemoteOnlyRepository());
const git = this._register(new GitApiImpl(this._repositoriesManager));
const createPullRequestHelper = this._register(new CreatePullRequestHelper());
const onDidChangeTheme = this._register(new vscode.EventEmitter<ThemeData | undefined>());
const themeWatcher: IThemeWatcher = {
onDidChangeTheme: onDidChangeTheme.event,
themeData: undefined,
};
this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager(
-1,
this._context,
repository,
this._telemetry,
git,
this._credentialStore,
createPullRequestHelper,
themeWatcher,
));
return this._remoteFolderRepositoryManager;
}
}

export function registerGitHubIssueOrPullRequestExternalUriOpener(
context: vscode.ExtensionContext,
repositoriesManager: RepositoriesManager,
credentialStore: CredentialStore,
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
telemetry: ITelemetry,
): vscode.Disposable {
return new GitHubIssueOrPullRequestExternalUriOpener(context, repositoriesManager, credentialStore, telemetry);
return new GitHubIssueOrPullRequestExternalUriOpener(context, folderRepositoryManagerResolver, telemetry);
}
58 changes: 58 additions & 0 deletions src/github/folderRepositoryManagerResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { RepositoriesManager } from './repositoriesManager';
import { GitApiImpl } from '../api/api1';
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
import { Disposable } from '../common/lifecycle';
import { ITelemetry } from '../common/telemetry';
import { IThemeWatcher } from '../themeWatcher';
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
import { ThemeData } from '../view/theme';

export class FolderRepositoryManagerResolver extends Disposable {
private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined;

constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _repositoriesManager: RepositoriesManager,
private readonly _telemetry: ITelemetry,
) {
super();
}

getManagerForRepository(owner: string, repo: string): FolderRepositoryManager {
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo)
?? this._repositoriesManager.folderManagers[0];
if (existingManager) {
return existingManager;
}
if (this._remoteFolderRepositoryManager) {
return this._remoteFolderRepositoryManager;
}

const repository = this._register(new RemoteOnlyRepository());
const git = this._register(new GitApiImpl(this._repositoriesManager));
const createPullRequestHelper = this._register(new CreatePullRequestHelper());
const onDidChangeTheme = this._register(new vscode.EventEmitter<ThemeData | undefined>());
const themeWatcher: IThemeWatcher = {
onDidChangeTheme: onDidChangeTheme.event,
themeData: undefined,
};
this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager(
-1,
this._context,
repository,
this._telemetry,
git,
this._repositoriesManager.credentialStore,
createPullRequestHelper,
themeWatcher,
));
return this._remoteFolderRepositoryManager;
}
}
6 changes: 4 additions & 2 deletions src/github/overviewRestorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as vscode from 'vscode';
import { CredentialStore } from './credentials';
import { registerGitHubIssueOrPullRequestExternalUriOpener } from './externalUriOpener';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver';
import { GitHubRepository } from './githubRepository';
import { IssueOverviewPanel } from './issueOverview';
import { PullRequestOverviewPanel } from './pullRequestOverview';
Expand All @@ -22,12 +23,13 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
constructor(private readonly _repositoriesManager: RepositoriesManager,
private readonly _telemetry: ITelemetry,
private readonly _context: vscode.ExtensionContext,
private readonly _credentialStore: CredentialStore
private readonly _credentialStore: CredentialStore,
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
) {
super();
this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this));
this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this));
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, _repositoriesManager, _credentialStore, _telemetry));
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, folderRepositoryManagerResolver, _telemetry));
}

async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise<void> {
Expand Down
6 changes: 4 additions & 2 deletions src/test/github/externalUriOpener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { RemoteOnlyRepository } from '../../api/remoteOnlyRepository';
import { CredentialStore } from '../../github/credentials';
import { registerGitHubIssueOrPullRequestExternalUriOpener } from '../../github/externalUriOpener';
import { FolderRepositoryManager } from '../../github/folderRepositoryManager';
import { FolderRepositoryManagerResolver } from '../../github/folderRepositoryManagerResolver';
import { RepositoriesManager } from '../../github/repositoriesManager';
import { MockExtensionContext } from '../mocks/mockExtensionContext';
import { MockTelemetry } from '../mocks/mockTelemetry';
Expand All @@ -30,6 +31,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {
const telemetry = new MockTelemetry();
const credentialStore = new CredentialStore(telemetry, context);
const repositoriesManager = new RepositoriesManager(credentialStore, telemetry);
const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry);
let opener: vscode.ExternalUriOpener | undefined;
sandbox.stub(vscode.window, 'registerExternalUriOpener').callsFake((_id, value) => {
opener = value;
Expand All @@ -43,8 +45,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {

const registration = registerGitHubIssueOrPullRequestExternalUriOpener(
context,
repositoriesManager,
credentialStore,
folderRepositoryManagerResolver,
telemetry,
);
const uri = vscode.Uri.parse('https://github.com/microsoft/vscode/issues/1');
Expand All @@ -56,6 +57,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {
assert.strictEqual(repositoriesManager.folderManagers.length, 0);
assert.strictEqual(resolveIssue.callCount, 1);
registration.dispose();
folderRepositoryManagerResolver.dispose();
repositoriesManager.dispose();
credentialStore.dispose();
});
Expand Down
66 changes: 66 additions & 0 deletions src/test/uriHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { default as assert } from 'assert';
import { createSandbox, SinonSandbox } from 'sinon';
import * as vscode from 'vscode';
import { GitApiImpl } from '../api/api1';
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
import { CredentialStore } from '../github/credentials';
import { FolderRepositoryManagerResolver } from '../github/folderRepositoryManagerResolver';
import { PullRequestOverviewPanel } from '../github/pullRequestOverview';
import { RepositoriesManager } from '../github/repositoriesManager';
import { UriHandler } from '../uriHandler';
import { ReviewsManager } from '../view/reviewsManager';
import { MockExtensionContext } from './mocks/mockExtensionContext';
import { MockTelemetry } from './mocks/mockTelemetry';

describe('UriHandler', function () {
let context: MockExtensionContext;
let credentialStore: CredentialStore;
let folderRepositoryManagerResolver: FolderRepositoryManagerResolver;
let git: GitApiImpl;
let repositoriesManager: RepositoriesManager;
let sandbox: SinonSandbox;
let telemetry: MockTelemetry;

beforeEach(function () {
sandbox = createSandbox();
context = new MockExtensionContext();
telemetry = new MockTelemetry();
credentialStore = new CredentialStore(telemetry, context);
repositoriesManager = new RepositoriesManager(credentialStore, telemetry);
git = new GitApiImpl(repositoriesManager);
folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry);
});

afterEach(function () {
folderRepositoryManagerResolver.dispose();
git.dispose();
repositoriesManager.dispose();
credentialStore.dispose();
context.dispose();
sandbox.restore();
});

it('opens a pull request with a remote-only manager when no workspace is open', async function () {
const createOrShow = sandbox.stub(PullRequestOverviewPanel, 'createOrShow').resolves();
const handler = new UriHandler(
repositoriesManager,
{} as ReviewsManager,
telemetry,
context,
git,
folderRepositoryManagerResolver,
);
const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/open-pull-request-webview?uri=https://github.com/microsoft/vscode/pull/1');

await handler.handleUri(uri);

assert.strictEqual(createOrShow.callCount, 1);
assert.ok(createOrShow.firstCall.args[2].repository instanceof RemoteOnlyRepository);
assert.strictEqual(repositoriesManager.folderManagers.length, 0);
});
});
8 changes: 5 additions & 3 deletions src/uriHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Logger from './common/logger';
import { ITelemetry } from './common/telemetry';
import { fromOpenIssueWebviewUri, fromOpenOrCheckoutPullRequestWebviewUri, UriHandlerPaths } from './common/uri';
import { FolderRepositoryManager } from './github/folderRepositoryManager';
import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver';
import { IssueOverviewPanel } from './github/issueOverview';
import { PullRequestModel } from './github/pullRequestModel';
import { PullRequestOverviewPanel } from './github/pullRequestOverview';
Expand Down Expand Up @@ -106,7 +107,8 @@ export class UriHandler implements vscode.UriHandler {
private readonly _reviewsManagers: ReviewsManager,
private readonly _telemetry: ITelemetry,
private readonly _context: vscode.ExtensionContext,
private readonly _git: GitApiImpl
private readonly _git: GitApiImpl,
private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
) { }

async handleUri(uri: vscode.Uri): Promise<void> {
Expand All @@ -129,7 +131,7 @@ export class UriHandler implements vscode.UriHandler {
if (!params) {
return;
}
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0];
const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo);
const identity = { owner: params.owner, repo: params.repo, number: params.issueNumber };
return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, identity);
}
Expand All @@ -141,7 +143,7 @@ export class UriHandler implements vscode.UriHandler {
Logger.error('Failed to parse pull request URI.', UriHandler.ID);
return;
}
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0];
const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo);
return { folderManager, identity: { owner: params.owner, repo: params.repo, number: params.pullRequestNumber } };
}

Expand Down