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
14 changes: 8 additions & 6 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { CrossChatSessionWithPR } from './github/copilotApi';
import { CopilotRemoteAgentManager, SessionIdForPr } from './github/copilotRemoteAgent';
import { guessExtensionFromMime, pickFilesForUpload, placeholdersForNames, runFileUploads, runPendingUploads } from './github/fileUpload';
import { FolderRepositoryManager } from './github/folderRepositoryManager';
import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver';
import { GitHubRepository } from './github/githubRepository';
import { Issue } from './github/interface';
import { IssueModel } from './github/issueModel';
Expand Down Expand Up @@ -192,7 +193,8 @@ export function registerCommands(
copilotRemoteAgentManager: CopilotRemoteAgentManager,
notificationManager: NotificationsManager,
prsTreeModel: PrsTreeModel,
tree: PullRequestsTreeDataProvider
tree: PullRequestsTreeDataProvider,
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
) {
const logId = 'RegisterCommands';

Expand Down Expand Up @@ -573,10 +575,7 @@ export function registerCommands(
return undefined;
}

const folderManager = reposManager.getManagerForRepository(context.owner, context.repo) ?? reposManager.folderManagers[0];
if (!folderManager) {
return undefined;
}
const folderManager = folderRepositoryManagerResolver.getManagerForRepository(context.owner, context.repo);

const pr = await folderManager.resolvePullRequest(context.owner, context.repo, context.number, true);
if (!pr) {
Expand Down Expand Up @@ -1123,7 +1122,10 @@ export function registerCommands(
return;
}

const folderManager = reposManager.getManagerForIssueModel(issueModel) ?? reposManager.folderManagers[0];
const folderManager = folderRepositoryManagerResolver.getManagerForRepository(
issueModel.remote.owner,
issueModel.remote.repositoryName,
);

let descriptionNode: PRNode | RepositoryChangesNode | undefined;
if (argument instanceof PRNode) {
Expand Down
9 changes: 5 additions & 4 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,10 @@ async function init(

tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager);

registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel, tree);
const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry);
context.subscriptions.push(folderRepositoryManagerResolver);

registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel, tree, folderRepositoryManagerResolver);

const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat');
Expand Down Expand Up @@ -293,9 +296,7 @@ async function init(

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

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

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

Expand Down
3 changes: 1 addition & 2 deletions src/github/folderRepositoryManagerResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ export class FolderRepositoryManagerResolver extends Disposable {
}

getManagerForRepository(owner: string, repo: string): FolderRepositoryManager {
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo)
?? this._repositoriesManager.folderManagers[0];
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo);
if (existingManager) {
return existingManager;
}
Expand Down
4 changes: 4 additions & 0 deletions src/github/issueOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W

const key = panelKey(identity.owner, identity.repo, identity.number);
let panel = this._panels.get(key);
if (existingPanel && panel && panel._panel !== existingPanel) {
panel.dispose();
panel = undefined;
}
const activeColumn = IssueOverviewPanel._getViewColumn(toTheSide, panel);

if (panel) {
Expand Down
60 changes: 8 additions & 52 deletions src/github/overviewRestorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,72 +6,44 @@
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';
import { RepositoriesManager } from './repositoriesManager';
import { PullRequest } from './views';
import { Disposable } from '../common/lifecycle';
import Logger from '../common/logger';
import { ITelemetry } from '../common/telemetry';

export class OverviewRestorer extends Disposable implements vscode.WebviewPanelSerializer {
private static ID = 'OverviewRestorer';

constructor(private readonly _repositoriesManager: RepositoriesManager,
private readonly _telemetry: ITelemetry,
constructor(private readonly _telemetry: ITelemetry,
private readonly _context: vscode.ExtensionContext,
private readonly _credentialStore: CredentialStore,
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
) {
super();
this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this));
this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this));
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, folderRepositoryManagerResolver, _telemetry));
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, _folderRepositoryManagerResolver, _telemetry));
}

async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise<void> {
await this.waitForAuth();
await this.waitForAnyGitHubRepos(this._repositoriesManager);

if (!state || !state.number || this._repositoriesManager.folderManagers.length === 0) {
if (!state || !state.number) {
webviewPanel.dispose();
return;
}

let repo: GitHubRepository | undefined;
let folderManager: FolderRepositoryManager | undefined;
for (const manager of this._repositoriesManager.folderManagers) {
const githubRepository = manager.findExistingGitHubRepository({ owner: state.owner, repositoryName: state.repo });
if (githubRepository) {
repo = githubRepository;
folderManager = manager;
break;
}
}

if (!repo || !folderManager) {
folderManager = this._repositoriesManager.folderManagers[0];
repo = await folderManager.createGitHubRepositoryFromOwnerName(state.owner, state.repo);
}

if (!repo || !folderManager) {
webviewPanel.dispose();
return;
}
await this.waitForAuth();

const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(state.owner, state.repo);
const identity = { owner: state.owner, repo: state.repo, number: state.number };
if (state.isIssue) {
const issueModel = await repo.getIssue(state.number, true);
const issueModel = await folderManager.resolveIssue(state.owner, state.repo, state.number, true, true);
if (!issueModel) {
webviewPanel.dispose();
return;
}
return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, identity, issueModel, undefined, true, webviewPanel);
} else {
const pullRequestModel = await repo.getPullRequest(state.number, 'OverviewRestorer.deserializeWebviewPanel', true);
const pullRequestModel = await folderManager.resolvePullRequest(state.owner, state.repo, state.number, true);
if (!pullRequestModel) {
webviewPanel.dispose();
return;
Expand All @@ -86,20 +58,4 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
}
return new Promise(resolve => this._credentialStore.onDidGetSession(() => resolve()));
}

protected async waitForAnyGitHubRepos(reposManager: RepositoriesManager): Promise<void> {
// Check if any folder manager already has GitHub repositories
if (reposManager.folderManagers.some(manager => manager.gitHubRepositories.length > 0)) {
return;
}

Logger.appendLine('Waiting for GitHub repositories.', OverviewRestorer.ID);
return new Promise(resolve => {
const disposable = reposManager.onDidChangeAnyGitHubRepository(() => {
Logger.appendLine('Found GitHub repositories.', OverviewRestorer.ID);
disposable.dispose();
resolve();
});
});
}
}
4 changes: 4 additions & 0 deletions src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode

const key = panelKey(identity.owner, identity.repo, identity.number);
let panel = this._panels.get(key);
if (existingPanel && panel && panel._panel !== existingPanel) {
panel.dispose();
panel = undefined;
}

const activeColumn = IssueOverviewPanel._getViewColumn(toTheSide, panel);

Expand Down
70 changes: 70 additions & 0 deletions src/test/github/overviewRestorer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* 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 { CredentialStore } from '../../github/credentials';
import { FolderRepositoryManagerResolver } from '../../github/folderRepositoryManagerResolver';
import { OverviewRestorer } from '../../github/overviewRestorer';
import { PullRequestModel } from '../../github/pullRequestModel';
import { PullRequestOverviewPanel } from '../../github/pullRequestOverview';
import { RepositoriesManager } from '../../github/repositoriesManager';
import { PullRequest } from '../../github/views';
import { MockExtensionContext } from '../mocks/mockExtensionContext';
import { MockTelemetry } from '../mocks/mockTelemetry';

describe('OverviewRestorer', function () {
let context: MockExtensionContext;
let credentialStore: CredentialStore;
let folderRepositoryManagerResolver: FolderRepositoryManagerResolver;
let repositoriesManager: RepositoriesManager;
let restorer: OverviewRestorer;
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);
folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry);
sandbox.stub(vscode.window, 'registerWebviewPanelSerializer').returns({ dispose() { } });
sandbox.stub(vscode.window, 'registerExternalUriOpener').returns({ dispose() { } });
sandbox.stub(credentialStore, 'isAnyAuthenticated').returns(true);
restorer = new OverviewRestorer(telemetry, context, credentialStore, folderRepositoryManagerResolver);
});

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

it('restores a pull request with a remote-only manager', async function () {
const folderManager = folderRepositoryManagerResolver.getManagerForRepository('microsoft', 'vscode-pull-request-github');
const pullRequest = {} as PullRequestModel;
sandbox.stub(folderManager, 'resolvePullRequest').resolves(pullRequest);
const createOrShow = sandbox.stub(PullRequestOverviewPanel, 'createOrShow').resolves();
const webviewPanel = { dispose: sandbox.spy() } as unknown as vscode.WebviewPanel;
const state = {
owner: 'microsoft',
repo: 'vscode-pull-request-github',
number: 8904,
isIssue: false,
} as PullRequest;

await restorer.deserializeWebviewPanel(webviewPanel, state);

assert.strictEqual(createOrShow.callCount, 1);
assert.strictEqual(createOrShow.firstCall.args[2], folderManager);
assert.strictEqual(createOrShow.firstCall.args[4], pullRequest);
assert.strictEqual(createOrShow.firstCall.args[7], webviewPanel);
});
});
21 changes: 21 additions & 0 deletions src/test/github/pullRequestOverview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ describe('PullRequestOverview', function () {
assert.notStrictEqual(PullRequestOverviewPanel.findPanel('aaa', 'bbb', 1000), undefined);
});

it('uses a serializer-provided panel when no panel is cached', async function () {
const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo);
const prModel = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem);
const identity = { owner: prModel.remote.owner, repo: prModel.remote.repositoryName, number: prModel.number };
const restoredWebviewPanel = vscode.window.createWebviewPanel(PullRequestOverviewPanel.viewType, '#1000', vscode.ViewColumn.One, {});

await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity, prModel, false, true, restoredWebviewPanel);

const restoredPanel = PullRequestOverviewPanel.findPanel(identity.owner, identity.repo, identity.number);
assert.strictEqual((restoredPanel as any)._panel, restoredWebviewPanel);
restoredWebviewPanel.dispose();
});

it('builds the active PR URL before the PR has loaded', async function () {
repo.addGraphQLPullRequest(builder => {
builder.pullRequest(response => {
Expand Down Expand Up @@ -160,6 +173,14 @@ describe('PullRequestOverview', function () {

assert.strictEqual(panel0, PullRequestOverviewPanel.findPanel(identity0.owner, identity0.repo, identity0.number));
assert.strictEqual(createWebviewPanel.callCount, 1);

const restoredWebviewPanel = vscode.window.createWebviewPanel(PullRequestOverviewPanel.viewType, '#1000', vscode.ViewColumn.One, {});
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, identity0, prModel0, false, true, restoredWebviewPanel);

const restoredPanel = PullRequestOverviewPanel.findPanel(identity0.owner, identity0.repo, identity0.number);
assert.notStrictEqual(restoredPanel, panel0);
assert.strictEqual((restoredPanel as any)._panel, restoredWebviewPanel);
restoredWebviewPanel.dispose();
});

it('coalesces an update requested during initialization', async function () {
Expand Down
25 changes: 25 additions & 0 deletions src/test/uriHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import * as vscode from 'vscode';
import { GitApiImpl } from '../api/api1';
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
import { CredentialStore } from '../github/credentials';
import { FolderRepositoryManager } from '../github/folderRepositoryManager';
import { FolderRepositoryManagerResolver } from '../github/folderRepositoryManagerResolver';
import { PullRequestOverviewPanel } from '../github/pullRequestOverview';
import { RepositoriesManager } from '../github/repositoriesManager';
import { UriHandler } from '../uriHandler';
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
import { ReviewsManager } from '../view/reviewsManager';
import { ThemeData } from '../view/theme';
import { MockExtensionContext } from './mocks/mockExtensionContext';
import { MockTelemetry } from './mocks/mockTelemetry';

Expand Down Expand Up @@ -63,4 +66,26 @@ describe('UriHandler', function () {
assert.ok(createOrShow.firstCall.args[2].repository instanceof RemoteOnlyRepository);
assert.strictEqual(repositoriesManager.folderManagers.length, 0);
});

it('uses a remote-only manager when no workspace manager matches the repository', function () {
const repository = new RemoteOnlyRepository();
const onDidChangeTheme = new vscode.EventEmitter<ThemeData | undefined>();
context.subscriptions.push(onDidChangeTheme);
const existingManager = new FolderRepositoryManager(
0,
context,
repository,
telemetry,
git,
credentialStore,
new CreatePullRequestHelper(),
{ onDidChangeTheme: onDidChangeTheme.event, themeData: undefined },
);
repositoriesManager.insertFolderManager(existingManager);

const resolvedManager = folderRepositoryManagerResolver.getManagerForRepository('microsoft', 'vscode');

assert.notStrictEqual(resolvedManager, existingManager);
assert.ok(resolvedManager.repository instanceof RemoteOnlyRepository);
});
});