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
62 changes: 29 additions & 33 deletions src/github/externalUriOpener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode';
import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver';
import { IssueModel } from './issueModel';
import { IssueOverviewPanel } from './issueOverview';
import { PullRequestOverviewPanel } from './pullRequestOverview';
import { getGitHubIssueOrPullRequestUriOpenerPriority, openWithDefaultExternalOpener, parseGitHubIssueOrPullRequestUri } from '../common/externalUri';
Expand Down Expand Up @@ -45,48 +46,43 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs
}

const folderRepositoryManager = this._folderRepositoryManagerResolver.getManagerForRepository(identity.owner, identity.repo);
if (identity.kind === 'pullRequest') {
const pullRequest = folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true, 'overview').then(async (pullRequest) => {
if (token.isCancellationRequested) {
throw new vscode.CancellationError();
}
if (!pullRequest) {
await openWithDefaultExternalOpener(openContext.sourceUri);
throw new vscode.CancellationError();
}
return pullRequest;
});
// Start the webview while the first repository and PR requests are in flight.
try {
const requireModel = async <T extends IssueModel>(model: T | undefined): Promise<T> => {
if (token.isCancellationRequested) {
throw new vscode.CancellationError();
}
if (!model) {
await openWithDefaultExternalOpener(openContext.sourceUri);
throw new vscode.CancellationError();
}
return model;
};

try {
if (identity.kind === 'pullRequest') {
const pullRequest = folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true, 'overview').then(requireModel);
await PullRequestOverviewPanel.createOrShow(
this._telemetry,
this._context.extensionUri,
folderRepositoryManager,
identity,
pullRequest,
);
} catch (error) {
if (!(error instanceof vscode.CancellationError)) {
Logger.error(`Failed to open pull request: ${formatError(error)}`, 'GitHubIssueOrPullRequestExternalUriOpener');
await vscode.window.showErrorMessage(formatError(error));
}
}
} else {
const issue = await folderRepositoryManager.resolveIssue(identity.owner, identity.repo, identity.number, true, true);
if (token.isCancellationRequested) {
return;
} else {
const issue = folderRepositoryManager.resolveIssue(identity.owner, identity.repo, identity.number, true, true).then(requireModel);
await IssueOverviewPanel.createOrShow(
this._telemetry,
this._context.extensionUri,
folderRepositoryManager,
identity,
issue,
);
}
if (!issue) {
await openWithDefaultExternalOpener(openContext.sourceUri);
return;
} catch (error) {
if (!(error instanceof vscode.CancellationError)) {
const itemType = identity.kind === 'pullRequest' ? 'pull request' : 'issue';
Logger.error(`Failed to open ${itemType}: ${formatError(error)}`, 'GitHubIssueOrPullRequestExternalUriOpener');
await vscode.window.showErrorMessage(formatError(error));
}
await IssueOverviewPanel.createOrShow(
this._telemetry,
this._context.extensionUri,
folderRepositoryManager,
identity,
issue,
);
}
}

Expand Down
25 changes: 24 additions & 1 deletion src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ import {
parseMilestone,
restPaginate,
} from './utils';
import { PullRequestPreview } from './views';
import { IssuePreview, PullRequestPreview } from './views';
import { StackCandidate } from '../../common/views';
import { AuthenticationError, AuthProvider, GitHubServerType, isSamlError } from '../common/authentication';

Expand Down Expand Up @@ -1497,6 +1497,29 @@ export class GitHubRepository extends Disposable {
};
}

async getIssuePreview(number: number): Promise<IssuePreview> {
if (!Number.isSafeInteger(number) || number <= 0) {
throw new Error(`Invalid issue number: ${number}`);
}
const { query, remote, schema } = await this.ensure();
type PreviewData = Omit<IssuePreview, 'author' | 'isIssue'> & {
author: GraphQLAccount | null;
};
const { data } = await query<{ repository: { issue: PreviewData | null } | null }>({
query: schema.IssuePreview,
variables: { owner: remote.owner, name: remote.repositoryName, number },
});
if (!data.repository?.issue) {
throw new Error(`Unable to load issue preview for ${remote.owner}/${remote.repositoryName}#${number}`);
}
const { author, ...preview } = data.repository.issue;
return {
...preview,
author: parseAccount(author, this),
isIssue: true,
};
}

async getPullRequest(id: number, callerName: string, useCache: boolean = false, silent: boolean = false, loadMode: 'default' | 'overview' = 'default'): Promise<PullRequestModel | undefined> {
if (useCache && this._pullRequestModelsByNumber.has(id)) {
Logger.debug(`Using cached pull request model for ${id}`, this.id);
Expand Down
117 changes: 87 additions & 30 deletions src/github/issueOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { CloseResult, OpenLocalFileArgs } from '../../common/views';
import { openItemOnGitHub } from '../commands';
import { decodeBase64, guessExtensionFromMime, pickFilesForUpload, placeholdersForNames, runFileUploads, runPendingUploads } from './fileUpload';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GitHubRepository } from './githubRepository';
import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface';
import { IssueModel } from './issueModel';
import { openIssueOrPullRequestOnGitHub } from './openOnGitHub';
import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks';
import { isInCodespaces, processPermalinks, vscodeDevPrLink } from './utils';
import { ChangeAssigneesReply, DisplayLabel, FileUploadCompletedMessage, Issue, ProjectItemsReply, SubmitReviewArgs, SubmitReviewReply, UnresolvedIdentity, UploadFilesReply, UploadPastedFilesArgs } from './views';
import { ChangeAssigneesReply, DisplayLabel, FileUploadCompletedMessage, Issue, IssuePreview, OverviewItemPreview, ProjectItemsReply, SubmitReviewArgs, SubmitReviewReply, UnresolvedIdentity, UploadFilesReply, UploadPastedFilesArgs } from './views';
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
import { emojify, ensureEmojis } from '../common/emoji';
import Logger from '../common/logger';
Expand Down Expand Up @@ -44,6 +45,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
protected _folderRepositoryManager: FolderRepositoryManager;
protected _scrollPosition = { x: 0, y: 0 };
private _identityUpdateSequence = 0;
protected readonly previewLog = { label: 'Issue', id: IssueOverviewPanel.ID };

protected static _getViewColumn(toTheSide: boolean, panel?: IssueOverviewPanel): number | undefined {
const tabViewColumn = vscode.window.tabGroups.activeTabGroup.viewColumn;
Expand All @@ -63,8 +65,6 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
_preserveFocus: boolean = true,
existingPanel?: vscode.WebviewPanel
) {
await ensureEmojis(folderRepositoryManager.context);

const key = panelKey(identity.owner, identity.repo, identity.number);
let panel = this._panels.get(key);
if (existingPanel && panel && panel._panel !== existingPanel) {
Expand Down Expand Up @@ -324,6 +324,26 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W

protected async updateItem(issueModel: TItem): Promise<void> {
try {
if (!this._item) {
this._postMessage({
Comment thread
alexr00 marked this conversation as resolved.
command: 'pr.preview',
pullrequest: {
number: issueModel.number,
title: issueModel.title,
titleHTML: issueModel.titleHTML,
url: issueModel.html_url,
body: issueModel.body,
bodyHTML: issueModel.bodyHTML,
author: issueModel.author,
createdAt: issueModel.createdAt,
state: issueModel.state,
stateReason: issueModel.stateReason,
isIssue: true,
} satisfies IssuePreview,
});
Logger.debug('Issue overview preview loaded from model', IssueOverviewPanel.ID);
}

const [
issue,
timelineEvents,
Expand All @@ -342,6 +362,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
issueModel.canEdit(),
this._folderRepositoryManager.getAssignableUsers(),
this._folderRepositoryManager.getCurrentUser(),
ensureEmojis(this._folderRepositoryManager.context),
]);

if (!issue) {
Expand Down Expand Up @@ -390,12 +411,18 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
return 'issue';
}

protected getPreview(repository: GitHubRepository, number: number): Promise<OverviewItemPreview> {
return repository.getIssuePreview(number);
}

/**
* Update the panel with an unresolved identity and optional model.
* If no model is provided, it will be resolved from the identity.
*/
public async updateWithIdentity(foldersManager: FolderRepositoryManager, identity: UnresolvedIdentity, issueModel?: TItem | Promise<TItem | undefined>, progressLocation?: string): Promise<void> {
const updateSequence = ++this._identityUpdateSequence;
let loading = true;
const isLoading = () => loading && !this.isDisposed && updateSequence === this._identityUpdateSequence;
this._identity = identity;
this._folderRepositoryManager = foldersManager;

Expand All @@ -411,39 +438,69 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
}
}

if (issueModel instanceof Promise) {
try {
issueModel = await issueModel;
} catch (error) {
if (updateSequence === this._identityUpdateSequence && !this._item) {
this.dispose();
if (isLoading() && (!issueModel || issueModel instanceof Promise)) {
void (async () => {
try {
const start = Date.now();
const repository = await foldersManager.createGitHubRepositoryFromOwnerName(identity.owner, identity.repo, false);
if (!isLoading()) {
return;
}
if (!repository) {
throw new Error(`Unable to find repository for ${identity.owner}/${identity.repo}`);
}
const preview = await this.getPreview(repository, identity.number);
if (isLoading()) {
await this._postMessage({ command: 'pr.preview', pullrequest: preview });
Comment thread
alexr00 marked this conversation as resolved.
Logger.debug(`${this.previewLog.label} overview preview loaded in ${Date.now() - start}ms`, this.previewLog.id);
}
} catch (error) {
Logger.error(`Unable to load ${this.previewLog.label} overview preview: ${formatError(error)}`, this.previewLog.id);
}
throw error;
}
}
if (this.isDisposed || updateSequence !== this._identityUpdateSequence) {
return;
})();
}

// If no model provided, resolve it from the identity
if (!issueModel) {
const resolvedModel = await this.resolveModel(identity);
if (!resolvedModel) {
throw new Error(
`Failed to resolve ${this.getItemTypeName()} #${identity.number} in ${identity.owner}/${identity.repo}`,
);
try {
if (issueModel instanceof Promise) {
try {
issueModel = await issueModel;
} catch (error) {
if (updateSequence === this._identityUpdateSequence && !this._item) {
this.dispose();
}
throw error;
}
}
if (this.isDisposed || updateSequence !== this._identityUpdateSequence) {
return;
}
issueModel = resolvedModel;
}

if (this.isDisposed || updateSequence !== this._identityUpdateSequence) {
return;
}
// If no model provided, resolve it from the identity
if (!issueModel) {
const resolvedModel = await this.resolveModel(identity);
if (!resolvedModel) {
throw new Error(
`Failed to resolve ${this.getItemTypeName()} #${identity.number} in ${identity.owner}/${identity.repo}`,
);
}
issueModel = resolvedModel;
}

if (progressLocation) {
return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel!));
} else {
return this.updateItem(issueModel);
if (this.isDisposed || updateSequence !== this._identityUpdateSequence) {
return;
}

if (issueModel instanceof Promise) {
throw new Error(`Failed to resolve ${this.getItemTypeName()} #${identity.number} in ${identity.owner}/${identity.repo}`);
}
const resolvedIssueModel = issueModel;
if (progressLocation) {
return await vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(resolvedIssueModel));
} else {
return await this.updateItem(resolvedIssueModel);
}
} finally {
loading = false;
}
}

Expand Down
37 changes: 8 additions & 29 deletions src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { addAttestationCommit, isAttestationCommitsEnabled } from './attestation
import { getCopilotApi } from './copilotApi';
import { SessionIdForPr } from './copilotRemoteAgent';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GitHubRepository } from './githubRepository';
import {
getUpdatableStackEntries,
GithubItemStateEnum,
Expand All @@ -33,7 +34,7 @@ import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
import type { RepositoriesManager } from './repositoriesManager';
import { supportsStackGitOperations, updateStackBranches } from './updateStackBranches';
import { getIssueOrURLExpression, parseIssueExpressionOutput, parseReviewers, processDiffLinks, processPermalinks } from './utils';
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, SubmitReviewArgs, UnresolvedIdentity, UnstackAllResult } from './views';
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, PullRequestPreview, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, SubmitReviewArgs, UnresolvedIdentity, UnstackAllResult } from './views';
import { debounce } from '../common/async';
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
import { COPILOT_REVIEWER, COPILOT_REVIEWER_ACCOUNT, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
Expand All @@ -56,6 +57,7 @@ import { getGitHubCommitFileSystemProvider } from '../view/githubFileContentProv
export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestModel> {
public static override ID: string = 'PullRequestOverviewPanel';
public static override readonly viewType = PULL_REQUEST_OVERVIEW_VIEW_TYPE;
protected override readonly previewLog = { label: 'PR', id: PullRequestOverviewPanel.ID };

/**
* All open PR panels, keyed by "owner/repo#number".
Expand All @@ -80,7 +82,6 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
private _refreshing = false;
private _updateItemPromise: Promise<void> | undefined;
private _updateSequence = 0;
private _previewSequence = 0;
private _stackLoaded = false;
private _stackPullRequestNumbers = new Set<number>();
private _stackRefreshPending = false;
Expand Down Expand Up @@ -836,39 +837,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
return 'Pull Request';
}

protected override getPreview(repository: GitHubRepository, number: number): Promise<PullRequestPreview> {
return repository.getPullRequestPreview(number);
}

public override async updateWithIdentity(
folderRepositoryManager: FolderRepositoryManager,
identity: UnresolvedIdentity,
pullRequestModel?: PullRequestModel | Promise<PullRequestModel>,
progressLocation?: string
): Promise<void> {
const previewSequence = ++this._previewSequence;
let loading = true;
const isLoading = () => loading && !this.isDisposed && previewSequence === this._previewSequence;
const update = super.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, progressLocation);
if (isLoading() && (!pullRequestModel || pullRequestModel instanceof Promise)) {
void (async () => {
try {
const start = Date.now();
const repository = await folderRepositoryManager.createGitHubRepositoryFromOwnerName(identity.owner, identity.repo, false);
if (!repository || !isLoading()) {
return;
}
const preview = await repository.getPullRequestPreview(identity.number);
if (isLoading()) {
await this._postMessage({ command: 'pr.preview', pullrequest: preview });
Logger.debug(`PR overview preview loaded in ${Date.now() - start}ms`, PullRequestOverviewPanel.ID);
}
} catch (error) {
Logger.error(`Unable to load PR overview preview: ${formatError(error)}`, PullRequestOverviewPanel.ID);
}
})();
}
try {
await update;
} finally {
loading = false;
}
await super.updateWithIdentity(folderRepositoryManager, identity, pullRequestModel, progressLocation);

// Notify that this PR overview is now active
if (!this.isDisposed && this._item) {
Expand Down
Loading
Loading