Skip to content

Commit 4bdfe90

Browse files
alexr00Copilot
andauthored
Fix issue view and other polish from linked issue change (#8718)
* Fix issue view and other polish from linked issue change * CCR feedback Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
1 parent c9ecbbe commit 4bdfe90

10 files changed

Lines changed: 83 additions & 29 deletions

File tree

src/github/graphql.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,7 @@ export interface PullRequest extends Issue {
770770
deletions?: number;
771771
closingIssuesReferences?: {
772772
nodes: {
773-
id: number,
773+
databaseId: number,
774774
title: string,
775775
number: number,
776776
state: 'CLOSED' | 'OPEN',

src/github/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export interface PullRequest extends Issue {
250250
mergeCommitMeta?: { title: string, description: string };
251251
squashCommitMeta?: { title: string, description: string };
252252
suggestedReviewers?: ISuggestedReviewer[];
253-
closingIssues?: IssueReference[]
253+
closingIssues?: IssueReference[];
254254
hasComments?: boolean;
255255
additions?: number;
256256
deletions?: number;

src/github/pullRequestOverview.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { IssueOverviewPanel, panelKey } from './issueOverview';
2727
import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel';
2828
import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon';
2929
import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks';
30-
import { ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, parseReviewers, processDiffLinks, processPermalinks } from './utils';
30+
import { getEnterpriseUri, getIssueOrURLExpression, parseIssueExpressionOutput, parseReviewers, processDiffLinks, processPermalinks } from './utils';
3131
import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views';
3232
import { debounce } from '../common/async';
3333
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
@@ -460,13 +460,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
460460
revertable: pullRequest.state === GithubItemStateEnum.Merged,
461461
isCopilotOnMyBehalf: await isCopilotOnMyBehalf(pullRequest, currentUser, coAuthors),
462462
generateDescriptionTitle: this.getGenerateDescriptionTitle(),
463-
closingIssues: await Promise.all((pullRequest.closingIssues ?? []).map(async issue => {
464-
const parsed = parseIssueExpressionOutput(issue.url.match(ISSUE_OR_URL_EXPRESSION));
465-
const owner = parsed?.owner ?? pullRequest.remote.owner;
466-
const repo = parsed?.name ?? pullRequest.remote.repositoryName;
467-
const webviewUri = await toOpenIssueWebviewUri({ owner, repo, issueNumber: issue.number });
468-
return { ...issue, url: webviewUri.toString() };
469-
})),
463+
closingIssues: await (async () => {
464+
const enterpriseUri = pullRequest.remote.isEnterprise ? getEnterpriseUri() : undefined;
465+
const issueOrUrlExpression = getIssueOrURLExpression(enterpriseUri);
466+
return Promise.all((pullRequest.closingIssues ?? []).map(async issue => {
467+
const parsed = parseIssueExpressionOutput(issue.url.match(issueOrUrlExpression));
468+
const owner = parsed?.owner ?? pullRequest.remote.owner;
469+
const repo = parsed?.name ?? pullRequest.remote.repositoryName;
470+
const webviewUri = await toOpenIssueWebviewUri({ owner, repo, issueNumber: issue.number });
471+
return { ...issue, url: webviewUri.toString() };
472+
}));
473+
})(),
470474
};
471475
this._postMessage({
472476
command: 'pr.initialize',

src/github/queries.gql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ fragment PullRequestFragment on PullRequest {
226226
}
227227
closingIssuesReferences(first: 50) {
228228
nodes {
229-
id
229+
databaseId
230230
number
231231
title
232232
state

src/github/queriesExtra.gql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ fragment PullRequestFragment on PullRequest {
239239
}
240240
closingIssuesReferences(first: 50) {
241241
nodes {
242-
id
242+
databaseId
243243
number
244244
title
245245
state

src/github/queriesLimited.gql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ fragment PullRequestFragment on PullRequest {
208208
}
209209
closingIssuesReferences(first: 50) {
210210
nodes {
211-
id
211+
databaseId
212212
number
213213
title
214214
state

src/github/utils.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
IMilestone,
2323
IProjectItem,
2424
Issue,
25+
IssueReference,
2526
ISuggestedReviewer,
2627
ITeam,
2728
MergeMethod,
@@ -60,6 +61,20 @@ import { escapeRegExp, gitHubLabelColor, processDiffLinks as processDiffLinksCor
6061
export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/;
6162
export const ISSUE_OR_URL_EXPRESSION = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+)(#issuecomment\-([0-9]+))?)|(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/;
6263

64+
/**
65+
* Returns an issue/URL regex matching the appropriate GitHub host. When a
66+
* GitHub Enterprise URI is provided, that host is used in addition to
67+
* github.com so that issue URLs from the enterprise instance are recognized.
68+
* The capture group layout matches `ISSUE_OR_URL_EXPRESSION` for use with
69+
* `parseIssueExpressionOutput`.
70+
*/
71+
export function getIssueOrURLExpression(enterpriseUri?: vscode.Uri): RegExp {
72+
const hostPattern = enterpriseUri
73+
? `(?:github\\.com|${escapeRegExp(enterpriseUri.authority)})`
74+
: 'github\\.com';
75+
return new RegExp(`(https?:\\/\\/${hostPattern}\\/(([^\\s]+)\\/([^\\s]+))\\/([^\\s]+\\/)?(issues|pull)\\/([0-9]+)(#issuecomment\\-([0-9]+))?)|(([A-Za-z0-9_.\\-]+)\\/([A-Za-z0-9_.\\-]+))?(#|GH-)([1-9][0-9]*)($|\\b)`);
76+
}
77+
6378
export interface CommentReactionHandler {
6479
toggleReaction(comment: vscode.Comment, reaction: vscode.CommentReaction): Promise<void>;
6580
}
@@ -1070,14 +1085,14 @@ function parseSuggestedReviewers(
10701085
}
10711086

10721087
function parseClosingIssuesReferences(
1073-
closingIssuesReferences: Array<{ id: number, number: number, title: string, state: string, url: string }> | undefined
1074-
): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum, url: string }> {
1088+
closingIssuesReferences: Array<{ databaseId: number, number: number, title: string, state: string, url: string }> | undefined
1089+
): IssueReference[] {
10751090
if (!closingIssuesReferences) {
10761091
return [];
10771092
}
10781093

10791094
return closingIssuesReferences.map(issue => ({
1080-
id: issue.id,
1095+
id: issue.databaseId,
10811096
number: issue.number,
10821097
title: issue.title,
10831098
state: issue.state === 'OPEN' ? GithubItemStateEnum.Open : GithubItemStateEnum.Closed,

src/github/views.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ILabel,
1010
IMilestone,
1111
IProjectItem,
12+
IssueReference,
1213
MergeMethod,
1314
MergeMethodsAvailability,
1415
MergeQueueState,
@@ -28,13 +29,6 @@ export enum ReviewType {
2829
RequestChanges = 'requestChanges',
2930
}
3031

31-
export interface IssueReference {
32-
number: number;
33-
title: string;
34-
state: GithubItemStateEnum;
35-
url: string;
36-
}
37-
3832
export interface DisplayLabel extends ILabel {
3933
displayName: string;
4034
}
@@ -119,7 +113,7 @@ export interface PullRequest extends Issue {
119113
busy?: boolean;
120114
loadingCommit?: string;
121115
generateDescriptionTitle?: string;
122-
closingIssues: IssueReference[];
116+
closingIssues?: IssueReference[];
123117
}
124118

125119
export interface ProjectItemsReply {

src/test/issues/issuesUtils.test.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { default as assert } from 'assert';
7-
import { parseIssueExpressionOutput, ISSUE_OR_URL_EXPRESSION } from '../../github/utils';
7+
import * as vscode from 'vscode';
8+
import { getIssueOrURLExpression, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput } from '../../github/utils';
89

910
describe('Issues utilities', function () {
1011
it('regular expressions', async function () {
@@ -65,4 +66,44 @@ describe('Issues utilities', function () {
6566
assert.strictEqual(prUrlHttpParsed?.name, 'repo');
6667
assert.strictEqual(prUrlHttpParsed?.owner, 'owner');
6768
});
69+
70+
it('getIssueOrURLExpression matches enterprise host URLs', function () {
71+
const enterpriseExpression = getIssueOrURLExpression(vscode.Uri.parse('https://my.ghe.host'));
72+
73+
// Enterprise host URL is matched
74+
const enterpriseIssueUrl = 'https://my.ghe.host/org/repo/issues/123';
75+
const enterpriseIssueParsed = parseIssueExpressionOutput(enterpriseIssueUrl.match(enterpriseExpression));
76+
assert.strictEqual(enterpriseIssueParsed?.issueNumber, 123);
77+
assert.strictEqual(enterpriseIssueParsed?.commentNumber, undefined);
78+
assert.strictEqual(enterpriseIssueParsed?.name, 'repo');
79+
assert.strictEqual(enterpriseIssueParsed?.owner, 'org');
80+
81+
// Enterprise PR URL is matched
82+
const enterprisePrUrl = 'https://my.ghe.host/org/repo/pull/456';
83+
const enterprisePrParsed = parseIssueExpressionOutput(enterprisePrUrl.match(enterpriseExpression));
84+
assert.strictEqual(enterprisePrParsed?.issueNumber, 456);
85+
assert.strictEqual(enterprisePrParsed?.name, 'repo');
86+
assert.strictEqual(enterprisePrParsed?.owner, 'org');
87+
88+
// Enterprise comment URL is matched
89+
const enterpriseCommentUrl = 'https://my.ghe.host/org/repo/issues/789#issuecomment-12345';
90+
const enterpriseCommentParsed = parseIssueExpressionOutput(enterpriseCommentUrl.match(enterpriseExpression));
91+
assert.strictEqual(enterpriseCommentParsed?.issueNumber, 789);
92+
assert.strictEqual(enterpriseCommentParsed?.commentNumber, 12345);
93+
assert.strictEqual(enterpriseCommentParsed?.name, 'repo');
94+
assert.strictEqual(enterpriseCommentParsed?.owner, 'org');
95+
96+
// github.com URLs are still matched when an enterprise URI is provided
97+
const dotComUrl = 'https://github.com/microsoft/vscode/issues/96';
98+
const dotComParsed = parseIssueExpressionOutput(dotComUrl.match(enterpriseExpression));
99+
assert.strictEqual(dotComParsed?.issueNumber, 96);
100+
assert.strictEqual(dotComParsed?.name, 'vscode');
101+
assert.strictEqual(dotComParsed?.owner, 'microsoft');
102+
103+
// Without an enterprise URI, only github.com URLs are matched as full URLs
104+
const defaultExpression = getIssueOrURLExpression();
105+
const enterpriseAgainstDefault = parseIssueExpressionOutput(enterpriseIssueUrl.match(defaultExpression));
106+
// The owner/repo/number should not match the URL form (the alternate `owner/repo#num` form is also not present here).
107+
assert.strictEqual(enterpriseAgainstDefault, undefined);
108+
});
68109
});

webviews/components/sidebar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { closeIcon, copilotIcon, issuescon, passIcon, settingsIcon } from './ico
88
import { Reviewer } from './reviewer';
99
import { COPILOT_LOGINS } from '../../src/common/copilot';
1010
import { gitHubLabelColor } from '../../src/common/utils';
11-
import { GithubItemStateEnum, IAccount, IMilestone, IProjectItem, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
12-
import { ChangeReviewersReply, IssueReference, PullRequest } from '../../src/github/views';
11+
import { GithubItemStateEnum, IAccount, IMilestone, IProjectItem, isITeam, IssueReference, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface';
12+
import { ChangeReviewersReply, PullRequest } from '../../src/github/views';
1313
import PullRequestContext from '../common/context';
1414
import { Label } from '../common/label';
1515
import { AuthorLink, Avatar } from '../components/user';
@@ -53,7 +53,7 @@ function Section({
5353
);
5454
}
5555

56-
export default function Sidebar({ reviewers, labels, closingIssues, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) {
56+
export default function Sidebar({ reviewers, labels, closingIssues = [], hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) {
5757
const {
5858
addReviewers,
5959
addReviewerCopilot,
@@ -276,7 +276,7 @@ export default function Sidebar({ reviewers, labels, closingIssues, hasWritePerm
276276
hasWritePermission={false}
277277
>
278278
{closingIssues.map(issue => (
279-
<IssueItem key={issue.number} issue={issue} />
279+
<IssueItem key={issue.url} issue={issue} />
280280
))}
281281
</Section>
282282
)}

0 commit comments

Comments
 (0)