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
8 changes: 7 additions & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { PullRequestModel } from './github/pullRequestModel';
import { PullRequestOverviewPanel } from './github/pullRequestOverview';
import { chooseItem } from './github/quickPicks';
import { RepositoriesManager } from './github/repositoriesManager';
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils';
import { codespacesPrLink, getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink, vscodeDevPrLinkFromUrl } from './github/utils';
import { BaseContext, OverviewContext } from './github/views';
import { checkoutPRInWorktree } from './github/worktree';
import { IssueChatContextItem } from './lm/issueContextProvider';
Expand Down Expand Up @@ -1915,6 +1915,9 @@ ${contents}

context.subscriptions.push(
vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async (params: BaseContext | undefined) => {
if (params?.url) {
return vscode.env.clipboard.writeText(vscodeDevPrLinkFromUrl(params.url));
}
let pr: PullRequestModel | undefined;
if (params) {
pr = await reposManager.getManagerForRepository(params.owner, params.repo)?.resolvePullRequest(params.owner, params.repo, params.number, true);
Expand All @@ -1935,6 +1938,9 @@ ${contents}

context.subscriptions.push(
vscode.commands.registerCommand('pr.copyPrLink', async (params: BaseContext | undefined) => {
if (params?.url) {
return vscode.env.clipboard.writeText(params.url);
}
let item: PullRequestModel | IssueModel | undefined;
if (params) {
const folderManager = reposManager.getManagerForRepository(params.owner, params.repo);
Expand Down
4 changes: 2 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { CompareChanges } from './view/compareChangesTreeDataProvider';
import { CreatePullRequestHelper } from './view/createPullRequestHelper';
import { EmojiCompletionProvider } from './view/emojiCompletionProvider';
import { FileTypeDecorationProvider } from './view/fileTypeDecorationProvider';
import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider';
import { getGitHubCommitFileSystemProvider } from './view/githubFileContentProvider';
import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider';
import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider';
import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';
Expand Down Expand Up @@ -495,7 +495,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
const readOnlyMessage = new vscode.MarkdownString(vscode.l10n.t('Cannot edit this pull request file. [Check out](command:pr.checkoutFromReadonlyFile) this pull request to edit.'));
readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] };
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage }));
const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore);
const githubFilesystemProvider = getGitHubCommitFileSystemProvider({ reposManager, gitAPI: apiImpl, credentialStore })!;
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) }));
context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(Schemes.CheckRunLog, new CheckRunLogContentProvider(reposManager)));

Expand Down
9 changes: 8 additions & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { toOpenIssueWebviewUri } from '../common/uri';
import { asPromise, formatError } from '../common/utils';
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
import { toCheckRunLogUri } from '../view/checkRunLogContentProvider';
import { getGitHubCommitFileSystemProvider } from '../view/githubFileContentProvider';

export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestModel> {
public static override ID: string = 'PullRequestOverviewPanel';
Expand Down Expand Up @@ -928,11 +929,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
private async openCommitChanges(message: IRequestMessage<OpenCommitChangesArgs>): Promise<void> {
try {
const { commitSha } = message.args;
const fileSystemProvider = getGitHubCommitFileSystemProvider();
if (!fileSystemProvider) {
throw new Error('GitHub commit file system provider is not initialized.');
}
fileSystemProvider.registerGitHubRepository(this._item.githubRepository);
await PullRequestModel.openCommitChanges(this._extensionUri, this._item.githubRepository, commitSha);
this._replyMessage(message, {});
await this._replyMessage(message, {});
} catch (error) {
Logger.error(`Failed to open commit changes: ${formatError(error)}`, PullRequestOverviewPanel.ID);
vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error)));
await this._throwError(message, formatError(error));
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/github/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1974,7 +1974,11 @@ export async function findDotComAndEnterpriseRemotes(folderManagers: FolderRepos
}

export function vscodeDevPrLink(pullRequest: IssueModel) {
const itemUri = vscode.Uri.parse(pullRequest.html_url);
return vscodeDevPrLinkFromUrl(pullRequest.html_url);
}

export function vscodeDevPrLinkFromUrl(url: string) {
const itemUri = vscode.Uri.parse(url);
return `https://${vscode.env.appName.toLowerCase().includes('insider') ? 'insiders.' : ''}vscode.dev/github${itemUri.path}`;
}

Expand Down
3 changes: 2 additions & 1 deletion src/github/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export interface BaseContext {
owner: string;
repo: string;
number: number;
[key: string]: boolean | string | number;
url?: string;
[key: string]: boolean | string | number | undefined;
}

export interface OverviewContext extends BaseContext {
Expand Down
55 changes: 55 additions & 0 deletions src/test/view/githubFileContentProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*---------------------------------------------------------------------------------------------
* 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 { GitApiImpl } from '../../api/api1';
import { toGitHubCommitUri } from '../../common/uri';
import { CredentialStore } from '../../github/credentials';
import { GitHubRepository } from '../../github/githubRepository';
import { RepositoriesManager } from '../../github/repositoriesManager';
import { GitHubCommitFileSystemProvider } from '../../view/githubFileContentProvider';

describe('GitHubCommitFileSystemProvider', function () {
let sandbox: SinonSandbox;

beforeEach(function () {
sandbox = createSandbox();
});

afterEach(function () {
sandbox.restore();
});

it('reads from a registered repository without a workspace repository manager', async function () {
const credentialStore = {
isAnyAuthenticated: () => true,
} as unknown as CredentialStore;
const provider = new GitHubCommitFileSystemProvider(
{} as RepositoriesManager,
{} as GitApiImpl,
credentialStore,
);
const content = new TextEncoder().encode('content');
const getFile = sandbox.stub().resolves(content);
const repository = {
remote: {
owner: 'Owner',
repositoryName: 'Repo',
},
getFile,
} as unknown as GitHubRepository;
provider.registerGitHubRepository(repository);

const result = await provider.readFile(toGitHubCommitUri('file.ts', {
commit: 'commit',
owner: 'owner',
repo: 'repo',
}));

assert.deepStrictEqual(result, content);
assert.strictEqual(getFile.calledOnceWithExactly('/file.ts', 'commit'), true);
});
});
27 changes: 26 additions & 1 deletion src/view/githubFileContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@ import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider';
import { GitApiImpl } from '../api/api1';
import { fromGitHubCommitUri } from '../common/uri';
import { CredentialStore } from '../github/credentials';
import { GitHubRepository } from '../github/githubRepository';
import { RepositoriesManager } from '../github/repositoriesManager';

export class GitHubCommitFileSystemProvider extends RepositoryFileSystemProvider {
private readonly _gitHubRepositories = new Map<string, GitHubRepository>();

constructor(private readonly repos: RepositoriesManager, gitAPI: GitApiImpl, credentialStore: CredentialStore) {
super(gitAPI, credentialStore);
}

registerGitHubRepository(repository: GitHubRepository): void {
this._gitHubRepositories.set(this.repositoryKey(repository.remote.owner, repository.remote.repositoryName), repository);
}

override async readFile(uri: vscode.Uri): Promise<Uint8Array> {
await this.waitForAuth();
await this.waitForAnyGitHubRepos(this.repos);

const params = fromGitHubCommitUri(uri);
if (!params) {
throw new Error(`Invalid GitHub commit URI: ${uri.toString()}`);
}

const registeredRepository = this._gitHubRepositories.get(this.repositoryKey(params.owner, params.repo));
if (registeredRepository) {
return registeredRepository.getFile(uri.path, params.commit);
}

await this.waitForAnyGitHubRepos(this.repos);
const folderManager = this.repos.getManagerForRepository(params.owner, params.repo);
if (!folderManager) {
throw new Error(`Repository not found for owner: ${params.owner}, repo: ${params.repo}`);
Expand All @@ -36,4 +48,17 @@ export class GitHubCommitFileSystemProvider extends RepositoryFileSystemProvider

return githubRepo.getFile(uri.path, params.commit);
}

private repositoryKey(owner: string, repo: string): string {
return `${owner.toLowerCase()}/${repo.toLowerCase()}`;
}
}

let githubCommitFileSystemProvider: GitHubCommitFileSystemProvider | undefined;

export function getGitHubCommitFileSystemProvider(initialize?: { reposManager: RepositoriesManager, gitAPI: GitApiImpl, credentialStore: CredentialStore }): GitHubCommitFileSystemProvider | undefined {
if (!githubCommitFileSystemProvider && initialize) {
githubCommitFileSystemProvider = new GitHubCommitFileSystemProvider(initialize.reposManager, initialize.gitAPI, initialize.credentialStore);
}
return githubCommitFileSystemProvider;
}
15 changes: 12 additions & 3 deletions webviews/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ interface TitleProps {
}

function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurrentTitle, canEdit, owner, repo }: TitleProps): JSX.Element {
const { setTitle, copyPrLink } = useContext(PullRequestContext);
const { setTitle, copyPrLink, openOnGitHub } = useContext(PullRequestContext);

const titleForm = (
<form
Expand Down Expand Up @@ -116,7 +116,8 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr
'preventDefaultContextMenuItems': true,
owner,
repo,
number
number,
url,
};
context['github:copyMenu'] = true;

Expand All @@ -125,7 +126,15 @@ function Title({ title, titleHTML, number, url, inEditMode, setEditMode, setCurr
<h2>
<span dangerouslySetInnerHTML={{ __html: titleHTML }} />
{' '}
<a href={url} title={url} data-vscode-context={JSON.stringify(context)}>
<a
href={url}
title={url}
data-vscode-context={JSON.stringify(context)}
onClick={event => {
event.preventDefault();
void openOnGitHub();
}}
>
#{number}
</a>
</h2>
Expand Down
6 changes: 5 additions & 1 deletion webviews/components/stickyHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function useStickyHeader(titleRef: React.RefObject<HTMLDivElement | null>

export function StickyHeader({ pr, visible }: { pr: PullRequest; visible: boolean }): JSX.Element {
const { text, color, icon } = getStatus(pr.state, !!pr.isDraft, pr.isIssue, pr.stateReason);
const { copyPrLink } = React.useContext(PullRequestContext);
const { copyPrLink, openOnGitHub } = React.useContext(PullRequestContext);

const stickyRef = React.useCallback((node: HTMLDivElement | null) => {
if (node) {
Expand Down Expand Up @@ -64,6 +64,10 @@ export function StickyHeader({ pr, visible }: { pr: PullRequest; visible: boolea
number: pr.number,
'github:copyMenu': true,
})}
onClick={event => {
event.preventDefault();
void openOnGitHub();
}}
>
#{pr.number}
</a>
Expand Down
23 changes: 22 additions & 1 deletion webviews/editorWebview/test/overview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { default as assert } from 'assert';
import * as React from 'react';
import { cleanup, render } from 'react-testing-library';
import { cleanup, fireEvent, render } from 'react-testing-library';
import { createSandbox, SinonSandbox } from 'sinon';

import { PRContext, default as PullRequestContext } from '../../common/context';
Expand Down Expand Up @@ -38,6 +38,27 @@ describe('Overview', function () {
assert(out.container.querySelector('.overview-title'));
});

it('opens PR number links on GitHub', function () {
const pr = new PullRequestBuilder().build();
const context = new PRContext(pr);
const openOnGitHub = sinon.stub(context, 'openOnGitHub');

const out = render(
<PullRequestContext.Provider value={context}>
<Overview {...pr} />
</PullRequestContext.Provider>,
);

const numberLinks = out.container.querySelectorAll('.overview-title a, .sticky-header-number');
assert.strictEqual(numberLinks.length, 2);
numberLinks.forEach(link => {
const contextData = JSON.parse(link.getAttribute('data-vscode-context')!);
assert.strictEqual(contextData.url, pr.url);
fireEvent.click(link);
});
assert.strictEqual(openOnGitHub.callCount, 2);
});

it('applies sticky class when scrolled', function () {
const pr = new PullRequestBuilder().build();
const context = new PRContext(pr);
Expand Down