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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
],
"activationEvents": [
"onStartupFinished",
"onOpenExternalUri:http",
"onOpenExternalUri:https",
"onFileSystem:newIssue",
"onFileSystem:pr",
"onFileSystem:githubpr",
Expand Down
3 changes: 2 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Repository } from './api/api';
import { GitErrorCodes } from './api/api1';
import { CommentReply, findActiveHandler, resolveCommentHandler } from './commentHandlerResolver';
import { commands } from './common/executeCommands';
import { openWithDefaultExternalOpener } from './common/externalUri';
import Logger from './common/logger';
import { FILE_LIST_LAYOUT, HIDE_VIEWED_FILES, PR_SETTINGS_NAMESPACE } from './common/settingKeys';
import { editQuery } from './common/settingsUtils';
Expand Down Expand Up @@ -116,7 +117,7 @@ export async function openPullRequestOnGitHub(e: PRNode | RepositoryChangesNode
}

function openPullRequestUrlOnGitHub(url: vscode.Uri, telemetry: ITelemetry): void {
vscode.commands.executeCommand('vscode.open', url);
openWithDefaultExternalOpener(url);

/** __GDPR__
"pr.openInGitHub" : {}
Expand Down
51 changes: 51 additions & 0 deletions src/common/externalUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* 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';

export interface GitHubIssueOrPullRequestUri {
kind: 'issue' | 'pullRequest';
owner: string;
repo: string;
number: number;
}

const GITHUB_PATH_PART = /^[A-Za-z0-9_.-]+$/;

export function parseGitHubIssueOrPullRequestUri(uri: vscode.Uri): GitHubIssueOrPullRequestUri | undefined {
if ((uri.scheme !== 'http' && uri.scheme !== 'https') || uri.authority.toLowerCase() !== 'github.com') {
return undefined;
}

const pathParts = uri.path.split('/').filter(Boolean);
const kind = pathParts[2] === 'pull'
? 'pullRequest'
: pathParts[2] === 'issues'
? 'issue'
: undefined;
if (pathParts.length < 4 || !kind || !GITHUB_PATH_PART.test(pathParts[0]) || !GITHUB_PATH_PART.test(pathParts[1])) {
return undefined;
}

if (!/^[1-9][0-9]*$/.test(pathParts[3])) {
return undefined;
}

const number = Number(pathParts[3]);
if (!Number.isSafeInteger(number)) {
return undefined;
}

return {
kind,
owner: pathParts[0],
repo: pathParts[1],
number,
};
}

export function openWithDefaultExternalOpener(uri: vscode.Uri): Thenable<boolean> {
return vscode.env.openExternal(uri, { allowContributedOpeners: 'default' });
}
77 changes: 77 additions & 0 deletions src/github/externalUriOpener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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 { IssueOverviewPanel } from './issueOverview';
import { PullRequestOverviewPanel } from './pullRequestOverview';
import { RepositoriesManager } from './repositoriesManager';
import { parseGitHubIssueOrPullRequestUri } from '../common/externalUri';
import { ITelemetry } from '../common/telemetry';
import { EXTENSION_ID } from '../constants';

export function registerGitHubIssueOrPullRequestExternalUriOpener(
extensionUri: vscode.Uri,
repositoriesManager: RepositoriesManager,
telemetry: ITelemetry,
): vscode.Disposable {
return vscode.window.registerExternalUriOpener(`${EXTENSION_ID}.issueOrPullRequest`, {
canOpenExternalUri(uri) {
if (!parseGitHubIssueOrPullRequestUri(uri)) {
return vscode.ExternalUriOpenerPriority.None;
}
return vscode.ExternalUriOpenerPriority.Preferred;
},
Comment thread
alexr00 marked this conversation as resolved.
async openExternalUri(_resolvedUri, openContext, token) {
const identity = parseGitHubIssueOrPullRequestUri(openContext.sourceUri);
if (!identity || token.isCancellationRequested) {
return;
}

const folderRepositoryManager = repositoriesManager.getManagerForRepository(identity.owner, identity.repo)
?? repositoriesManager.folderManagers[0];
if (!folderRepositoryManager) {
await vscode.window.showErrorMessage(vscode.l10n.t('Unable to open issue or pull request #{0}: no GitHub repository is available.', identity.number));
return;
}

if (identity.kind === 'pullRequest') {
const pullRequest = await folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true);
if (token.isCancellationRequested) {
return;
}
if (!pullRequest) {
await vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0} in {1}/{2}.', identity.number, identity.owner, identity.repo));
return;
}
await PullRequestOverviewPanel.createOrShow(
telemetry,
extensionUri,
folderRepositoryManager,
identity,
pullRequest,
);
} else {
const issue = await folderRepositoryManager.resolveIssue(identity.owner, identity.repo, identity.number, true, true);
if (token.isCancellationRequested) {
return;
}
if (!issue) {
await vscode.window.showErrorMessage(vscode.l10n.t('Unable to find issue #{0} in {1}/{2}.', identity.number, identity.owner, identity.repo));
return;
}
await IssueOverviewPanel.createOrShow(
telemetry,
extensionUri,
folderRepositoryManager,
identity,
issue,
);
}
},
}, {
schemes: ['http', 'https'],
label: vscode.l10n.t('Open GitHub Issue or Pull Request'),
});
}
2 changes: 2 additions & 0 deletions src/github/overviewRestorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode';
import { CredentialStore } from './credentials';
import { registerGitHubIssueOrPullRequestExternalUriOpener } from './externalUriOpener';
import { FolderRepositoryManager } from './folderRepositoryManager';
import { GitHubRepository } from './githubRepository';
import { IssueOverviewPanel } from './issueOverview';
Expand All @@ -26,6 +27,7 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
super();
this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this));
this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this));
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_extensionUri, _repositoriesManager, _telemetry));
}

async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise<void> {
Expand Down
3 changes: 2 additions & 1 deletion src/github/pullRequestModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import { Repository } from '../api/api';
import { COPILOT_ACCOUNTS, DiffSide, IComment, IReviewThread, SubjectType, ViewedState } from '../common/comment';
import { getGitChangeType, getModifiedContentFromDiffHunk, parseDiff } from '../common/diffHunk';
import { commands } from '../common/executeCommands';
import { openWithDefaultExternalOpener } from '../common/externalUri';
import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file';
import { GitHubRef } from '../common/githubRef';
import Logger from '../common/logger';
Expand Down Expand Up @@ -318,7 +319,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
const openString = vscode.l10n.t('Open on GitHub');
vscode.window.showWarningMessage(message, openString).then(action => {
if (action && action === openString) {
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(this.html_url));
openWithDefaultExternalOpener(vscode.Uri.parse(this.html_url));
}
});

Expand Down
3 changes: 2 additions & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ 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';
import { commands, contexts } from '../common/executeCommands';
import { openWithDefaultExternalOpener } from '../common/externalUri';
import { disposeAll } from '../common/lifecycle';
import Logger from '../common/logger';
import { CHECKOUT_DEFAULT_BRANCH, CHECKOUT_PULL_REQUEST_BASE_BRANCH, DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, POST_DONE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
Expand Down Expand Up @@ -748,7 +749,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
}

// Fallback to opening external URL
await vscode.env.openExternal(vscode.Uri.parse(message.args.href));
await openWithDefaultExternalOpener(vscode.Uri.parse(message.args.href));
}

private async openSessionLog(message: IRequestMessage<{ link: SessionLinkInfo }>): Promise<void> {
Expand Down
3 changes: 0 additions & 3 deletions src/lm/skills/summarize-github-issue-pr-notification/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ To use this skill, provide a JSON representation of a GitHub issue. The skill wi

- Do not output code. When you try to summarize PR changes, summarize in a textual format.
- Output references to other issues and PRs as Markdown links.
- If a comment references for example issue or PR #123, then output either of the following in the summary depending on if it is an issue or a PR:
- [#123](https://github.com/${owner}/${repo}/issues/123)
- [#123](https://github.com/${owner}/${repo}/pull/123)
- Comments should be summarized with the author first. Ex:
- @username: This is a comment that summarizes the main point of the comment.
- If the content contains images in Markdown format (e.g., ![alt text](image-url)), always preserve them in the output exactly as they appear. Images are important visual content and should not be removed or summarized.
Expand Down
72 changes: 72 additions & 0 deletions src/test/common/externalUri.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*---------------------------------------------------------------------------------------------
* 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 * as vscode from 'vscode';
import { parseGitHubIssueOrPullRequestUri } from '../../common/externalUri';

describe('externalUri', () => {
describe('parseGitHubIssueOrPullRequestUri', () => {
it('parses a GitHub pull request URL', () => {
const result = parseGitHubIssueOrPullRequestUri(vscode.Uri.parse('https://github.com/microsoft/vscode/pull/123'));

assert.deepStrictEqual(result, {
kind: 'pullRequest',
owner: 'microsoft',
repo: 'vscode',
number: 123,
});
});

it('parses pull request subpages, queries, and fragments', () => {
const result = parseGitHubIssueOrPullRequestUri(vscode.Uri.parse('https://github.com/microsoft/vscode/pull/123/files?diff=split#discussion_r1'));

assert.deepStrictEqual(result, {
kind: 'pullRequest',
owner: 'microsoft',
repo: 'vscode',
number: 123,
});
});

it('parses a GitHub issue URL', () => {
const result = parseGitHubIssueOrPullRequestUri(vscode.Uri.parse('https://github.com/microsoft/vscode/issues/456#issuecomment-1'));

assert.deepStrictEqual(result, {
kind: 'issue',
owner: 'microsoft',
repo: 'vscode',
number: 456,
});
});

it('supports HTTP and case-insensitive GitHub hosts', () => {
const result = parseGitHubIssueOrPullRequestUri(vscode.Uri.parse('http://GitHub.com/owner/repo_name/pull/1'));

assert.deepStrictEqual(result, {
kind: 'pullRequest',
owner: 'owner',
repo: 'repo_name',
number: 1,
});
});

for (const url of [
'https://example.com/microsoft/vscode/pull/123',
'https://github.com:443/microsoft/vscode/pull/123',
'https://github.com/microsoft/vscode/issue/123',
'https://github.com/microsoft/vscode/pulls/123',
'https://github.com/microsoft/vscode/pull/0',
'https://github.com/microsoft/vscode/pull/not-a-number',
'https://github.com/microsoft/vscode/pull/999999999999999999999',
'https://github.com/microsoft/vscode/pull/123.diff',
'https://github.com/microsoft/vscode/pull',
]) {
it(`does not parse ${url}`, () => {
assert.strictEqual(parseGitHubIssueOrPullRequestUri(vscode.Uri.parse(url)), undefined);
});
}
});
});