Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/fixtures/ghpr/commands.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
'use strict';
6
7
import * as pathLib from 'path';
8
import * as vscode from 'vscode';
9
import { Repository } from './api/api';
10
import { GitErrorCodes } from './api/api1';
11
import { CommentReply, resolveCommentHandler } from './commentHandlerResolver';
12
import { IComment } from './common/comment';
13
import Logger from './common/logger';
14
import { ITelemetry } from './common/telemetry';
15
import { Schemes, asImageDataURI, fromReviewUri } from './common/uri';
16
import { formatError } from './common/utils';
17
import { EXTENSION_ID } from './constants';
18
import { FolderRepositoryManager } from './github/folderRepositoryManager';
19
import { GitHubRepository } from './github/githubRepository';
20
import { PullRequest } from './github/interface';
21
import { NotificationProvider } from './github/notifications';
22
import { GHPRComment, GHPRCommentThread, TemporaryComment } from './github/prComment';
23
import { PullRequestModel } from './github/pullRequestModel';
24
import { PullRequestOverviewPanel } from './github/pullRequestOverview';
25
import { RepositoriesManager } from './github/repositoriesManager';
26
import { getIssuesUrl, getPullsUrl, isInCodespaces, vscodeDevPrLink } from './github/utils';
27
import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';
28
import { ReviewCommentController } from './view/reviewCommentController';
29
import { ReviewManager } from './view/reviewManager';
30
import { CategoryTreeNode } from './view/treeNodes/categoryNode';
31
import { CommitNode } from './view/treeNodes/commitNode';
32
import { DescriptionNode } from './view/treeNodes/descriptionNode';
33
import {
34
FileChangeNode,
35
GitFileChangeNode,
36
InMemFileChangeNode,
37
RemoteFileChangeNode,
38
openFileCommand,
39
} from './view/treeNodes/fileChangeNode';
40
import { PRNode } from './view/treeNodes/pullRequestNode';
41
42
const _onDidUpdatePR = new vscode.EventEmitter<PullRequest | void>();
43
export const onDidUpdatePR: vscode.Event<PullRequest | void> = _onDidUpdatePR.event;
44
45
function ensurePR(folderRepoManager: FolderRepositoryManager, pr?: PRNode | PullRequestModel): PullRequestModel {
46
// If the command is called from the command palette, no arguments are passed.
47
if (!pr) {
48
if (!folderRepoManager.activePullRequest) {
49
vscode.window.showErrorMessage(vscode.l10n.t('Unable to find current pull request.'));
50
throw new Error('Unable to find current pull request.');
51
}
52
53
return folderRepoManager.activePullRequest;
54
} else {
55
return pr instanceof PRNode ? pr.pullRequestModel : pr;
56
}
57
}
58
59
export async function openDescription(
60
context: vscode.ExtensionContext,
61
telemetry: ITelemetry,
62
pullRequestModel: PullRequestModel,
63
descriptionNode: DescriptionNode | undefined,
64
folderManager: FolderRepositoryManager,
65
notificationProvider?: NotificationProvider
66
) {
67
const pullRequest = ensurePR(folderManager, pullRequestModel);
68
descriptionNode?.reveal(descriptionNode, { select: true, focus: true });
69
// Create and show a new webview
70
await PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest);
71
72
if (notificationProvider?.hasNotification(pullRequest)) {
73
notificationProvider.markPrNotificationsAsRead(pullRequest);
74
}
75
}
76
77
async function chooseItem<T>(
78
activePullRequests: T[],
79
propertyGetter: (itemValue: T) => string,
80
options?: vscode.QuickPickOptions,
81
): Promise<T | undefined> {
82
if (activePullRequests.length === 1) {
83
return activePullRequests[0];
84
}
85
interface Item extends vscode.QuickPickItem {
86
itemValue: T;
87
}
88
const items: Item[] = activePullRequests.map(currentItem => {
89
return {
90
label: propertyGetter(currentItem),
91
itemValue: currentItem,
92
};
93
});
94
return (await vscode.window.showQuickPick(items, options))?.itemValue;
95
}
96
97
export async function openPullRequestOnGitHub(e: PRNode | DescriptionNode | PullRequestModel, telemetry: ITelemetry) {
98
if (e instanceof PRNode || e instanceof DescriptionNode) {
99
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.pullRequestModel.html_url));
100
} else {
101
vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.html_url));
102
}
103
}
104
105
export function registerCommands(
106
context: vscode.ExtensionContext,
107
reposManager: RepositoriesManager,
108
reviewManagers: ReviewManager[],
109
telemetry: ITelemetry,
110
tree: PullRequestsTreeDataProvider
111
) {
112
context.subscriptions.push(
113
vscode.commands.registerCommand(
114
'pr.openPullRequestOnGitHub',
115
async (e: PRNode | DescriptionNode | PullRequestModel | undefined) => {
116
if (!e) {
117
const activePullRequests: PullRequestModel[] = reposManager.folderManagers
118
.map(folderManager => folderManager.activePullRequest!)
119
.filter(activePR => !!activePR);
120
121
if (activePullRequests.length >= 1) {
122
const result = await chooseItem<PullRequestModel>(
123
activePullRequests,
124
itemValue => itemValue.html_url,
125
);
126
if (result) {
127
openPullRequestOnGitHub(result, telemetry);
128
}
129
}
130
} else {
131
openPullRequestOnGitHub(e, telemetry);
132
}
133
},
134
),
135
);
136
137
context.subscriptions.push(
138
vscode.commands.registerCommand(
139
'pr.openAllDiffs',
140
async () => {
141
const activePullRequestsWithFolderManager = reposManager.folderManagers
142
.filter(folderManager => folderManager.activePullRequest)
143
.map(folderManager => {
144
return (({ activePr: folderManager.activePullRequest!, folderManager }));
145
});
146
147
const activePullRequestAndFolderManager = activePullRequestsWithFolderManager.length >= 1
148
? (
149
await chooseItem(
150
activePullRequestsWithFolderManager,
151
itemValue => itemValue.activePr.html_url,
152
)
153
)
154
: activePullRequestsWithFolderManager[0];
155
156
if (!activePullRequestAndFolderManager) {
157
return;
158
}
159
160
const { folderManager } = activePullRequestAndFolderManager;
161
const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);
162
163
if (!reviewManager) {
164
return;
165
}
166
167
reviewManager.reviewModel.localFileChanges
168
.forEach(localFileChange => localFileChange.openDiff(folderManager, { preview: false }));
169
}
170
),
171
);
172
173
context.subscriptions.push(
174
vscode.commands.registerCommand('review.suggestDiff', async e => {
175
try {
176
const folderManager = await chooseItem<FolderRepositoryManager>(
177
reposManager.folderManagers,
178
itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath),
179
);
180
if (!folderManager || !folderManager.activePullRequest) {
181
return;
182
}
183
184
const { indexChanges, workingTreeChanges } = folderManager.repository.state;
185
186
if (!indexChanges.length) {
187
if (workingTreeChanges.length) {
188
const yes = vscode.l10n.t('Yes');
189
const stageAll = await vscode.window.showWarningMessage(
190
vscode.l10n.t('There are no staged changes to suggest.\n\nWould you like to automatically stage all your of changes and suggest them?'),
191
{ modal: true },
192
yes,
193
);
194
if (stageAll === yes) {
195
await vscode.commands.executeCommand('git.stageAll');
196
} else {
197
return;
198
}
199
} else {
200
vscode.window.showInformationMessage(vscode.l10n.t('There are no changes to suggest.'));
201
return;
202
}
203
}
204
205
const diff = await folderManager.repository.diff(true);
206
207
let suggestEditMessage = vscode.l10n.t('Suggested edit:\n');
208
if (e && e.inputBox && e.inputBox.value) {
209
suggestEditMessage = `${e.inputBox.value}\n`;
210
e.inputBox.value = '';
211
}
212
213
const suggestEditText = `${suggestEditMessage}\`\`\`diff\n${diff}\n\`\`\``;
214
await folderManager.activePullRequest.createIssueComment(suggestEditText);
215
216
// Reset HEAD and then apply reverse diff
217
await vscode.commands.executeCommand('git.unstageAll');
218
219
const tempFilePath = pathLib.join(
220
folderManager.repository.rootUri.fsPath,
221
'.git',
222
`${folderManager.activePullRequest.number}.diff`,
223
);
224
const encoder = new TextEncoder();
225
const tempUri = vscode.Uri.file(tempFilePath);
226
227
await vscode.workspace.fs.writeFile(tempUri, encoder.encode(diff));
228
await folderManager.repository.apply(tempFilePath, true);
229
await vscode.workspace.fs.delete(tempUri);
230
} catch (err) {
231
const moreError = `${err}${err.stderr ? `\n${err.stderr}` : ''}`;
232
Logger.error(`Applying patch failed: ${moreError}`);
233
vscode.window.showErrorMessage(vscode.l10n.t('Applying patch failed: {0}', formatError(err)));
234
}
235
}),
236
);
237
238
context.subscriptions.push(
239
vscode.commands.registerCommand('pr.openFileOnGitHub', async (e: GitFileChangeNode | RemoteFileChangeNode) => {
240
if (e instanceof RemoteFileChangeNode) {
241
const choice = await vscode.window.showInformationMessage(
242
vscode.l10n.t('{0} can\'t be opened locally. Do you want to open it on GitHub?', e.changeModel.fileName),
243
vscode.l10n.t('Open'),
244
);
245
if (!choice) {
246
return;
247
}
248
}
249
if (e.changeModel.blobUrl) {
250
return vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(e.changeModel.blobUrl));
251
}
252
}),
253
);
254
255
context.subscriptions.push(
256
vscode.commands.registerCommand('pr.copyCommitHash', (e: CommitNode) => {
257
vscode.env.clipboard.writeText(e.sha);
258
}),
259
);
260
261
context.subscriptions.push(
262
vscode.commands.registerCommand('pr.openOriginalFile', async (e: GitFileChangeNode) => {
263
// if this is an image, encode it as a base64 data URI
264
const folderManager = reposManager.getManagerForIssueModel(e.pullRequest);
265
if (folderManager) {
266
const imageDataURI = await asImageDataURI(e.changeModel.parentFilePath, folderManager.repository);
267
vscode.commands.executeCommand('vscode.open', imageDataURI || e.changeModel.parentFilePath);
268
}
269
}),
270
);
271
272
context.subscriptions.push(
273
vscode.commands.registerCommand('pr.openModifiedFile', (e: GitFileChangeNode | undefined) => {
274
let uri: vscode.Uri | undefined;
275
const tab = vscode.window.tabGroups.activeTabGroup.activeTab;
276
277
if (e) {
278
uri = e.changeModel.filePath;
279
} else {
280
if (tab?.input instanceof vscode.TabInputTextDiff) {
281
uri = tab.input.modified;
282
}
283
}
284
if (uri) {
285
vscode.commands.executeCommand('vscode.open', uri, tab?.group.viewColumn);
286
}
287
}),
288
);
289
290
async function openDiffView(fileChangeNode: GitFileChangeNode | InMemFileChangeNode | vscode.Uri | undefined) {
291
if (fileChangeNode && !(fileChangeNode instanceof vscode.Uri)) {
292
const folderManager = reposManager.getManagerForIssueModel(fileChangeNode.pullRequest);
293
if (!folderManager) {
294
return;
295
}
296
return fileChangeNode.openDiff(folderManager);
297
} else if (fileChangeNode || vscode.window.activeTextEditor) {
298
const editor = fileChangeNode ? vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === fileChangeNode.toString())! : vscode.window.activeTextEditor!;
299
const visibleRanges = editor.visibleRanges;
300
const folderManager = reposManager.getManagerForFile(editor.document.uri);
301
if (!folderManager?.activePullRequest) {
302
return;
303
}
304
const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);
305
if (!reviewManager) {
306
return;
307
}
308
const change = reviewManager.reviewModel.localFileChanges.find(change => change.resourceUri.with({ query: '' }).toString() === editor.document.uri.toString());
309
await change?.openDiff(folderManager);
310
const tabInput = vscode.window.tabGroups.activeTabGroup.activeTab?.input;
311
const diffEditor = (tabInput instanceof vscode.TabInputTextDiff && tabInput.modified.toString() === editor.document.uri.toString()) ? vscode.window.activeTextEditor : undefined;
312
if (diffEditor) {
313
diffEditor.revealRange(visibleRanges[0]);
314
}
315
}
316
}
317
318
context.subscriptions.push(
319
vscode.commands.registerCommand(
320
'pr.openDiffView',
321
(fileChangeNode: GitFileChangeNode | InMemFileChangeNode | undefined) => {
322
return openDiffView(fileChangeNode);
323
},
324
),
325
);
326
327
context.subscriptions.push(
328
vscode.commands.registerCommand(
329
'pr.openDiffViewFromEditor',
330
(uri: vscode.Uri) => {
331
return openDiffView(uri);
332
},
333
),
334
);
335
336
context.subscriptions.push(
337
vscode.commands.registerCommand('pr.deleteLocalBranch', async (e: PRNode) => {
338
const folderManager = reposManager.getManagerForIssueModel(e.pullRequestModel);
339
if (!folderManager) {
340
return;
341
}
342
const pullRequestModel = ensurePR(folderManager, e);
343
const DELETE_BRANCH_FORCE = 'Delete Unmerged Branch';
344
let error = null;
345
346
try {
347
await folderManager.deleteLocalPullRequest(pullRequestModel);
348
} catch (e) {
349
if (e.gitErrorCode === GitErrorCodes.BranchNotFullyMerged) {
350
const action = await vscode.window.showErrorMessage(
351
vscode.l10n.t('The local branch \'{0}\' is not fully merged. Are you sure you want to delete it?', pullRequestModel.localBranchName ?? 'unknown branch'),
352
DELETE_BRANCH_FORCE,
353
);
354
355
if (action !== DELETE_BRANCH_FORCE) {
356
return;
357
}
358
359
try {
360
await folderManager.deleteLocalPullRequest(pullRequestModel, true);
361
} catch (e) {
362
error = e;
363
}
364
} else {
365
error = e;
366
}
367
}
368
369
if (error) {
370
await vscode.window.showErrorMessage(`Deleting local pull request branch failed: ${error}`);
371
} else {
372
// fire and forget
373
vscode.commands.executeCommand('pr.refreshList');
374
}
375
}),
376
);
377
378
function chooseReviewManager(repoPath?: string) {
379
if (repoPath) {
380
const uri = vscode.Uri.file(repoPath).toString();
381
for (const mgr of reviewManagers) {
382
if (mgr.repository.rootUri.toString() === uri) {
383
return mgr;
384
}
385
}
386
}
387
return chooseItem<ReviewManager>(
388
reviewManagers,
389
itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath),
390
{ placeHolder: vscode.l10n.t('Choose a repository to create a pull request in'), ignoreFocusOut: true },
391
);
392
}
393
394
function isSourceControl(x: any): x is Repository {
395
return !!x?.rootUri;
396
}
397
398
context.subscriptions.push(
399
vscode.commands.registerCommand(
400
'pr.create',
401
async (args?: { repoPath: string; compareBranch: string } | Repository) => {
402
// The arguments this is called with are either from the SCM view, or manually passed.
403
if (isSourceControl(args)) {
404
(await chooseReviewManager(args.rootUri.fsPath))?.createPullRequest();
405
} else {
406
(await chooseReviewManager(args?.repoPath))?.createPullRequest(args?.compareBranch);
407
}
408
},
409
),
410
);
411
412
context.subscriptions.push(
413
vscode.commands.registerCommand(
414
'pr.pushAndCreate',
415
async (args?: any | Repository) => {
416
if (isSourceControl(args)) {
417
const reviewManager = await chooseReviewManager(args.rootUri.fsPath);
418
if (reviewManager) {
419
if (args.state.HEAD?.upstream) {
420
await args.push();
421
}
422
reviewManager.createPullRequest();
423
}
424
}
425
},
426
),
427
);
428
429
context.subscriptions.push(
430
vscode.commands.registerCommand('pr.pick', async (pr: PRNode | DescriptionNode | PullRequestModel) => {
431
if (pr === undefined) {
432
// This is unexpected, but has happened a few times.
433
Logger.error('Unexpectedly received undefined when picking a PR.');
434
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
435
}
436
437
let pullRequestModel: PullRequestModel;
438
let repository: Repository | undefined;
439
440
if (pr instanceof PRNode || pr instanceof DescriptionNode) {
441
pullRequestModel = pr.pullRequestModel;
442
repository = pr.repository;
443
} else {
444
pullRequestModel = pr;
445
}
446
447
const fromDescriptionPage = pr instanceof PullRequestModel;
448
449
return vscode.window.withProgress(
450
{
451
location: vscode.ProgressLocation.SourceControl,
452
title: vscode.l10n.t('Switching to Pull Request #{0}', pullRequestModel.number),
453
},
454
async () => {
455
await ReviewManager.getReviewManagerForRepository(
456
reviewManagers,
457
pullRequestModel.githubRepository,
458
repository
459
)?.switch(pullRequestModel);
460
},
461
);
462
}),
463
);
464
465
context.subscriptions.push(
466
vscode.commands.registerCommand('pr.pickOnVscodeDev', async (pr: PRNode | DescriptionNode | PullRequestModel) => {
467
if (pr === undefined) {
468
// This is unexpected, but has happened a few times.
469
Logger.error('Unexpectedly received undefined when picking a PR.');
470
return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.'));
471
}
472
473
let pullRequestModel: PullRequestModel;
474
475
if (pr instanceof PRNode || pr instanceof DescriptionNode) {
476
pullRequestModel = pr.pullRequestModel;
477
} else {
478
pullRequestModel = pr;
479
}
480
481
return vscode.env.openExternal(vscode.Uri.parse(vscodeDevPrLink(pullRequestModel)));
482
}),
483
);
484
485
context.subscriptions.push(
486
vscode.commands.registerCommand('pr.exit', async (pr: PRNode | DescriptionNode | PullRequestModel | undefined) => {
487
let pullRequestModel: PullRequestModel | undefined;
488
489
if (pr instanceof PRNode || pr instanceof DescriptionNode) {
490
pullRequestModel = pr.pullRequestModel;
491
} else if (pr === undefined) {
492
pullRequestModel = await chooseItem<PullRequestModel>(reposManager.folderManagers
493
.map(folderManager => folderManager.activePullRequest!)
494
.filter(activePR => !!activePR),
495
itemValue => `${itemValue.number}: ${itemValue.title}`,
496
{ placeHolder: vscode.l10n.t('Choose the pull request to exit') });
497
} else {
498
pullRequestModel = pr;
499
}
500
501
if (!pullRequestModel) {
502
return;
503
}
504
505
const fromDescriptionPage = pr instanceof PullRequestModel;
506
507
return vscode.window.withProgress(
508
{
509
location: vscode.ProgressLocation.SourceControl,
510
title: vscode.l10n.t('Exiting Pull Request'),
511
},
512
async () => {
513
const branch = await pullRequestModel!.githubRepository.getDefaultBranch();
514
const manager = reposManager.getManagerForIssueModel(pullRequestModel);
515
if (manager) {
516
const prBranch = manager.repository.state.HEAD?.name;
517
await manager.checkoutDefaultBranch(branch);
518
if (prBranch) {
519
await manager.cleanupAfterPullRequest(prBranch, pullRequestModel!);
520
}
521
}
522
},
523
);
524
}),
525
);
526
527
context.subscriptions.push(
528
vscode.commands.registerCommand('pr.merge', async (pr?: PRNode) => {
529
const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel);
530
if (!folderManager) {
531
return;
532
}
533
const pullRequest = ensurePR(folderManager, pr);
534
// TODO check is codespaces
535
536
const isCrossRepository =
537
pullRequest.base &&
538
pullRequest.head &&
539
!pullRequest.base.repositoryCloneUrl.equals(pullRequest.head.repositoryCloneUrl);
540
541
const showMergeOnGitHub = isCrossRepository && isInCodespaces();
542
if (showMergeOnGitHub) {
543
return openPullRequestOnGitHub(pullRequest, telemetry);
544
}
545
546
const yes = vscode.l10n.t('Yes');
547
return vscode.window
548
.showWarningMessage(
549
vscode.l10n.t('Are you sure you want to merge this pull request on GitHub?'),
550
{ modal: true },
551
yes,
552
)
553
.then(async value => {
554
let newPR;
555
if (value === yes) {
556
try {
557
newPR = await folderManager.mergePullRequest(pullRequest);
558
return newPR;
559
} catch (e) {
560
vscode.window.showErrorMessage(`Unable to merge pull request. ${formatError(e)}`);
561
return newPR;
562
}
563
}
564
});
565
}),
566
);
567
568
context.subscriptions.push(
569
vscode.commands.registerCommand('pr.readyForReview', async (pr?: PRNode) => {
570
const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel);
571
if (!folderManager) {
572
return;
573
}
574
const pullRequest = ensurePR(folderManager, pr);
575
const yes = vscode.l10n.t('Yes');
576
return vscode.window
577
.showWarningMessage(
578
vscode.l10n.t('Are you sure you want to mark this pull request as ready to review on GitHub?'),
579
{ modal: true },
580
yes,
581
)
582
.then(async value => {
583
let isDraft;
584
if (value === yes) {
585
try {
586
isDraft = await pullRequest.setReadyForReview();
587
vscode.commands.executeCommand('pr.refreshList');
588
return isDraft;
589
} catch (e) {
590
vscode.window.showErrorMessage(
591
`Unable to mark pull request as ready to review. ${formatError(e)}`,
592
);
593
return isDraft;
594
}
595
}
596
});
597
}),
598
);
599
600
context.subscriptions.push(
601
vscode.commands.registerCommand('pr.close', async (pr?: PRNode | PullRequestModel, message?: string) => {
602
let pullRequestModel: PullRequestModel | undefined;
603
if (pr) {
604
pullRequestModel = pr instanceof PullRequestModel ? pr : pr.pullRequestModel;
605
} else {
606
const activePullRequests: PullRequestModel[] = reposManager.folderManagers
607
.map(folderManager => folderManager.activePullRequest!)
608
.filter(activePR => !!activePR);
609
pullRequestModel = await chooseItem<PullRequestModel>(
610
activePullRequests,
611
itemValue => `${itemValue.number}: ${itemValue.title}`,
612
{ placeHolder: vscode.l10n.t('Pull request to close') },
613
);
614
}
615
if (!pullRequestModel) {
616
return;
617
}
618
const pullRequest: PullRequestModel = pullRequestModel;
619
const yes = vscode.l10n.t('Yes');
620
return vscode.window
621
.showWarningMessage(
622
vscode.l10n.t('Are you sure you want to close this pull request on GitHub? This will close the pull request without merging.'),
623
{ modal: true },
624
yes,
625
vscode.l10n.t('No'),
626
)
627
.then(async value => {
628
if (value === yes) {
629
try {
630
let newComment: IComment | undefined = undefined;
631
if (message) {
632
newComment = await pullRequest.createIssueComment(message);
633
}
634
635
const newPR = await pullRequest.close();
636
vscode.commands.executeCommand('pr.refreshList');
637
_onDidUpdatePR.fire(newPR);
638
return newComment;
639
} catch (e) {
640
vscode.window.showErrorMessage(`Unable to close pull request. ${formatError(e)}`);
641
_onDidUpdatePR.fire();
642
}
643
}
644
645
_onDidUpdatePR.fire();
646
});
647
}),
648
);
649
650
context.subscriptions.push(
651
vscode.commands.registerCommand('pr.dismissNotification', node => {
652
if (node instanceof PRNode) {
653
tree.notificationProvider.markPrNotificationsAsRead(node.pullRequestModel).then(
654
() => tree.refresh(node)
655
);
656
657
}
658
}),
659
);
660
661
context.subscriptions.push(
662
vscode.commands.registerCommand(
663
'pr.openDescription',
664
async (argument: DescriptionNode | PullRequestModel | undefined) => {
665
let pullRequestModel: PullRequestModel | undefined;
666
if (!argument) {
667
const activePullRequests: PullRequestModel[] = reposManager.folderManagers
668
.map(manager => manager.activePullRequest!)
669
.filter(activePR => !!activePR);
670
if (activePullRequests.length >= 1) {
671
pullRequestModel = await chooseItem<PullRequestModel>(
672
activePullRequests,
673
itemValue => itemValue.title,
674
);
675
}
676
} else {
677
pullRequestModel = argument instanceof DescriptionNode ? argument.pullRequestModel : argument;
678
}
679
680
if (!pullRequestModel) {
681
Logger.appendLine('No pull request found.');
682
return;
683
}
684
685
const folderManager = reposManager.getManagerForIssueModel(pullRequestModel);
686
if (!folderManager) {
687
return;
688
}
689
690
let descriptionNode: DescriptionNode | undefined;
691
if (argument instanceof DescriptionNode) {
692
descriptionNode = argument;
693
} else {
694
const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);
695
if (!reviewManager) {
696
return;
697
}
698
699
descriptionNode = reviewManager.changesInPrDataProvider.getDescriptionNode(folderManager);
700
}
701
702
await openDescription(context, telemetry, pullRequestModel, descriptionNode, folderManager, tree.notificationProvider);
703
},
704
),
705
);
706
707
context.subscriptions.push(
708
vscode.commands.registerCommand('pr.refreshDescription', async () => {
709
if (PullRequestOverviewPanel.currentPanel) {
710
PullRequestOverviewPanel.refresh();
711
}
712
}),
713
);
714
715
context.subscriptions.push(
716
vscode.commands.registerCommand('pr.openDescriptionToTheSide', async (descriptionNode: DescriptionNode) => {
717
const folderManager = reposManager.getManagerForIssueModel(descriptionNode.pullRequestModel);
718
if (!folderManager) {
719
return;
720
}
721
const pr = descriptionNode.pullRequestModel;
722
const pullRequest = ensurePR(folderManager, pr);
723
descriptionNode.reveal(descriptionNode, { select: true, focus: true });
724
// Create and show a new webview
725
PullRequestOverviewPanel.createOrShow(context.extensionUri, folderManager, pullRequest, true);
726
727
}),
728
);
729
730
context.subscriptions.push(
731
vscode.commands.registerCommand('pr.showDiffSinceLastReview', async (descriptionNode: DescriptionNode) => {
732
descriptionNode.pullRequestModel.showChangesSinceReview = true;
733
}),
734
);
735
736
context.subscriptions.push(
737
vscode.commands.registerCommand('pr.showDiffAll', async (descriptionNode: DescriptionNode) => {
738
descriptionNode.pullRequestModel.showChangesSinceReview = false;
739
}),
740
);
741
742
context.subscriptions.push(
743
vscode.commands.registerCommand('pr.signin', async () => {
744
await reposManager.authenticate();
745
}),
746
);
747
748
context.subscriptions.push(
749
vscode.commands.registerCommand('pr.signinNoEnterprise', async () => {
750
await reposManager.authenticate(false);
751
}),
752
);
753
754
context.subscriptions.push(
755
vscode.commands.registerCommand('pr.signinenterprise', async () => {
756
await reposManager.authenticate(true);
757
}),
758
);
759
760
context.subscriptions.push(
761
vscode.commands.registerCommand('pr.deleteLocalBranchesNRemotes', async () => {
762
for (const folderManager of reposManager.folderManagers) {
763
await folderManager.deleteLocalBranchesNRemotes();
764
}
765
}),
766
);
767
768
context.subscriptions.push(
769
vscode.commands.registerCommand('pr.signinAndRefreshList', async () => {
770
if (await reposManager.authenticate()) {
771
vscode.commands.executeCommand('pr.refreshList');
772
}
773
}),
774
);
775
776
context.subscriptions.push(
777
vscode.commands.registerCommand('pr.configureRemotes', async () => {
778
return vscode.commands.executeCommand('workbench.action.openSettings', `@ext:${EXTENSION_ID} remotes`);
779
}),
780
);
781
782
context.subscriptions.push(
783
vscode.commands.registerCommand('pr.startReview', async (reply: CommentReply) => {
784
const handler = resolveCommentHandler(reply.thread);
785
786
if (handler) {
787
handler.startReview(reply.thread, reply.text);
788
}
789
}),
790
);
791
792
context.subscriptions.push(
793
vscode.commands.registerCommand('pr.openReview', async (thread: GHPRCommentThread) => {
794
const handler = resolveCommentHandler(thread);
795
796
if (handler) {
797
await handler.openReview(thread);
798
}
799
}),
800
);
801
802
function threadAndText(commentLike: CommentReply | GHPRCommentThread | GHPRComment | any): { thread: GHPRCommentThread, text: string } {
803
let thread: GHPRCommentThread;
804
let text: string = '';
805
if (commentLike instanceof GHPRComment) {
806
thread = commentLike.parent;
807
} else if (CommentReply.is(commentLike)) {
808
thread = commentLike.thread;
809
} else if (GHPRCommentThread.is(commentLike?.thread)) {
810
thread = commentLike.thread;
811
} else {
812
thread = commentLike;
813
}
814
return { thread, text };
815
}
816
817
context.subscriptions.push(
818
vscode.commands.registerCommand('pr.resolveReviewThread', async (commentLike: CommentReply | GHPRCommentThread | GHPRComment) => {
819
const { thread, text } = threadAndText(commentLike);
820
const handler = resolveCommentHandler(thread);
821
822
if (handler) {
823
await handler.resolveReviewThread(thread, text);
824
}
825
}),
826
);
827
828
context.subscriptions.push(
829
vscode.commands.registerCommand('pr.unresolveReviewThread', async (commentLike: CommentReply | GHPRCommentThread | GHPRComment) => {
830
const { thread, text } = threadAndText(commentLike);
831
832
const handler = resolveCommentHandler(thread);
833
834
if (handler) {
835
await handler.unresolveReviewThread(thread, text);
836
}
837
}),
838
);
839
840
context.subscriptions.push(
841
vscode.commands.registerCommand('pr.createComment', async (reply: CommentReply) => {
842
const handler = resolveCommentHandler(reply.thread);
843
844
if (handler) {
845
handler.createOrReplyComment(reply.thread, reply.text, false);
846
}
847
}),
848
);
849
850
context.subscriptions.push(
851
vscode.commands.registerCommand('pr.createSingleComment', async (reply: CommentReply) => {
852
const handler = resolveCommentHandler(reply.thread);
853
854
if (handler) {
855
handler.createOrReplyComment(reply.thread, reply.text, true);
856
}
857
}),
858
);
859
860
context.subscriptions.push(
861
vscode.commands.registerCommand('pr.makeSuggestion', async (reply: CommentReply | GHPRComment) => {
862
const thread = reply instanceof GHPRComment ? reply.parent : reply.thread;
863
const commentEditor = vscode.window.activeTextEditor?.document.uri.scheme === Schemes.Comment ? vscode.window.activeTextEditor
864
: vscode.window.visibleTextEditors.find(visible => (visible.document.uri.scheme === Schemes.Comment) && (visible.document.uri.query === ''));
865
if (!commentEditor) {
866
Logger.error('No comment editor visible for making a suggestion.');
867
vscode.window.showErrorMessage(vscode.l10n.t('No available comment editor to make a suggestion in.'));
868
return;
869
}
870
const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === thread.uri.toString());
871
const contents = editor?.document.getText(new vscode.Range(thread.range.start.line, 0, thread.range.end.line, editor.document.lineAt(thread.range.end.line).text.length));
872
return commentEditor.edit((editBuilder) => {
873
editBuilder.insert(commentEditor.selection.end, `
874
\`\`\`suggestion
875
${contents}
876
\`\`\``);
877
});
878
})
879
);
880
881
context.subscriptions.push(
882
vscode.commands.registerCommand('pr.editComment', async (comment: GHPRComment | TemporaryComment) => {
883
comment.startEdit();
884
}),
885
);
886
887
context.subscriptions.push(
888
vscode.commands.registerCommand('pr.editQuery', (query: CategoryTreeNode) => {
889
return query.editQuery();
890
}),
891
);
892
893
context.subscriptions.push(
894
vscode.commands.registerCommand('pr.cancelEditComment', async (comment: GHPRComment | TemporaryComment) => {
895
comment.cancelEdit();
896
}),
897
);
898
899
context.subscriptions.push(
900
vscode.commands.registerCommand('pr.saveComment', async (comment: GHPRComment | TemporaryComment) => {
901
const handler = resolveCommentHandler(comment.parent);
902
903
if (handler) {
904
await handler.editComment(comment.parent, comment);
905
}
906
}),
907
);
908
909
context.subscriptions.push(
910
vscode.commands.registerCommand('pr.deleteComment', async (comment: GHPRComment | TemporaryComment) => {
911
const deleteOption = vscode.l10n.t('Delete');
912
const shouldDelete = await vscode.window.showWarningMessage(vscode.l10n.t('Delete comment?'), { modal: true }, deleteOption);
913
914
if (shouldDelete === deleteOption) {
915
const handler = resolveCommentHandler(comment.parent);
916
917
if (handler) {
918
await handler.deleteComment(comment.parent, comment);
919
}
920
}
921
}),
922
);
923
924
context.subscriptions.push(
925
vscode.commands.registerCommand('review.openFile', (value: GitFileChangeNode | vscode.Uri) => {
926
const command = value instanceof GitFileChangeNode ? value.openFileCommand() : openFileCommand(value);
927
vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
928
}),
929
);
930
931
context.subscriptions.push(
932
vscode.commands.registerCommand('review.openLocalFile', (value: vscode.Uri) => {
933
const { path, rootPath } = fromReviewUri(value.query);
934
const localUri = vscode.Uri.joinPath(vscode.Uri.file(rootPath), path);
935
const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === value.toString());
936
const command = openFileCommand(localUri, editor ? { selection: editor.selection } : undefined);
937
vscode.commands.executeCommand(command.command, ...(command.arguments ?? []));
938
}),
939
);
940
941
context.subscriptions.push(
942
vscode.commands.registerCommand('pr.refreshChanges', _ => {
943
reviewManagers.forEach(reviewManager => {
944
reviewManager.updateComments();
945
PullRequestOverviewPanel.refresh();
946
reviewManager.changesInPrDataProvider.refresh();
947
});
948
}),
949
);
950
951
context.subscriptions.push(
952
vscode.commands.registerCommand('pr.setFileListLayoutAsTree', _ => {
953
vscode.workspace.getConfiguration('githubPullRequests').update('fileListLayout', 'tree', true);
954
}),
955
);
956
957
context.subscriptions.push(
958
vscode.commands.registerCommand('pr.setFileListLayoutAsFlat', _ => {
959
vscode.workspace.getConfiguration('githubPullRequests').update('fileListLayout', 'flat', true);
960
}),
961
);
962
963
context.subscriptions.push(
964
vscode.commands.registerCommand('pr.refreshPullRequest', (prNode: PRNode) => {
965
const folderManager = reposManager.getManagerForIssueModel(prNode.pullRequestModel);
966
if (folderManager && prNode.pullRequestModel.equals(folderManager?.activePullRequest)) {
967
ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager)?.updateComments();
968
}
969
970
PullRequestOverviewPanel.refresh();
971
tree.refresh(prNode);
972
}),
973
);
974
975
context.subscriptions.push(
976
vscode.commands.registerCommand('pr.markFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri | undefined) => {
977
try {
978
if (treeNode === undefined) {
979
// Use the active editor to enable keybindings
980
treeNode = vscode.window.activeTextEditor?.document.uri;
981
}
982
983
if (treeNode instanceof FileChangeNode) {
984
await treeNode.markFileAsViewed();
985
} else if (treeNode) {
986
// When the argument is a uri it came from the editor menu and we should also close the file
987
// Do the close first to improve perceived performance of marking as viewed.
988
const tab = vscode.window.tabGroups.activeTabGroup.activeTab;
989
if (tab) {
990
let compareUri: vscode.Uri | undefined = undefined;
991
if (tab.input instanceof vscode.TabInputTextDiff) {
992
compareUri = tab.input.modified;
993
} else if (tab.input instanceof vscode.TabInputText) {
994
compareUri = tab.input.uri;
995
}
996
if (compareUri && treeNode.toString() === compareUri.toString()) {
997
vscode.window.tabGroups.close(tab);
998
}
999
}
1000
const manager = reposManager.getManagerForFile(treeNode);
1001
await manager?.activePullRequest?.markFileAsViewed(treeNode.path);
1002
manager?.setFileViewedContext();
1003
}
1004
} catch (e) {
1005
vscode.window.showErrorMessage(`Marked file as viewed failed: ${e}`);
1006
}
1007
}),
1008
);
1009
1010
context.subscriptions.push(
1011
vscode.commands.registerCommand('pr.unmarkFileAsViewed', async (treeNode: FileChangeNode | vscode.Uri | undefined) => {
1012
try {
1013
if (treeNode === undefined) {
1014
// Use the active editor to enable keybindings
1015
treeNode = vscode.window.activeTextEditor?.document.uri;
1016
}
1017
1018
if (treeNode instanceof FileChangeNode) {
1019
treeNode.unmarkFileAsViewed();
1020
} else if (treeNode) {
1021
const manager = reposManager.getManagerForFile(treeNode);
1022
await manager?.activePullRequest?.unmarkFileAsViewed(treeNode.path);
1023
manager?.setFileViewedContext();
1024
}
1025
} catch (e) {
1026
vscode.window.showErrorMessage(`Marked file as not viewed failed: ${e}`);
1027
}
1028
}),
1029
);
1030
1031
context.subscriptions.push(
1032
vscode.commands.registerCommand('pr.resetViewedFiles', async () => {
1033
try {
1034
return reposManager.folderManagers.map(async (manager) => {
1035
await manager.activePullRequest?.unmarkAllFilesAsViewed();
1036
manager.setFileViewedContext();
1037
});
1038
} catch (e) {
1039
vscode.window.showErrorMessage(`Marked file as not viewed failed: ${e}`);
1040
}
1041
}),
1042
);
1043
1044
context.subscriptions.push(
1045
vscode.commands.registerCommand('pr.collapseAllComments', () => {
1046
return vscode.commands.executeCommand('workbench.action.collapseAllComments');
1047
}));
1048
1049
context.subscriptions.push(
1050
vscode.commands.registerCommand('pr.copyCommentLink', (comment) => {
1051
if (comment instanceof GHPRComment) {
1052
return vscode.env.clipboard.writeText(comment.rawComment.htmlUrl);
1053
}
1054
}));
1055
1056
context.subscriptions.push(
1057
vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async () => {
1058
const activePullRequests: PullRequestModel[] = reposManager.folderManagers
1059
.map(folderManager => folderManager.activePullRequest!)
1060
.filter(activePR => !!activePR);
1061
const pr = await chooseItem<PullRequestModel>(
1062
activePullRequests,
1063
itemValue => `${itemValue.number}: ${itemValue.title}`,
1064
{ placeHolder: vscode.l10n.t('Pull request to create a link for') },
1065
);
1066
if (pr) {
1067
return vscode.env.clipboard.writeText(vscodeDevPrLink(pr));
1068
}
1069
}));
1070
1071
context.subscriptions.push(
1072
vscode.commands.registerCommand('pr.checkoutByNumber', async () => {
1073
1074
const githubRepositories: { manager: FolderRepositoryManager, repo: GitHubRepository }[] = [];
1075
reposManager.folderManagers.forEach(manager => {
1076
githubRepositories.push(...(manager.gitHubRepositories.map(repo => { return { manager, repo }; })));
1077
});
1078
const githubRepo = await chooseItem<{ manager: FolderRepositoryManager, repo: GitHubRepository }>(
1079
githubRepositories,
1080
itemValue => `${itemValue.repo.remote.owner}/${itemValue.repo.remote.repositoryName}`,
1081
{ placeHolder: vscode.l10n.t('Which GitHub repository do you want to checkout the pull request from?') }
1082
);
1083
if (!githubRepo) {
1084
return;
1085
}
1086
const prNumberMatcher = /^#?(\d*)$/;
1087
const prNumber = await vscode.window.showInputBox({
1088
ignoreFocusOut: true, prompt: vscode.l10n.t('Enter the pull request number'),
1089
validateInput: (input: string) => {
1090
const matches = input.match(prNumberMatcher);
1091
if (!matches || (matches.length !== 2) || Number.isNaN(Number(matches[1]))) {
1092
return vscode.l10n.t('Value must be a number');
1093
}
1094
return undefined;
1095
}
1096
});
1097
if ((prNumber === undefined) || prNumber === '#') {
1098
return;
1099
}
1100
const prModel = await githubRepo.manager.fetchById(githubRepo.repo, Number(prNumber.match(prNumberMatcher)![1]));
1101
if (prModel) {
1102
return ReviewManager.getReviewManagerForFolderManager(reviewManagers, githubRepo.manager)?.switch(prModel);
1103
}
1104
}));
1105
1106
function chooseRepoToOpen() {
1107
const githubRepositories: GitHubRepository[] = [];
1108
reposManager.folderManagers.forEach(manager => {
1109
githubRepositories.push(...(manager.gitHubRepositories));
1110
});
1111
return chooseItem<GitHubRepository>(
1112
githubRepositories,
1113
itemValue => `${itemValue.remote.owner}/${itemValue.remote.repositoryName}`,
1114
{ placeHolder: vscode.l10n.t('Which GitHub repository do you want to open?') }
1115
);
1116
}
1117
context.subscriptions.push(
1118
vscode.commands.registerCommand('pr.openPullsWebsite', async () => {
1119
const githubRepo = await chooseRepoToOpen();
1120
if (githubRepo) {
1121
vscode.env.openExternal(getPullsUrl(githubRepo));
1122
}
1123
}));
1124
context.subscriptions.push(
1125
vscode.commands.registerCommand('issues.openIssuesWebsite', async () => {
1126
const githubRepo = await chooseRepoToOpen();
1127
if (githubRepo) {
1128
vscode.env.openExternal(getIssuesUrl(githubRepo));
1129
}
1130
}));
1131
1132
context.subscriptions.push(
1133
vscode.commands.registerCommand('pr.applySuggestion', async (comment: GHPRComment) => {
1134
1135
const handler = resolveCommentHandler(comment.parent);
1136
1137
if (handler instanceof ReviewCommentController) {
1138
handler.applySuggestion(comment);
1139
}
1140
}));
1141
1142
function goToNextPrevDiff(diffs: vscode.LineChange[], next: boolean) {
1143
const tab = vscode.window.tabGroups.activeTabGroup.activeTab;
1144
const input = tab?.input;
1145
if (!(input instanceof vscode.TabInputTextDiff)) {
1146
return vscode.window.showErrorMessage(vscode.l10n.t('Current editor isn\'t a diff editor.'));
1147
}
1148
1149
const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.toString() === input.modified.toString());
1150
if (!editor) {
1151
return vscode.window.showErrorMessage(vscode.l10n.t('Unexpectedly unable to find the current modified editor.'));
1152
}
1153
1154
const editorUri = editor.document.uri;
1155
if (input.original.scheme !== Schemes.Review) {
1156
return vscode.window.showErrorMessage(vscode.l10n.t('Current file isn\'t a pull request diff.'));
1157
}
1158
1159
// Find the next diff in the current file to scroll to
1160
const visibleRange = editor.visibleRanges[0];
1161
const iterateThroughDiffs = next ? diffs : diffs.reverse();
1162
for (const diff of iterateThroughDiffs) {
1163
const practicalModifiedEndLineNumber = (diff.modifiedEndLineNumber > diff.modifiedStartLineNumber) ? diff.modifiedEndLineNumber : diff.modifiedStartLineNumber as number + 1;
1164
const diffRange = new vscode.Range(diff.modifiedStartLineNumber ? diff.modifiedStartLineNumber - 1 : diff.modifiedStartLineNumber, 0, practicalModifiedEndLineNumber, 0);
1165
if (next && (visibleRange.end.line < practicalModifiedEndLineNumber) && (visibleRange.end.line !== (editor.document.lineCount - 1))) {
1166
editor.revealRange(diffRange);
1167
return;
1168
} else if (!next && (visibleRange.start.line > diff.modifiedStartLineNumber) && (visibleRange.start.line !== 0)) {
1169
editor.revealRange(diffRange);
1170
return;
1171
}
1172
}
1173
1174
// There is no new range to reveal, time to go to the next file.
1175
const folderManager = reposManager.getManagerForFile(editorUri);
1176
if (!folderManager) {
1177
return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find a repository for pull request.'));
1178
}
1179
1180
const reviewManager = ReviewManager.getReviewManagerForFolderManager(reviewManagers, folderManager);
1181
if (!reviewManager) {
1182
return vscode.window.showErrorMessage(vscode.l10n.t('Cannot find active pull request.'));
1183
}
1184
1185
if (!reviewManager.reviewModel.hasLocalFileChanges || (reviewManager.reviewModel.localFileChanges.length === 0)) {
1186
return vscode.window.showWarningMessage(vscode.l10n.t('Pull request data is not yet complete, please try again in a moment.'));
1187
}
1188
1189
for (let i = 0; i < reviewManager.reviewModel.localFileChanges.length; i++) {
1190
const index = next ? i : reviewManager.reviewModel.localFileChanges.length - 1;
1191
const localFileChange = reviewManager.reviewModel.localFileChanges[index];
1192
if (localFileChange.changeModel.filePath.toString() === editorUri.toString()) {
1193
const nextIndex = next ? index + 1 : index - 1;
1194
if (reviewManager.reviewModel.localFileChanges.length > nextIndex) {
1195
return reviewManager.reviewModel.localFileChanges[nextIndex].openDiff(folderManager);
1196
}
1197
}
1198
}
1199
// No further files in PR.
1200
const goInCircle = next ? vscode.l10n.t('Go to first diff') : vscode.l10n.t('Go to last diff');
1201
return vscode.window.showInformationMessage(vscode.l10n.t('There are no more diffs in this pull request.'), goInCircle).then(result => {
1202
if (result === goInCircle) {
1203
return reviewManager.reviewModel.localFileChanges[next ? 0 : reviewManager.reviewModel.localFileChanges.length - 1].openDiff(folderManager);
1204
}
1205
});
1206
}
1207
1208
context.subscriptions.push(
1209
vscode.commands.registerDiffInformationCommand('pr.goToNextDiffInPr', async (diffs: vscode.LineChange[]) => {
1210
goToNextPrevDiff(diffs, true);
1211
}));
1212
context.subscriptions.push(
1213
vscode.commands.registerDiffInformationCommand('pr.goToPreviousDiffInPr', async (diffs: vscode.LineChange[]) => {
1214
goToNextPrevDiff(diffs, false);
1215
}));
1216
}
1217
1218