Revert button to already merged pull requests (#6097)

Fixes #2103
This commit is contained in:
Alex Ross 2024-07-19 16:12:26 +02:00 коммит произвёл GitHub
Родитель 9cd82696c4
Коммит a963d76820
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
33 изменённых файлов: 948 добавлений и 683 удалений

Просмотреть файл

@ -89,6 +89,7 @@ export interface CreatePullRequestNew {
// #region new create view
export interface CreateParamsNew {
canModifyBranches: boolean;
defaultBaseRemote?: RemoteInfo;
defaultBaseBranch?: string;
defaultCompareRemote?: RemoteInfo;

Просмотреть файл

@ -685,7 +685,7 @@
"id": "github:createPullRequestWebview",
"type": "webview",
"name": "%view.github.create.pull.request.name%",
"when": "github:createPullRequest",
"when": "github:createPullRequest || github:revertPullRequest",
"visibility": "visible",
"initialSize": 2
},
@ -706,7 +706,7 @@
{
"id": "prStatus:github",
"name": "%view.pr.status.github.name%",
"when": "github:inReviewMode && !github:createPullRequest",
"when": "github:inReviewMode && !github:createPullRequest && !github:revertPullRequest",
"icon": "$(git-pull-request)",
"visibility": "visible",
"initialSize": 3
@ -715,7 +715,7 @@
"id": "github:activePullRequest",
"type": "webview",
"name": "%view.github.active.pull.request.name%",
"when": "github:inReviewMode && github:focusedReview && !github:createPullRequest && github:activePRCount <= 1",
"when": "github:inReviewMode && github:focusedReview && !github:createPullRequest && !github:revertPullRequest && github:activePRCount <= 1",
"initialSize": 2
},
{

Просмотреть файл

@ -28,5 +28,13 @@ declare module 'vscode' {
* Worth noting that we already have this problem for the `comments` property.
*/
state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability };
readonly uri: Uri;
range: Range | undefined;
comments: readonly Comment[];
collapsibleState: CommentThreadCollapsibleState;
canReply: boolean;
contextValue?: string;
label?: string;
dispose(): void;
}
}

71
src/@types/vscode.proposed.fileComments.d.ts поставляемый
Просмотреть файл

@ -6,68 +6,27 @@
declare module 'vscode' {
export interface CommentThread2 {
/**
* The uri of the document the thread has been created on.
*/
readonly uri: Uri;
/**
* The range the comment thread is located within the document. The thread icon will be shown
* at the last line of the range.
* at the last line of the range. When set to undefined, the comment will be associated with the
* file, and not a specific range.
*/
range: Range | undefined;
}
/**
* The ranges a CommentingRangeProvider enables commenting on.
*/
export interface CommentingRanges {
/**
* Enables comments to be added to a file without a specific range.
*/
enableFileComments: boolean;
/**
* The ordered comments of the thread.
* The ranges which allow new comment threads creation.
*/
comments: readonly Comment[];
/**
* Whether the thread should be collapsed or expanded when opening the document.
* Defaults to Collapsed.
*/
collapsibleState: CommentThreadCollapsibleState;
/**
* Whether the thread supports reply.
* Defaults to true.
*/
canReply: boolean;
/**
* Context value of the comment thread. This can be used to contribute thread specific actions.
* For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title`
* using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`.
* ```json
* "contributes": {
* "menus": {
* "comments/commentThread/title": [
* {
* "command": "extension.deleteCommentThread",
* "when": "commentThread == editable"
* }
* ]
* }
* }
* ```
* This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`.
*/
contextValue?: string;
/**
* The optional human-readable label describing the {@link CommentThread Comment Thread}
*/
label?: string;
// from the commentThreadRelevance proposal
state?: CommentThreadState | { resolved?: CommentThreadState; applicability?: CommentThreadApplicability };
/**
* Dispose this comment thread.
*
* Once disposed, this comment thread will be removed from visible editors and Comment Panel when appropriate.
*/
dispose(): void;
ranges?: Range[];
}
export interface CommentController {
@ -78,6 +37,6 @@ declare module 'vscode' {
/**
* Provide a list of ranges which allow new comment threads creation or null for a given document
*/
provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult<Range[] | { enableFileComments: boolean; ranges?: Range[] }>;
provideCommentingRanges(document: TextDocument, token: CancellationToken): ProviderResult<Range[] | CommentingRanges>;
}
}

Просмотреть файл

@ -60,7 +60,6 @@ function ensurePR(folderRepoManager: FolderRepositoryManager, pr?: PRNode | Pull
}
export async function openDescription(
context: vscode.ExtensionContext,
telemetry: ITelemetry,
pullRequestModel: PullRequestModel,
descriptionNode: DescriptionNode | undefined,
@ -74,7 +73,7 @@ export async function openDescription(
descriptionNode?.reveal(descriptionNode, { select: true, focus: true });
}
// Create and show a new webview
await PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest, undefined, preserveFocus);
await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, pullRequest, undefined, preserveFocus);
if (notificationProvider?.hasNotification(pullRequest)) {
notificationProvider.markPrNotificationsAsRead(pullRequest);
@ -124,7 +123,7 @@ export function registerCommands(
reposManager: RepositoriesManager,
reviewsManager: ReviewsManager,
telemetry: ITelemetry,
tree: PullRequestsTreeDataProvider
tree: PullRequestsTreeDataProvider,
) {
context.subscriptions.push(
vscode.commands.registerCommand(
@ -812,7 +811,7 @@ export function registerCommands(
descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager);
}
await openDescription(context, telemetry, pullRequestModel, descriptionNode, folderManager, !(argument instanceof DescriptionNode), !(argument instanceof RepositoryChangesNode), tree.notificationProvider);
await openDescription(telemetry, pullRequestModel, descriptionNode, folderManager, !(argument instanceof DescriptionNode), !(argument instanceof RepositoryChangesNode), tree.notificationProvider);
},
),
);
@ -843,7 +842,7 @@ export function registerCommands(
const pullRequest = ensurePR(folderManager, pr);
descriptionNode.reveal(descriptionNode, { select: true, focus: true });
// Create and show a new webview
PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest, true);
PullRequestOverviewPanel.createOrShow(telemetry, context.extensionUri, folderManager, pullRequest, true);
/* __GDPR__
"pr.openDescriptionToTheSide" : {}

Просмотреть файл

@ -54,6 +54,7 @@ async function init(
liveshareApiPromise: Promise<LiveShare | undefined>,
showPRController: ShowPullRequest,
reposManager: RepositoriesManager,
createPrHelper: CreatePullRequestHelper
): Promise<void> {
context.subscriptions.push(Logger);
Logger.appendLine('Git repository found, initializing review manager and pr tree view.');
@ -146,8 +147,7 @@ async function init(
const activePrViewCoordinator = new WebviewViewCoordinator(context);
context.subscriptions.push(activePrViewCoordinator);
const createPrHelper = new CreatePullRequestHelper();
context.subscriptions.push(createPrHelper);
let reviewManagerIndex = 0;
const reviewManagers = reposManager.folderManagers.map(
folderManager => new ReviewManager(reviewManagerIndex++, context, folderManager.repository, folderManager, telemetry, changesTree, tree, showPRController, activePrViewCoordinator, createPrHelper, git),
@ -170,7 +170,7 @@ async function init(
Logger.appendLine(`Repo ${repo.rootUri} has already been setup.`);
return;
}
const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore);
const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore, createPrHelper);
reposManager.insertFolderManager(newFolderManager);
const newReviewManager = new ReviewManager(
reviewManagerIndex++,
@ -364,10 +364,12 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp
Logger.appendLine('Looking for git repository');
const repositories = apiImpl.repositories;
Logger.appendLine(`Found ${repositories.length} repositories during activation`);
const createPrHelper = new CreatePullRequestHelper();
context.subscriptions.push(createPrHelper);
let folderManagerIndex = 0;
const folderManagers = repositories.map(
repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore),
repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore, createPrHelper),
);
context.subscriptions.push(...folderManagers);
for (const folderManager of folderManagers) {
@ -379,7 +381,7 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp
readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] };
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage }));
await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager);
await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper);
}
export async function deactivate() {

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -33,6 +33,7 @@ import { batchPromiseAll, compareIgnoreCase, formatError, Predicate } from '../c
import { PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
import { NEVER_SHOW_PULL_NOTIFICATION, REPO_KEYS, ReposState } from '../extensionState';
import { git } from '../gitProviders/gitCommands';
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
import { OctokitCommon } from './common';
import { ConflictModel } from './conflictGuide';
import { ConflictResolutionCoordinator } from './conflictResolutionCoordinator';
@ -217,6 +218,7 @@ export class FolderRepositoryManager implements vscode.Disposable {
public readonly telemetry: ITelemetry,
private _git: GitApiImpl,
private _credentialStore: CredentialStore,
public readonly createPullRequestHelper: CreatePullRequestHelper
) {
this._subs = [];
this._githubRepositories = [];
@ -1954,6 +1956,18 @@ export class FolderRepositoryManager implements vscode.Disposable {
});
}
async revert(pullRequest: PullRequestModel, title: string, body: string, draft: boolean): Promise<PullRequestModel | undefined> {
const repo = this._githubRepositories.find(
r => r.remote.owner === pullRequest.remote.owner && r.remote.repositoryName === pullRequest.remote.repositoryName,
);
if (!repo) {
throw new Error(`No matching repository ${pullRequest.remote.repositoryName} found for ${pullRequest.remote.owner}`);
}
const pullRequestModel: PullRequestModel | undefined = await repo.revertPullRequest(pullRequest.graphNodeId, title, body, draft);
return pullRequestModel;
}
async getPullRequestRepositoryDefaultBranch(issue: IssueModel): Promise<string> {
const branch = await issue.githubRepository.getDefaultBranch();
return branch;

Просмотреть файл

@ -36,6 +36,7 @@ import {
PullRequestsResponse,
PullRequestTemplatesResponse,
RepoProjectsResponse,
RevertPullRequestResponse,
ViewerPermissionResponse,
} from './graphql';
import {
@ -180,7 +181,7 @@ export class GitHubRepository implements vscode.Disposable {
`github-browse-${this.remote.normalizedHost}-${this.remote.owner}-${this.remote.repositoryName}`,
`Pull Request (${this.remote.owner}/${this.remote.repositoryName})`,
);
this.commentsHandler = new PRCommentControllerRegistry(this.commentsController);
this.commentsHandler = new PRCommentControllerRegistry(this.commentsController, this._telemetry);
this._toDispose.push(this.commentsHandler);
this._toDispose.push(this.commentsController);
} catch (e) {
@ -986,6 +987,33 @@ export class GitHubRepository implements vscode.Disposable {
}
}
async revertPullRequest(pullRequestId: string, title: string, body: string, draft: boolean): Promise<PullRequestModel> {
try {
Logger.debug(`Revert pull request - enter`, this.id);
const { mutate, schema } = await this.ensure();
const { data } = await mutate<RevertPullRequestResponse>({
mutation: schema.RevertPullRequest,
variables: {
input: {
pullRequestId,
title,
body,
draft
}
}
});
Logger.debug(`Revert pull request - done`, this.id);
if (!data) {
throw new Error('Failed to create revert pull request.');
}
return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.revertPullRequest.revertPullRequest, this));
} catch (e) {
Logger.error(`Unable to create revert PR: ${e}`, this.id);
throw e;
}
}
async getPullRequest(id: number): Promise<PullRequestModel | undefined> {
try {
Logger.debug(`Fetch pull request ${id} - enter`, this.id);

Просмотреть файл

@ -368,6 +368,12 @@ export interface CreatePullRequestResponse {
}
}
export interface RevertPullRequestResponse {
revertPullRequest: {
revertPullRequest: PullRequest
}
}
export interface AddReviewThreadResponse {
addPullRequestReviewThread: {
thread: ReviewThread;

Просмотреть файл

@ -7,6 +7,7 @@
import * as vscode from 'vscode';
import { IComment } from '../common/comment';
import Logger from '../common/logger';
import { ITelemetry } from '../common/telemetry';
import { asPromise, formatError } from '../common/utils';
import { getNonce, IRequestMessage, WebviewBase } from '../common/webview';
import { DescriptionNode } from '../view/treeNodes/descriptionNode';
@ -32,6 +33,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
protected _scrollPosition = { x: 0, y: 0 };
public static async createOrShow(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepositoryManager: FolderRepositoryManager,
issue: IssueModel,
@ -50,6 +52,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
} else {
const title = `Issue #${issue.number.toString()}`;
IssueOverviewPanel.currentPanel = new IssueOverviewPanel(
telemetry,
extensionUri,
activeColumn || vscode.ViewColumn.Active,
title,
@ -76,7 +79,8 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
}
protected constructor(
private readonly _extensionUri: vscode.Uri,
protected readonly _telemetry: ITelemetry,
protected readonly _extensionUri: vscode.Uri,
column: vscode.ViewColumn,
title: string,
folderRepositoryManager: FolderRepositoryManager,

Просмотреть файл

@ -10,6 +10,7 @@ import { IComment } from '../common/comment';
import { commands, contexts } from '../common/executeCommands';
import Logger from '../common/logger';
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
import { ITelemetry } from '../common/telemetry';
import { ReviewEvent as CommonReviewEvent } from '../common/timelineEvent';
import { asPromise, dispose, formatError } from '../common/utils';
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
@ -51,6 +52,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
private _isUpdating: boolean = false;
public static async createOrShow(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepositoryManager: FolderRepositoryManager,
issue: PullRequestModel,
@ -70,10 +72,11 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
} else {
const title = `Pull Request #${issue.number.toString()}`;
PullRequestOverviewPanel.currentPanel = new PullRequestOverviewPanel(
telemetry,
extensionUri,
activeColumn || vscode.ViewColumn.Active,
title,
folderRepositoryManager,
folderRepositoryManager
);
}
@ -97,12 +100,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
}
protected constructor(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
column: vscode.ViewColumn,
title: string,
folderRepositoryManager: FolderRepositoryManager,
) {
super(extensionUri, column, title, folderRepositoryManager, PULL_REQUEST_OVERVIEW_VIEW_TYPE);
super(telemetry, extensionUri, column, title, folderRepositoryManager, PULL_REQUEST_OVERVIEW_VIEW_TYPE);
this.registerPrListeners();
onDidUpdatePR(
@ -303,7 +307,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
isAuthor: currentUser.login === pullRequest.author.login,
currentUserReviewState: reviewState,
isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark,
isEnterprise: pullRequest.githubRepository.remote.isEnterprise
isEnterprise: pullRequest.githubRepository.remote.isEnterprise,
revertable: pullRequest.state === GithubItemStateEnum.Merged
};
this._postMessage({
command: 'pr.initialize',
@ -400,11 +405,11 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
case 'pr.update-branch':
return this.updateBranch(message);
case 'pr.gotoChangesSinceReview':
this.gotoChangesSinceReview();
break;
return this.gotoChangesSinceReview();
case 'pr.re-request-review':
this.reRequestReview(message);
break;
return this.reRequestReview(message);
case 'pr.revert':
return this.revert(message);
}
}
@ -809,6 +814,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
});
}
private async revert(message: IRequestMessage<string>): Promise<void> {
await this._folderRepositoryManager.createPullRequestHelper.revert(this._telemetry, this._extensionUri, this._folderRepositoryManager, this._item, async (pullRequest) => {
const result: Partial<PullRequest> = { revertable: !pullRequest };
return this._replyMessage(message, result);
});
}
private async copyPrLink(): Promise<void> {
return vscode.env.clipboard.writeText(this._item.html_url);
}

Просмотреть файл

@ -175,6 +175,14 @@ mutation CreatePullRequest($input: CreatePullRequestInput!) {
}
}
mutation RevertPullRequest($input: RevertPullRequestInput!) {
revertPullRequest(input: $input) {
revertPullRequest {
...PullRequestFragment
}
}
}
# Queries that only exist in this file and in extra
mutation DequeuePullRequest($input: DequeuePullRequestInput!) {

Просмотреть файл

@ -142,4 +142,12 @@ mutation CreatePullRequest($input: CreatePullRequestInput!) {
...PullRequestFragment
}
}
}
}
mutation RevertPullRequest($input: RevertPullRequestInput!) {
revertPullRequest(input: $input) {
revertPullRequest {
...PullRequestFragment
}
}
}

Просмотреть файл

@ -0,0 +1,89 @@
/*---------------------------------------------------------------------------------------------
* 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 { CreatePullRequestNew } from '../../common/views';
import { openDescription } from '../commands';
import { commands } from '../common/executeCommands';
import { ITelemetry } from '../common/telemetry';
import { IRequestMessage } from '../common/webview';
import { BaseCreatePullRequestViewProvider, BasePullRequestDataModel } from './createPRViewProvider';
import {
FolderRepositoryManager,
PullRequestDefaults,
} from './folderRepositoryManager';
import { BaseBranchMetadata } from './pullRequestGitHelper';
import { PullRequestModel } from './pullRequestModel';
export class RevertPullRequestViewProvider extends BaseCreatePullRequestViewProvider implements vscode.WebviewViewProvider, vscode.Disposable {
protected _canModifyBranches: boolean = false;
constructor(
telemetry: ITelemetry,
model: BasePullRequestDataModel,
extensionUri: vscode.Uri,
folderRepositoryManager: FolderRepositoryManager,
pullRequestDefaults: PullRequestDefaults,
private readonly pullRequest: PullRequestModel
) {
super(telemetry, model, extensionUri, folderRepositoryManager, pullRequestDefaults, pullRequest.base.name);
}
protected async getTitleAndDescription(): Promise<{ title: string; description: string; }> {
return {
title: vscode.l10n.t('Revert "{0}"', this.pullRequest.title),
description: vscode.l10n.t('Reverts {0}', `${this.pullRequest.remote.owner}/${this.pullRequest.remote.repositoryName}#${this.pullRequest.number}`)
};
}
protected async detectBaseMetadata(): Promise<BaseBranchMetadata | undefined> {
return {
owner: this.pullRequest.remote.owner,
repositoryName: this.pullRequest.remote.repositoryName,
branch: this.pullRequest.base.name
};
}
protected getTitleAndDescriptionProvider(_name?: string) {
return undefined;
}
protected async create(message: IRequestMessage<CreatePullRequestNew>): Promise<void> {
let revertPr: PullRequestModel | undefined;
RevertPullRequestViewProvider.withProgress(async () => {
commands.setContext('pr:creating', true);
try {
revertPr = await this._folderRepositoryManager.revert(this.pullRequest, message.args.title, message.args.body, message.args.draft);
if (revertPr) {
await this.postCreate(message, revertPr);
await openDescription(this.telemetry, revertPr, undefined, this._folderRepositoryManager, true);
}
} catch (e) {
if (!revertPr) {
let errorMessage: string = e.message;
if (errorMessage.startsWith('GraphQL error: ')) {
errorMessage = errorMessage.substring('GraphQL error: '.length);
}
this._throwError(message, errorMessage);
} else {
if ((e as Error).message === 'GraphQL error: ["Pull request Pull request is in unstable status"]') {
// This error can happen if the PR isn't fully created by the time we try to set properties on it. Try again.
await this.postCreate(message, revertPr);
}
// All of these errors occur after the PR is created, so the error is not critical.
vscode.window.showErrorMessage(vscode.l10n.t('There was an error creating the pull request: {0}', (e as Error).message));
}
} finally {
commands.setContext('pr:creating', false);
if (revertPr) {
this._onDone.fire(revertPr);
} else {
await this._replyMessage(message, {});
}
}
});
}
}

Просмотреть файл

@ -89,6 +89,7 @@ export interface PullRequest {
hasReviewDraft: boolean;
lastReviewType?: ReviewType;
revertable?: boolean;
busy?: boolean;
}
@ -108,4 +109,4 @@ export enum PreReviewState {
Available,
ReviewedWithComments,
ReviewedWithoutComments
}
}

Просмотреть файл

@ -21,6 +21,7 @@ import { CredentialStore } from '../../github/credentials';
import { MockExtensionContext } from '../mocks/mockExtensionContext';
import { Uri } from 'vscode';
import { GitHubServerType } from '../../common/authentication';
import { CreatePullRequestHelper } from '../../view/createPullRequestHelper';
describe('PullRequestManager', function () {
let sinon: SinonSandbox;
@ -35,7 +36,7 @@ describe('PullRequestManager', function () {
const repository = new MockRepository();
const context = new MockExtensionContext();
const credentialStore = new CredentialStore(telemetry, context);
manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore);
manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, new CreatePullRequestHelper());
});
afterEach(function () {

Просмотреть файл

@ -23,6 +23,7 @@ import { CredentialStore } from '../../github/credentials';
import { GitHubServerType } from '../../common/authentication';
import { GitHubRemote } from '../../common/remote';
import { CheckState } from '../../github/interface';
import { CreatePullRequestHelper } from '../../view/createPullRequestHelper';
const EXTENSION_URI = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../..');
@ -43,7 +44,8 @@ describe('PullRequestOverview', function () {
const repository = new MockRepository();
telemetry = new MockTelemetry();
credentialStore = new CredentialStore(telemetry, context);
pullRequestManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore);
const createPrHelper = new CreatePullRequestHelper();
pullRequestManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper);
const url = 'https://github.com/aaa/bbb';
remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom);
@ -76,7 +78,7 @@ describe('PullRequestOverview', function () {
const prItem = convertRESTPullRequestToRawPullRequest(new PullRequestBuilder().number(1000).build(), repo);
const prModel = new PullRequestModel(credentialStore, telemetry, repo, remote, prItem);
await PullRequestOverviewPanel.createOrShow(EXTENSION_URI, pullRequestManager, prModel);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel);
assert(
createWebviewPanel.calledWith(sinonMatch.string, 'Pull Request #1000', vscode.ViewColumn.One, {
@ -112,7 +114,7 @@ describe('PullRequestOverview', function () {
sinon.stub(prModel0, 'getReviewRequests').resolves([]);
sinon.stub(prModel0, 'getTimelineEvents').resolves([]);
sinon.stub(prModel0, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
await PullRequestOverviewPanel.createOrShow(EXTENSION_URI, pullRequestManager, prModel0);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel0);
const panel0 = PullRequestOverviewPanel.currentPanel;
assert.notStrictEqual(panel0, undefined);
@ -125,7 +127,7 @@ describe('PullRequestOverview', function () {
sinon.stub(prModel1, 'getReviewRequests').resolves([]);
sinon.stub(prModel1, 'getTimelineEvents').resolves([]);
sinon.stub(prModel1, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]);
await PullRequestOverviewPanel.createOrShow(EXTENSION_URI, pullRequestManager, prModel1);
await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel1);
assert.strictEqual(panel0, PullRequestOverviewPanel.currentPanel);
assert.strictEqual(createWebviewPanel.callCount, 1);

Просмотреть файл

@ -30,6 +30,7 @@ import { GitHubServerType } from '../../common/authentication';
import { DataUri } from '../../common/uri';
import { IAccount, ITeam } from '../../github/interface';
import { asPromise } from '../../common/utils';
import { CreatePullRequestHelper } from '../../view/createPullRequestHelper';
describe('GitHub Pull Requests view', function () {
let sinon: SinonSandbox;
@ -38,6 +39,8 @@ describe('GitHub Pull Requests view', function () {
let provider: PullRequestsTreeDataProvider;
let credentialStore: CredentialStore;
let reposManager: RepositoriesManager;
let createPrHelper: CreatePullRequestHelper;
beforeEach(function () {
sinon = createSandbox();
@ -52,6 +55,7 @@ describe('GitHub Pull Requests view', function () {
);
provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager);
credentialStore = new CredentialStore(telemetry, context);
createPrHelper = new CreatePullRequestHelper();
// For tree view unit tests, we don't test the authentication flow, so `showSignInNotification` returns
// a dummy GitHub/Octokit object.
@ -97,7 +101,7 @@ describe('GitHub Pull Requests view', function () {
it('has no children when repositories have not yet been initialized', async function () {
const repository = new MockRepository();
repository.addRemote('origin', 'git@github.com:aaa/bbb');
reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore));
reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper));
provider.initialize([], credentialStore);
const rootNodes = await provider.getChildren();
@ -107,7 +111,7 @@ describe('GitHub Pull Requests view', function () {
it('opens the viewlet and displays the default categories', async function () {
const repository = new MockRepository();
repository.addRemote('origin', 'git@github.com:aaa/bbb');
reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore));
reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper));
sinon.stub(credentialStore, 'isAuthenticated').returns(true);
await reposManager.folderManagers[0].updateRepositories();
provider.initialize([], credentialStore);
@ -175,7 +179,7 @@ describe('GitHub Pull Requests view', function () {
await repository.createBranch('non-pr-branch', false);
const manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore);
const manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper);
reposManager.insertFolderManager(manager);
sinon.stub(manager, 'createGitHubRepository').callsFake((r, cs) => {
assert.deepStrictEqual(r, remote);

Просмотреть файл

@ -77,7 +77,7 @@ describe('ReviewCommentController', function () {
const createPrHelper = new CreatePullRequestHelper();
Resource.initialize(context);
gitApiImpl = new GitApiImpl();
manager = new FolderRepositoryManager(0, context, repository, telemetry, gitApiImpl, credentialStore);
manager = new FolderRepositoryManager(0, context, repository, telemetry, gitApiImpl, credentialStore, createPrHelper);
reposManager.insertFolderManager(manager);
const tree = new PullRequestChangesTreeDataProvider(context, gitApiImpl, reposManager);
reviewManager = new ReviewManager(0, context, repository, manager, telemetry, tree, provider, new ShowPullRequest(), activePrViewCoordinator, createPrHelper, gitApiImpl);
@ -172,7 +172,7 @@ describe('ReviewCommentController', function () {
const localFileChanges = [createLocalFileChange(uri, fileName, repository.rootUri)];
const reviewModel = new ReviewModel();
reviewModel.localFileChanges = localFileChanges;
const reviewCommentController = new TestReviewCommentController(reviewManager, manager, repository, reviewModel, gitApiImpl);
const reviewCommentController = new TestReviewCommentController(reviewManager, manager, repository, reviewModel, gitApiImpl, telemetry);
sinon.stub(activePullRequest, 'validateDraftMode').returns(Promise.resolve(false));
sinon.stub(activePullRequest, 'getReviewThreads').returns(
@ -237,7 +237,8 @@ describe('ReviewCommentController', function () {
manager,
repository,
reviewModel,
gitApiImpl
gitApiImpl,
telemetry
);
const thread = createGHPRCommentThread('review-1.1', uri);

Просмотреть файл

@ -4,13 +4,15 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { ITelemetry } from '../common/telemetry';
import { FolderRepositoryManager } from '../github/folderRepositoryManager';
import { GitHubRepository } from '../github/githubRepository';
import { PullRequestModel } from '../github/pullRequestModel';
export abstract class CommentControllerBase {
constructor(
protected _folderRepoManager: FolderRepositoryManager
protected _folderRepoManager: FolderRepositoryManager,
protected _telemetry: ITelemetry
) { }
protected _commentController: vscode.CommentController;

Просмотреть файл

@ -7,45 +7,42 @@ import * as vscode from 'vscode';
import { Repository } from '../api/api';
import { ITelemetry } from '../common/telemetry';
import { dispose } from '../common/utils';
import { CreatePullRequestViewProvider } from '../github/createPRViewProvider';
import { BaseCreatePullRequestViewProvider, BasePullRequestDataModel, CreatePullRequestViewProvider } from '../github/createPRViewProvider';
import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager';
import { PullRequestModel } from '../github/pullRequestModel';
import { RevertPullRequestViewProvider } from '../github/revertPRViewProvider';
import { CompareChanges } from './compareChangesTreeDataProvider';
import { CreatePullRequestDataModel } from './createPullRequestDataModel';
export class CreatePullRequestHelper implements vscode.Disposable {
private _disposables: vscode.Disposable[] = [];
private _createPRViewProvider: CreatePullRequestViewProvider | undefined;
private _createPRViewProvider: BaseCreatePullRequestViewProvider | undefined;
private _treeView: CompareChanges | undefined;
private _postCreateCallback: ((pullRequestModel: PullRequestModel) => Promise<void>) | undefined;
private _postCreateCallback: ((pullRequestModel: PullRequestModel | undefined) => Promise<void>) | undefined;
constructor() { }
private registerListeners(repository: Repository, usingCurrentBranchAsCompare: boolean) {
private registerListeners(repository: Repository, usingCurrentBranchAsCompare: boolean, activeContext: string) {
this._disposables.push(
this._createPRViewProvider!.onDone(async createdPR => {
if (createdPR) {
await CreatePullRequestViewProvider.withProgress(async () => {
return this._postCreateCallback?.(createdPR);
});
}
vscode.commands.executeCommand('setContext', activeContext, false);
await CreatePullRequestViewProvider.withProgress(async () => {
return this._postCreateCallback?.(createdPR);
});
this.dispose();
}),
);
this._disposables.push(
vscode.commands.registerCommand('pr.addAssigneesToNewPr', _ => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
return this._createPRViewProvider.addAssignees();
}
return this._createPRViewProvider?.addAssignees();
}),
);
this._disposables.push(
vscode.commands.registerCommand('pr.addReviewersToNewPr', _ => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
return this._createPRViewProvider.addReviewers();
}
return this._createPRViewProvider?.addReviewers();
}),
);
@ -57,60 +54,50 @@ export class CreatePullRequestHelper implements vscode.Disposable {
this._disposables.push(
vscode.commands.registerCommand('pr.addMilestoneToNewPr', _ => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
return this._createPRViewProvider.addMilestone();
}
return this._createPRViewProvider?.addMilestone();
}),
);
this._disposables.push(
vscode.commands.registerCommand('pr.addProjectsToNewPr', _ => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
return this._createPRViewProvider.addProjects();
}
return this._createPRViewProvider?.addProjects();
}),
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuCreate', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(false, false, undefined);
}
this._createPRViewProvider?.createFromCommand(false, false, undefined);
})
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuDraft', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(true, false, undefined);
}
this._createPRViewProvider?.createFromCommand(true, false, undefined);
})
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuMergeWhenReady', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(false, true, undefined, true);
}
this._createPRViewProvider?.createFromCommand(false, true, undefined, true);
})
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuMerge', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(false, true, 'merge');
}
this._createPRViewProvider?.createFromCommand(false, true, 'merge');
})
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuSquash', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(false, true, 'squash');
}
this._createPRViewProvider?.createFromCommand(false, true, 'squash');
})
);
this._disposables.push(
vscode.commands.registerCommand('pr.createPrMenuRebase', () => {
if (this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.createFromCommand(false, true, 'rebase');
}
this._createPRViewProvider?.createFromCommand(false, true, 'rebase');
})
);
this._disposables.push(
@ -124,7 +111,7 @@ export class CreatePullRequestHelper implements vscode.Disposable {
if (usingCurrentBranchAsCompare) {
this._disposables.push(
repository.state.onDidChange(_ => {
if (this._createPRViewProvider && repository.state.HEAD) {
if (this._createPRViewProvider && repository.state.HEAD && this._createPRViewProvider instanceof CreatePullRequestViewProvider) {
this._createPRViewProvider.setDefaultCompareBranch(repository.state.HEAD);
}
}),
@ -158,24 +145,69 @@ export class CreatePullRequestHelper implements vscode.Disposable {
}
}
async create(
async revert(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepoManager: FolderRepositoryManager,
compareBranch: string | undefined,
callback: (pullRequestModel: PullRequestModel) => Promise<void>,
pullRequestModel: PullRequestModel,
callback: (pullRequest: PullRequestModel | undefined) => Promise<void>,
) {
this.reset();
this._postCreateCallback = callback;
await folderRepoManager.loginAndUpdate();
vscode.commands.executeCommand('setContext', 'github:createPullRequest', true);
const activeContext = 'github:revertPullRequest';
vscode.commands.executeCommand('setContext', activeContext, true);
if (!this._createPRViewProvider || !(this._createPRViewProvider instanceof RevertPullRequestViewProvider)) {
this._createPRViewProvider?.dispose();
const model: BasePullRequestDataModel = {
baseOwner: pullRequestModel.remote.owner,
repositoryName: pullRequestModel.remote.repositoryName
};
this._createPRViewProvider = new RevertPullRequestViewProvider(
telemetry,
model,
extensionUri,
folderRepoManager,
{ base: pullRequestModel.base.name, owner: pullRequestModel.remote.owner, repo: pullRequestModel.remote.repositoryName },
pullRequestModel
);
this.registerListeners(folderRepoManager.repository, false, activeContext);
this._disposables.push(
vscode.window.registerWebviewViewProvider(
this._createPRViewProvider.viewType,
this._createPRViewProvider,
),
);
}
this._createPRViewProvider.show();
}
async create(
telemetry: ITelemetry,
extensionUri: vscode.Uri,
folderRepoManager: FolderRepositoryManager,
compareBranch: string | undefined,
callback: (pullRequestModel: PullRequestModel | undefined) => Promise<void>,
) {
this.reset();
this._postCreateCallback = callback;
await folderRepoManager.loginAndUpdate();
const activeContext = 'github:createPullRequest';
vscode.commands.executeCommand('setContext', activeContext, true);
const branch =
((compareBranch ? await folderRepoManager.repository.getBranch(compareBranch) : undefined) ??
folderRepoManager.repository.state.HEAD?.name ? folderRepoManager.repository.state.HEAD : undefined);
if (!this._createPRViewProvider) {
let createViewProvider: CreatePullRequestViewProvider;
if (!this._createPRViewProvider || !(this._createPRViewProvider instanceof CreatePullRequestViewProvider)) {
this._createPRViewProvider?.dispose();
const pullRequestDefaults = await this.ensureDefaultsAreLocal(
folderRepoManager,
await folderRepoManager.getPullRequestDefaults(branch),
@ -183,7 +215,7 @@ export class CreatePullRequestHelper implements vscode.Disposable {
const compareOrigin = await folderRepoManager.getOrigin(branch);
const model = new CreatePullRequestDataModel(folderRepoManager, pullRequestDefaults.owner, pullRequestDefaults.base, compareOrigin.remote.owner, branch?.name ?? pullRequestDefaults.base, compareOrigin.remote.repositoryName);
this._createPRViewProvider = new CreatePullRequestViewProvider(
createViewProvider = this._createPRViewProvider = new CreatePullRequestViewProvider(
telemetry,
model,
extensionUri,
@ -196,7 +228,7 @@ export class CreatePullRequestHelper implements vscode.Disposable {
model
);
this.registerListeners(folderRepoManager.repository, !compareBranch);
this.registerListeners(folderRepoManager.repository, !compareBranch, activeContext);
this._disposables.push(
vscode.window.registerWebviewViewProvider(
@ -204,9 +236,11 @@ export class CreatePullRequestHelper implements vscode.Disposable {
this._createPRViewProvider,
),
);
} else {
createViewProvider = this._createPRViewProvider;
}
this._createPRViewProvider.show(branch);
createViewProvider.show(branch);
}
private reset() {

Просмотреть файл

@ -8,6 +8,7 @@ import { v4 as uuid } from 'uuid';
import * as vscode from 'vscode';
import { CommentHandler, registerCommentHandler, unregisterCommentHandler } from '../commentHandlerResolver';
import { DiffSide, IComment, SubjectType } from '../common/comment';
import { ITelemetry } from '../common/telemetry';
import { fromPRUri, Schemes } from '../common/uri';
import { dispose, groupBy } from '../common/utils';
import { FolderRepositoryManager } from '../github/folderRepositoryManager';
@ -38,8 +39,9 @@ export class PullRequestCommentController extends CommentControllerBase implemen
private readonly pullRequestModel: PullRequestModel,
folderRepoManager: FolderRepositoryManager,
commentController: vscode.CommentController,
telemetry: ITelemetry
) {
super(folderRepoManager);
super(folderRepoManager, telemetry);
this._commentController = commentController;
this._context = folderRepoManager.context;
this._commentHandlerId = uuid();
@ -454,7 +456,7 @@ export class PullRequestCommentController extends CommentControllerBase implemen
public async openReview(): Promise<void> {
await PullRequestOverviewPanel.createOrShow(this._folderRepoManager.context.extensionUri, this._folderRepoManager, this.pullRequestModel);
await PullRequestOverviewPanel.createOrShow(this._telemetry, this._folderRepoManager.context.extensionUri, this._folderRepoManager, this.pullRequestModel);
PullRequestOverviewPanel.scrollToReview();
/* __GDPR__

Просмотреть файл

@ -5,6 +5,7 @@
'use strict';
import * as vscode from 'vscode';
import { ITelemetry } from '../common/telemetry';
import { fromPRUri, Schemes } from '../common/uri';
import { FolderRepositoryManager } from '../github/folderRepositoryManager';
import { GHPRComment } from '../github/prComment';
@ -24,7 +25,7 @@ export class PRCommentControllerRegistry implements vscode.CommentingRangeProvid
private _activeChangeListeners: Map<FolderRepositoryManager, vscode.Disposable> = new Map();
public readonly resourceHints = { schemes: [Schemes.Pr] };
constructor(public commentsController: vscode.CommentController) {
constructor(public commentsController: vscode.CommentController, private _telemetry: ITelemetry) {
this.commentsController.commentingRangeProvider = this;
this.commentsController.reactionHandler = this.toggleReaction.bind(this);
}
@ -84,7 +85,7 @@ export class PRCommentControllerRegistry implements vscode.CommentingRangeProvid
}));
}
const handler = new PullRequestCommentController(pullRequestModel, folderRepositoryManager, this.commentsController);
const handler = new PullRequestCommentController(pullRequestModel, folderRepositoryManager, this.commentsController, this._telemetry);
this._prCommentHandlers[prNumber] = {
handler,
refCount: 1,

Просмотреть файл

@ -15,6 +15,7 @@ import { mapNewPositionToOld, mapOldPositionToNew } from '../common/diffPosition
import { GitChangeType } from '../common/file';
import Logger from '../common/logger';
import { PR_SETTINGS_NAMESPACE, PULL_BRANCH, PULL_PR_BRANCH_BEFORE_CHECKOUT, PullPRBranchVariants } from '../common/settingKeys';
import { ITelemetry } from '../common/telemetry';
import { fromReviewUri, ReviewUriParams, Schemes, toReviewUri } from '../common/uri';
import { dispose, formatError, groupBy, uniqBy } from '../common/utils';
import { FolderRepositoryManager } from '../github/folderRepositoryManager';
@ -59,9 +60,10 @@ export class ReviewCommentController extends CommentControllerBase
folderRepoManager: FolderRepositoryManager,
private _repository: Repository,
private _reviewModel: ReviewModel,
private _gitApi: GitApiImpl
private _gitApi: GitApiImpl,
telemetry: ITelemetry
) {
super(folderRepoManager);
super(folderRepoManager, telemetry);
this._context = this._folderRepoManager.context;
this._commentController = vscode.comments.createCommentController(
`github-review-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest!.number}`,

Просмотреть файл

@ -905,7 +905,8 @@ export class ReviewManager {
this._folderRepoManager,
this._repository,
this._reviewModel,
this._gitApi
this._gitApi,
this._telemetry
);
await this._reviewCommentController.initialize();
@ -1149,12 +1150,15 @@ export class ReviewManager {
}
public async createPullRequest(compareBranch?: string): Promise<void> {
const postCreate = async (createdPR: PullRequestModel) => {
const postCreate = async (createdPR: PullRequestModel | undefined) => {
if (!createdPR) {
return;
}
const postCreate = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'none' | 'openOverview' | 'checkoutDefaultBranch' | 'checkoutDefaultBranchAndShow' | 'checkoutDefaultBranchAndCopy'>(POST_CREATE, 'openOverview');
if (postCreate === 'openOverview') {
const descriptionNode = this.changesInPrDataProvider.getDescriptionNode(this._folderRepoManager);
await openDescription(
this._context,
this._telemetry,
createdPR,
descriptionNode,
@ -1192,7 +1196,6 @@ export class ReviewManager {
const descriptionNode = this.changesInPrDataProvider.getDescriptionNode(this._folderRepoManager);
await openDescription(
this._context,
this._telemetry,
pullRequest,
descriptionNode,

Просмотреть файл

@ -18,7 +18,7 @@ export class DescriptionNode extends TreeNode implements vscode.TreeItem {
constructor(
public parent: TreeNodeParent,
public label: string,
public pullRequestModel: PullRequestModel,
public readonly pullRequestModel: PullRequestModel,
public readonly repository: Repository,
private readonly folderRepositoryManager: FolderRepositoryManager
) {

Просмотреть файл

@ -64,6 +64,12 @@ export class PRContext {
public deleteBranch = () => this.postMessage({ command: 'pr.deleteBranch' });
public revert = async () => {
this.updatePR({ busy: true });
const revertResult = await this.postMessage({ command: 'pr.revert' });
this.updatePR({ busy: false, ...revertResult });
};
public readyForReview = (): Promise<ReadyForReview> => this.postMessage({ command: 'pr.readyForReview' });
public addReviewers = () => this.postMessage({ command: 'pr.change-reviewers' });

Просмотреть файл

@ -5,10 +5,12 @@
import { createContext } from 'react';
import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, RemoteInfo, ScrollPosition, TitleAndDescriptionArgs, TitleAndDescriptionResult } from '../../common/views';
import { compareIgnoreCase } from '../../src/common/utils';
import { PreReviewState } from '../../src/github/views';
import { getMessageHandler, MessageHandler, vscode } from './message';
const defaultCreateParams: CreateParamsNew = {
canModifyBranches: true,
defaultBaseRemote: undefined,
defaultBaseBranch: undefined,
defaultCompareRemote: undefined,
@ -49,7 +51,25 @@ export class CreatePRContextNew {
}
}
get isCreatable(): boolean {
if (!this.createParams.canModifyBranches) {
return true;
}
if (this.createParams.baseRemote && this.createParams.compareRemote && this.createParams.baseBranch && this.createParams.compareBranch
&& compareIgnoreCase(this.createParams.baseRemote?.owner, this.createParams.compareRemote?.owner) === 0
&& compareIgnoreCase(this.createParams.baseRemote?.repositoryName, this.createParams.compareRemote?.repositoryName) === 0
&& compareIgnoreCase(this.createParams.baseBranch, this.createParams.compareBranch) === 0) {
return false;
}
return true;
}
get initialized(): boolean {
if (!this.createParams.canModifyBranches) {
return true;
}
if (this.createParams.defaultBaseRemote !== undefined
|| this.createParams.defaultBaseBranch !== undefined
|| this.createParams.defaultCompareRemote !== undefined

Просмотреть файл

@ -17,7 +17,7 @@ import {
} from '../../src/common/timelineEvent';
import { groupBy, UnreachableCaseError } from '../../src/common/utils';
import PullRequestContext from '../common/context';
import { CommentView } from './comment';
import { CommentView } from './comment';
import Diff from './diff';
import { commitIcon, mergeIcon, plusIcon } from './icon';
import { nbsp } from './space';
@ -26,26 +26,26 @@ import { AuthorLink, Avatar } from './user';
export const Timeline = ({ events }: { events: TimelineEvent[] }) => (
<>
{events.map(event => {
switch (event.event) {
case EventType.Committed:
return <CommitEventView key={`commit${event.id}`} {...event} />;
case EventType.Reviewed:
return <ReviewEventView key={`review${event.id}`} {...event} />;
case EventType.Commented:
return <CommentEventView key={`comment${event.id}`} {...event} />;
case EventType.Merged:
return <MergedEventView key={`merged${event.id}`} {...event} />;
case EventType.Assigned:
return <AssignEventView key={`assign${event.id}`} {...event} />;
case EventType.HeadRefDeleted:
return <HeadDeleteEventView key={`head${event.id}`} {...event} />;
case EventType.NewCommitsSinceReview:
return <NewCommitsSinceReviewEventView key={`newCommits${event.id}`} />;
default:
throw new UnreachableCaseError(event);
}
})}
{events.map(event => {
switch (event.event) {
case EventType.Committed:
return <CommitEventView key={`commit${event.id}`} {...event} />;
case EventType.Reviewed:
return <ReviewEventView key={`review${event.id}`} {...event} />;
case EventType.Commented:
return <CommentEventView key={`comment${event.id}`} {...event} />;
case EventType.Merged:
return <MergedEventView key={`merged${event.id}`} {...event} />;
case EventType.Assigned:
return <AssignEventView key={`assign${event.id}`} {...event} />;
case EventType.HeadRefDeleted:
return <HeadDeleteEventView key={`head${event.id}`} {...event} />;
case EventType.NewCommitsSinceReview:
return <NewCommitsSinceReviewEventView key={`newCommits${event.id}`} />;
default:
throw new UnreachableCaseError(event);
}
})}
</>
);
@ -66,7 +66,7 @@ const CommitEventView = (event: CommitEvent) => (
</a>
</div>
</div>
<div className="sha-with-timestamp">
<div className="timeline-detail">
<a className="sha" href={event.htmlUrl} title={event.htmlUrl}>
{event.sha.slice(0, 7)}
</a>
@ -106,16 +106,16 @@ const ReviewEventView = (event: ReviewEvent) => {
const reviewIsPending = event.state === 'PENDING';
return (
<CommentView comment={event} allowEmpty={true}>
{/* Don't show the empty comment body unless a comment has been written. Shows diffs and suggested changes. */}
{event.comments.length ? (
<div className="comment-body review-comment-body">
{Object.entries(comments).map(([key, thread]) => {
return <CommentThread key={key} thread={thread} event={event} />;
})}
</div>
) : null}
{/* Don't show the empty comment body unless a comment has been written. Shows diffs and suggested changes. */}
{event.comments.length ? (
<div className="comment-body review-comment-body">
{Object.entries(comments).map(([key, thread]) => {
return <CommentThread key={key} thread={thread} event={event} />;
})}
</div>
) : null}
{reviewIsPending ? <AddReviewSummaryComment /> : null}
{reviewIsPending ? <AddReviewSummaryComment /> : null}
</CommentView>
);
};
@ -222,28 +222,36 @@ function AddReviewSummaryComment() {
const CommentEventView = (event: CommentEvent) => <CommentView headerInEditMode comment={event} />;
const MergedEventView = (event: MergedEvent) => (
<div className="comment-container commit">
<div className="commit-message">
{mergeIcon}
{nbsp}
<div className="avatar-container">
<Avatar for={event.user} />
</div>
<AuthorLink for={event.user} />
<div className="message">
merged commit{nbsp}
<a className="sha" href={event.commitUrl} title={event.commitUrl}>
{event.sha.substr(0, 7)}
</a>
{nbsp}
into {event.mergeRef}
const MergedEventView = (event: MergedEvent) => {
const { revert, pr } = useContext(PullRequestContext);
return (
<div className="comment-container commit">
<div className="commit-message">
{mergeIcon}
{nbsp}
<div className="avatar-container">
<Avatar for={event.user} />
</div>
<AuthorLink for={event.user} />
<div className="message">
merged commit{nbsp}
<a className="sha" href={event.commitUrl} title={event.commitUrl}>
{event.sha.substr(0, 7)}
</a>
{nbsp}
into {event.mergeRef}
{nbsp}
</div>
<Timestamp href={event.url} date={event.createdAt} />
</div>
<Timestamp href={event.url} date={event.createdAt} />
{pr.revertable ?
<div className="timeline-detail">
<button className='secondary' disabled={pr.busy} onClick={revert}>Revert</button>
</div> : null}
</div>
</div>
);
);
};
const HeadDeleteEventView = (event: HeadRefDeleteEvent) => (
<div className="comment-container commit">

Просмотреть файл

@ -6,7 +6,6 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { render } from 'react-dom';
import { CreateParamsNew, RemoteInfo } from '../../common/views';
import { compareIgnoreCase } from '../../src/common/utils';
import { isTeam, MergeMethod } from '../../src/github/interface';
import PullRequestContextNew from '../common/createContextNew';
import { ErrorBoundary } from '../common/errorBoundary';
@ -89,15 +88,6 @@ export function main() {
setBusy(false);
}
let isCreateable: boolean = true;
if (ctx.createParams.baseRemote && ctx.createParams.compareRemote && ctx.createParams.baseBranch && ctx.createParams.compareBranch
&& compareIgnoreCase(ctx.createParams.baseRemote?.owner, ctx.createParams.compareRemote?.owner) === 0
&& compareIgnoreCase(ctx.createParams.baseRemote?.repositoryName, ctx.createParams.compareRemote?.repositoryName) === 0
&& compareIgnoreCase(ctx.createParams.baseBranch, ctx.createParams.compareBranch) === 0) {
isCreateable = false;
}
const onKeyDown = useCallback((isTitle: boolean, e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
e.preventDefault();
@ -198,7 +188,7 @@ export function main() {
}
return <div className='group-main' data-vscode-context='{"preventDefaultContextMenuItems": true}'>
<div className='group-branches'>
<div className='group-branches' hidden={!ctx.createParams.canModifyBranches}>
<div className='input-label base'>
<div className="deco">
<span title='Base branch' aria-hidden='true'>{prBaseIcon} Base</span>
@ -357,7 +347,7 @@ export function main() {
defaultOptionLabel={() => createMethodLabel(ctx.createParams.isDraft, ctx.createParams.autoMerge, ctx.createParams.autoMergeMethod, ctx.createParams.baseHasMergeQueue).label}
defaultOptionValue={() => createMethodLabel(ctx.createParams.isDraft, ctx.createParams.autoMerge, ctx.createParams.autoMergeMethod, ctx.createParams.baseHasMergeQueue).value}
optionsTitle='Create with Option'
disabled={isBusy || isGeneratingTitle || params.reviewing || !isCreateable || !ctx.initialized}
disabled={isBusy || isGeneratingTitle || params.reviewing || !ctx.isCreatable || !ctx.initialized}
/>
</div>

Просмотреть файл

@ -137,6 +137,7 @@ button.input-box {
.group-description {
flex-grow: 1;
margin-top: 10px;
max-height: 500px;
}
input[type=text],

Просмотреть файл

@ -658,7 +658,7 @@ body button .icon {
white-space: nowrap;
}
.sha-with-timestamp {
.timeline-detail {
display: flex;
align-items: center;
gap: 8px;