Skip to content

Commit 1e61bd4

Browse files
authored
Add assign to copilot button (#6815)
1 parent 1d5ce98 commit 1e61bd4

14 files changed

Lines changed: 224 additions & 156 deletions

File tree

resources/icons/copilot.svg

Lines changed: 1 addition & 0 deletions
Loading

resources/icons/link.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/common/comment.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import { IAccount } from '../github/interface';
8+
import { COPILOT_LOGINS } from './copilot';
89
import { DiffHunk } from './diffHunk';
910

1011
export enum DiffSide {
@@ -79,8 +80,5 @@ const COPILOT_AUTHOR = {
7980
postComment: vscode.l10n.t('Copilot is powered by AI, so mistakes are possible. Review output carefully before use.')
8081
};
8182

82-
export const SPECIAL_COMMENT_AUTHORS: { [key: string]: { postComment: string, name: string } } = {
83-
'copilot-pull-request-reviewer': COPILOT_AUTHOR,
84-
'copilot-swe-agent': COPILOT_AUTHOR,
85-
'Copilot': COPILOT_AUTHOR
86-
};
83+
export const COPILOT_ACCOUNTS: { [key: string]: { postComment: string, name: string } } =
84+
Object.fromEntries(COPILOT_LOGINS.map(login => [login, COPILOT_AUTHOR]));

src/common/copilot.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
6+
export const COPILOT_LOGINS = [
7+
'copilot-pull-request-reviewer',
8+
'copilot-swe-agent',
9+
'Copilot'
10+
];

src/github/issueOverview.ts

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import * as vscode from 'vscode';
88
import { openPullRequestOnGitHub } from '../commands';
9-
import { IComment } from '../common/comment';
9+
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
1010
import Logger from '../common/logger';
1111
import { ITelemetry } from '../common/telemetry';
1212
import { CommentEvent, EventType, TimelineEvent } from '../common/timelineEvent';
@@ -151,7 +151,7 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
151151
return isInCodespaces();
152152
}
153153

154-
protected getInitializeContext(issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean): Issue {
154+
protected getInitializeContext(issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean, assignableUsers: IAccount[]): Issue {
155155
const hasWritePermission = repositoryAccess!.hasWritePermission;
156156
const canEdit = hasWritePermission || viewerCanEdit;
157157
const context: Issue = {
@@ -174,43 +174,51 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
174174
milestone: issue.milestone,
175175
assignees: issue.assignees ?? [],
176176
isEnterprise: issue.githubRepository.remote.isEnterprise,
177-
isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark
177+
isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark,
178+
canAssignCopilot: assignableUsers.find(user => COPILOT_ACCOUNTS[user.login]) !== undefined
178179
};
179180

180181
return context;
181182
}
182183

183184
public async updateIssue(issueModel: IssueModel): Promise<void> {
184-
return Promise.all([
185-
this._folderRepositoryManager.resolveIssue(
186-
issueModel.remote.owner,
187-
issueModel.remote.repositoryName,
188-
issueModel.number,
189-
),
190-
issueModel.getIssueTimelineEvents(),
191-
this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(issueModel),
192-
issueModel.canEdit()
193-
])
194-
.then(result => {
195-
const [issue, timelineEvents, repositoryAccess, viewerCanEdit] = result;
196-
if (!issue) {
197-
throw new Error(
198-
`Fail to resolve issue #${issueModel.number} in ${issueModel.remote.owner}/${issueModel.remote.repositoryName}`,
199-
);
200-
}
185+
try {
186+
const [
187+
issue,
188+
timelineEvents,
189+
repositoryAccess,
190+
viewerCanEdit,
191+
assignableUsers
192+
] = await Promise.all([
193+
this._folderRepositoryManager.resolveIssue(
194+
issueModel.remote.owner,
195+
issueModel.remote.repositoryName,
196+
issueModel.number,
197+
),
198+
issueModel.getIssueTimelineEvents(),
199+
this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(issueModel),
200+
issueModel.canEdit(),
201+
this._folderRepositoryManager.getAssignableUsers()
202+
]);
203+
204+
if (!issue) {
205+
throw new Error(
206+
`Fail to resolve issue #${issueModel.number} in ${issueModel.remote.owner}/${issueModel.remote.repositoryName}`,
207+
);
208+
}
201209

202-
this._item = issue as TItem;
203-
this.setPanelTitle(`Issue #${issueModel.number.toString()}`);
210+
this._item = issue as TItem;
211+
this.setPanelTitle(`Issue #${issueModel.number.toString()}`);
204212

205-
Logger.debug('pr.initialize', IssueOverviewPanel.ID);
206-
this._postMessage({
207-
command: 'pr.initialize',
208-
pullrequest: this.getInitializeContext(issue, timelineEvents, repositoryAccess, viewerCanEdit)
209-
});
210-
})
211-
.catch(e => {
212-
vscode.window.showErrorMessage(`Error updating issue description: ${formatError(e)}`);
213+
Logger.debug('pr.initialize', IssueOverviewPanel.ID);
214+
this._postMessage({
215+
command: 'pr.initialize',
216+
pullrequest: this.getInitializeContext(issue, timelineEvents, repositoryAccess, viewerCanEdit, assignableUsers[this._item.remote.remoteName]),
213217
});
218+
219+
} catch (e) {
220+
vscode.window.showErrorMessage(`Error updating issue description: ${formatError(e)}`);
221+
}
214222
}
215223

216224
public async update(foldersManager: FolderRepositoryManager, issueModel: IssueModel): Promise<void> {
@@ -268,6 +276,8 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
268276
return this.removeProject(message);
269277
case 'pr.add-assignee-yourself':
270278
return this.addAssigneeYourself(message);
279+
case 'pr.add-assignee-copilot':
280+
return this.addAssigneeCopilot(message);
271281
case 'pr.copy-prlink':
272282
return this.copyItemLink();
273283
case 'pr.copy-vscodedevlink':
@@ -465,6 +475,24 @@ export class IssueOverviewPanel<TItem extends IssueModel = IssueModel> extends W
465475
}
466476
}
467477

478+
private async addAssigneeCopilot(message: IRequestMessage<void>): Promise<void> {
479+
try {
480+
const copilotUser = (await this._folderRepositoryManager.getAssignableUsers())[this._item.remote.remoteName].find(user => COPILOT_ACCOUNTS[user.login]);
481+
if (copilotUser) {
482+
const newAssignees = (this._item.assignees ?? []).concat(copilotUser);
483+
await this._item.replaceAssignees(newAssignees);
484+
}
485+
const events = await this._item.getIssueTimelineEvents();
486+
const reply: ChangeAssigneesReply = {
487+
assignees: this._item.assignees ?? [],
488+
events
489+
};
490+
this._replyMessage(message, reply);
491+
} catch (e) {
492+
vscode.window.showErrorMessage(formatError(e));
493+
}
494+
}
495+
468496
private async copyItemLink(): Promise<void> {
469497
return vscode.env.clipboard.writeText(this._item.html_url);
470498
}

src/github/pullRequestOverview.ts

Lines changed: 98 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -195,107 +195,106 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
195195
}
196196

197197
private async updatePullRequest(pullRequestModel: PullRequestModel): Promise<void> {
198-
return Promise.all([
199-
this._folderRepositoryManager.resolvePullRequest(
200-
pullRequestModel.remote.owner,
201-
pullRequestModel.remote.repositoryName,
202-
pullRequestModel.number,
203-
),
204-
pullRequestModel.getTimelineEvents(),
205-
this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(pullRequestModel),
206-
pullRequestModel.getStatusChecks(),
207-
pullRequestModel.getReviewRequests(),
208-
this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(pullRequestModel),
209-
this._folderRepositoryManager.getBranchNameForPullRequest(pullRequestModel),
210-
this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository),
211-
pullRequestModel.canEdit(),
212-
this._folderRepositoryManager.getOrgTeamsCount(pullRequestModel.githubRepository),
213-
this._folderRepositoryManager.mergeQueueMethodForBranch(pullRequestModel.base.ref, pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName),
214-
this._folderRepositoryManager.isHeadUpToDateWithBase(pullRequestModel),
215-
pullRequestModel.getMergeability(),
216-
this._folderRepositoryManager.getPreferredEmail(pullRequestModel)])
217-
.then(result => {
218-
const [
219-
pullRequest,
220-
timelineEvents,
221-
defaultBranch,
222-
status,
223-
requestedReviewers,
224-
repositoryAccess,
225-
branchInfo,
226-
currentUser,
227-
viewerCanEdit,
228-
orgTeamsCount,
229-
mergeQueueMethod,
230-
isBranchUpToDateWithBase,
231-
mergeability,
232-
emailForCommit,
233-
] = result;
234-
if (!pullRequest) {
235-
throw new Error(
236-
`Fail to resolve Pull Request #${pullRequestModel.number} in ${pullRequestModel.remote.owner}/${pullRequestModel.remote.repositoryName}`,
237-
);
238-
}
198+
try {
199+
const [
200+
pullRequest,
201+
timelineEvents,
202+
defaultBranch,
203+
status,
204+
requestedReviewers,
205+
repositoryAccess,
206+
branchInfo,
207+
currentUser,
208+
viewerCanEdit,
209+
orgTeamsCount,
210+
mergeQueueMethod,
211+
isBranchUpToDateWithBase,
212+
mergeability,
213+
emailForCommit,
214+
] = await Promise.all([
215+
this._folderRepositoryManager.resolvePullRequest(
216+
pullRequestModel.remote.owner,
217+
pullRequestModel.remote.repositoryName,
218+
pullRequestModel.number,
219+
),
220+
pullRequestModel.getTimelineEvents(),
221+
this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(pullRequestModel),
222+
pullRequestModel.getStatusChecks(),
223+
pullRequestModel.getReviewRequests(),
224+
this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(pullRequestModel),
225+
this._folderRepositoryManager.getBranchNameForPullRequest(pullRequestModel),
226+
this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository),
227+
pullRequestModel.canEdit(),
228+
this._folderRepositoryManager.getOrgTeamsCount(pullRequestModel.githubRepository),
229+
this._folderRepositoryManager.mergeQueueMethodForBranch(pullRequestModel.base.ref, pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName),
230+
this._folderRepositoryManager.isHeadUpToDateWithBase(pullRequestModel),
231+
pullRequestModel.getMergeability(),
232+
this._folderRepositoryManager.getPreferredEmail(pullRequestModel),
233+
]);
234+
if (!pullRequest) {
235+
throw new Error(
236+
`Fail to resolve Pull Request #${pullRequestModel.number} in ${pullRequestModel.remote.owner}/${pullRequestModel.remote.repositoryName}`,
237+
);
238+
}
239239

240-
this._item = pullRequest;
241-
this.registerPrListeners();
242-
this._repositoryDefaultBranch = defaultBranch!;
243-
this._teamsCount = orgTeamsCount;
244-
this.setPanelTitle(`Pull Request #${pullRequestModel.number.toString()}`);
245-
246-
const isCurrentlyCheckedOut = pullRequestModel.equals(this._folderRepositoryManager.activePullRequest);
247-
const mergeMethodsAvailability = repositoryAccess!.mergeMethodsAvailability;
248-
249-
const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability);
250-
this._existingReviewers = parseReviewers(requestedReviewers!, timelineEvents!, pullRequest.author);
251-
252-
const isUpdateBranchWithGitHubEnabled: boolean = this.isUpdateBranchWithGitHubEnabled();
253-
const reviewState = this.getCurrentUserReviewState(this._existingReviewers, currentUser);
254-
255-
Logger.debug('pr.initialize', PullRequestOverviewPanel.ID);
256-
const baseContext = this.getInitializeContext(pullRequest, timelineEvents, repositoryAccess, viewerCanEdit);
257-
258-
const context: Partial<PullRequest> = {
259-
...baseContext,
260-
isCurrentlyCheckedOut: isCurrentlyCheckedOut,
261-
isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted,
262-
base: pullRequest.base.label,
263-
isRemoteHeadDeleted: pullRequest.isRemoteHeadDeleted,
264-
isLocalHeadDeleted: !branchInfo,
265-
head: pullRequest.head?.label ?? '',
266-
repositoryDefaultBranch: defaultBranch,
267-
status: status[0],
268-
reviewRequirement: status[1],
269-
canUpdateBranch: pullRequest.item.viewerCanUpdate && !isBranchUpToDateWithBase && isUpdateBranchWithGitHubEnabled,
270-
mergeable: mergeability.mergeability,
271-
reviewers: this._existingReviewers,
272-
isDraft: pullRequest.isDraft,
273-
mergeMethodsAvailability,
274-
defaultMergeMethod,
275-
autoMerge: pullRequest.autoMerge,
276-
allowAutoMerge: pullRequest.allowAutoMerge,
277-
autoMergeMethod: pullRequest.autoMergeMethod,
278-
mergeQueueMethod: mergeQueueMethod,
279-
mergeQueueEntry: pullRequest.mergeQueueEntry,
280-
mergeCommitMeta: pullRequest.mergeCommitMeta,
281-
squashCommitMeta: pullRequest.squashCommitMeta,
282-
isIssue: false,
283-
emailForCommit,
284-
isAuthor: currentUser.login === pullRequest.author.login,
285-
currentUserReviewState: reviewState,
286-
revertable: pullRequest.state === GithubItemStateEnum.Merged
287-
};
288-
this._postMessage({
289-
command: 'pr.initialize',
290-
pullrequest: context
291-
});
292-
if (pullRequest.isResolved()) {
293-
this._folderRepositoryManager.checkBranchUpToDate(pullRequest, true);
294-
}
295-
})
296-
.catch(e => {
297-
vscode.window.showErrorMessage(`Error updating pull request description: ${formatError(e)}`);
240+
this._item = pullRequest;
241+
this.registerPrListeners();
242+
this._repositoryDefaultBranch = defaultBranch!;
243+
this._teamsCount = orgTeamsCount;
244+
this.setPanelTitle(`Pull Request #${pullRequestModel.number.toString()}`);
245+
246+
const isCurrentlyCheckedOut = pullRequestModel.equals(this._folderRepositoryManager.activePullRequest);
247+
const mergeMethodsAvailability = repositoryAccess!.mergeMethodsAvailability;
248+
249+
const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability);
250+
this._existingReviewers = parseReviewers(requestedReviewers!, timelineEvents!, pullRequest.author);
251+
252+
const isUpdateBranchWithGitHubEnabled: boolean = this.isUpdateBranchWithGitHubEnabled();
253+
const reviewState = this.getCurrentUserReviewState(this._existingReviewers, currentUser);
254+
255+
Logger.debug('pr.initialize', PullRequestOverviewPanel.ID);
256+
const baseContext = this.getInitializeContext(pullRequest, timelineEvents, repositoryAccess, viewerCanEdit, []);
257+
258+
const context: Partial<PullRequest> = {
259+
...baseContext,
260+
isCurrentlyCheckedOut: isCurrentlyCheckedOut,
261+
isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted,
262+
base: pullRequest.base.label,
263+
isRemoteHeadDeleted: pullRequest.isRemoteHeadDeleted,
264+
isLocalHeadDeleted: !branchInfo,
265+
head: pullRequest.head?.label ?? '',
266+
repositoryDefaultBranch: defaultBranch,
267+
status: status[0],
268+
reviewRequirement: status[1],
269+
canUpdateBranch: pullRequest.item.viewerCanUpdate && !isBranchUpToDateWithBase && isUpdateBranchWithGitHubEnabled,
270+
mergeable: mergeability.mergeability,
271+
reviewers: this._existingReviewers,
272+
isDraft: pullRequest.isDraft,
273+
mergeMethodsAvailability,
274+
defaultMergeMethod,
275+
autoMerge: pullRequest.autoMerge,
276+
allowAutoMerge: pullRequest.allowAutoMerge,
277+
autoMergeMethod: pullRequest.autoMergeMethod,
278+
mergeQueueMethod: mergeQueueMethod,
279+
mergeQueueEntry: pullRequest.mergeQueueEntry,
280+
mergeCommitMeta: pullRequest.mergeCommitMeta,
281+
squashCommitMeta: pullRequest.squashCommitMeta,
282+
isIssue: false,
283+
emailForCommit,
284+
isAuthor: currentUser.login === pullRequest.author.login,
285+
currentUserReviewState: reviewState,
286+
revertable: pullRequest.state === GithubItemStateEnum.Merged
287+
};
288+
this._postMessage({
289+
command: 'pr.initialize',
290+
pullrequest: context
298291
});
292+
if (pullRequest.isResolved()) {
293+
this._folderRepositoryManager.checkBranchUpToDate(pullRequest, true);
294+
}
295+
} catch (e) {
296+
vscode.window.showErrorMessage(`Error updating pull request description: ${formatError(e)}`);
297+
}
299298
}
300299

301300
public override async update(

src/github/quickPicks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { Buffer } from 'buffer';
88
import * as vscode from 'vscode';
9-
import { SPECIAL_COMMENT_AUTHORS } from '../common/comment';
9+
import { COPILOT_ACCOUNTS } from '../common/comment';
1010
import Logger from '../common/logger';
1111
import { DataUri } from '../common/uri';
1212
import { formatError } from '../common/utils';
@@ -43,7 +43,7 @@ async function getItems<T extends IAccount | ITeam | ISuggestedReviewer>(context
4343
}
4444

4545
alreadyAssignedItems.push({
46-
label: isTeam(user) ? `${user.org}/${user.slug}` : SPECIAL_COMMENT_AUTHORS[user.login] ? SPECIAL_COMMENT_AUTHORS[user.login].name : user.login,
46+
label: isTeam(user) ? `${user.org}/${user.slug}` : COPILOT_ACCOUNTS[user.login] ? COPILOT_ACCOUNTS[user.login].name : user.login,
4747
description: user.name,
4848
user,
4949
picked,

0 commit comments

Comments
 (0)