diff --git a/src/github/graphql.ts b/src/github/graphql.ts index ea05342343..46cc5869a4 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -768,6 +768,15 @@ export interface PullRequest extends Issue { suggestedReviewers: SuggestedReviewerResponse[]; additions?: number; deletions?: number; + closingIssuesReferences?: { + nodes: { + id: number, + title: string, + number: number, + state: 'CLOSED' | 'OPEN', + url: string, + }[]; + }; } export enum DefaultCommitTitle { diff --git a/src/github/interface.ts b/src/github/interface.ts index 4bf23999c5..eb28ac8a86 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -223,6 +223,14 @@ export interface Issue { reactions: Reaction[]; } +export interface IssueReference { + id: number; + number: number; + title: string; + state: GithubItemStateEnum; + url: string; +} + export interface PullRequest extends Issue { isDraft?: boolean; isRemoteHeadDeleted?: boolean; @@ -242,6 +250,7 @@ export interface PullRequest extends Issue { mergeCommitMeta?: { title: string, description: string }; squashCommitMeta?: { title: string, description: string }; suggestedReviewers?: ISuggestedReviewer[]; + closingIssues?: IssueReference[] hasComments?: boolean; additions?: number; deletions?: number; diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 74b7be8c69..9c0a0a5b08 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -54,6 +54,7 @@ import { IGitTreeItem, IRawFileChange, IRawFileContent, + IssueReference, ISuggestedReviewer, ITeam, MergeMethod, @@ -137,6 +138,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public conflicts?: string[]; public suggestedReviewers?: ISuggestedReviewer[]; public hasChangesSinceLastReview?: boolean; + public closingIssues: IssueReference[] = []; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; private _onDidChangePendingReviewState: vscode.EventEmitter = this._register(new vscode.EventEmitter()); @@ -265,7 +267,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } this.suggestedReviewers = item.suggestedReviewers; - + this.closingIssues = item.closingIssues ?? []; if (item.isRemoteHeadDeleted != null) { this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; } diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 3abb26ada6..4ced752d95 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -27,7 +27,7 @@ import { IssueOverviewPanel, panelKey } from './issueOverview'; import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon'; import { branchPicks, pickEmail, reviewersQuickPick } from './quickPicks'; -import { parseReviewers, processDiffLinks, processPermalinks } from './utils'; +import { ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, parseReviewers, processDiffLinks, processPermalinks } from './utils'; import { CancelCodingAgentReply, ChangeBaseReply, ChangeReviewersReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewAndMergeContext, ReadyForReviewContext, ReviewCommentContext, ReviewType, UnresolvedIdentity } from './views'; import { debounce } from '../common/async'; import { COPILOT_ACCOUNTS, IComment } from '../common/comment'; @@ -38,6 +38,7 @@ import Logger from '../common/logger'; import { CHECKOUT_DEFAULT_BRANCH, CHECKOUT_PULL_REQUEST_BASE_BRANCH, DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, POST_DONE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent'; +import { toOpenIssueWebviewUri } from '../common/uri'; import { asPromise, formatError } from '../common/utils'; import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview'; import { toCheckRunLogUri } from '../view/checkRunLogContentProvider'; @@ -458,7 +459,14 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + const parsed = parseIssueExpressionOutput(issue.url.match(ISSUE_OR_URL_EXPRESSION)); + const owner = parsed?.owner ?? pullRequest.remote.owner; + const repo = parsed?.name ?? pullRequest.remote.repositoryName; + const webviewUri = await toOpenIssueWebviewUri({ owner, repo, issueNumber: issue.number }); + return { ...issue, url: webviewUri.toString() }; + })), }; this._postMessage({ command: 'pr.initialize', diff --git a/src/github/queries.gql b/src/github/queries.gql index b4fe0a69fa..2b55528285 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -224,6 +224,15 @@ fragment PullRequestFragment on PullRequest { mergeCommitMessage mergeCommitTitle } + closingIssuesReferences(first: 50) { + nodes { + id + number + title + state + url + } + } merged mergeable mergeQueueEntry { diff --git a/src/github/queriesExtra.gql b/src/github/queriesExtra.gql index ccb1c9f107..61e49c3cd7 100644 --- a/src/github/queriesExtra.gql +++ b/src/github/queriesExtra.gql @@ -237,6 +237,15 @@ fragment PullRequestFragment on PullRequest { mergeCommitMessage mergeCommitTitle } + closingIssuesReferences(first: 50) { + nodes { + id + number + title + state + url + } + } merged mergeable mergeQueueEntry { diff --git a/src/github/queriesLimited.gql b/src/github/queriesLimited.gql index 9ce0dffa9d..93a489dc34 100644 --- a/src/github/queriesLimited.gql +++ b/src/github/queriesLimited.gql @@ -206,6 +206,15 @@ fragment PullRequestFragment on PullRequest { } url } + closingIssuesReferences(first: 50) { + nodes { + id + number + title + state + url + } + } merged mergeable mergeStateStatus diff --git a/src/github/utils.ts b/src/github/utils.ts index 62035f7119..e096aeb4c2 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -924,6 +924,7 @@ export async function parseGraphQLPullRequest( commentCount: graphQLPullRequest.comments.totalCount, additions: graphQLPullRequest.additions, deletions: graphQLPullRequest.deletions, + closingIssues: parseClosingIssuesReferences(graphQLPullRequest.closingIssuesReferences?.nodes), }; pr.mergeCommitMeta = parseCommitMeta(graphQLPullRequest.baseRepository.mergeCommitTitle, graphQLPullRequest.baseRepository.mergeCommitMessage, pr); pr.squashCommitMeta = parseCommitMeta(graphQLPullRequest.baseRepository.squashMergeCommitTitle, graphQLPullRequest.baseRepository.squashMergeCommitMessage, pr); @@ -1068,6 +1069,22 @@ function parseSuggestedReviewers( return ret.sort(loginComparator); } +function parseClosingIssuesReferences( + closingIssuesReferences: Array<{ id: number, number: number, title: string, state: string, url: string }> | undefined +): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum, url: string }> { + if (!closingIssuesReferences) { + return []; + } + + return closingIssuesReferences.map(issue => ({ + id: issue.id, + number: issue.number, + title: issue.title, + state: issue.state === 'OPEN' ? GithubItemStateEnum.Open : GithubItemStateEnum.Closed, + url: issue.url, + })); +} + /** * Used for case insensitive sort by login */ diff --git a/src/github/views.ts b/src/github/views.ts index 3517c6f6a9..54d765a12c 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -28,6 +28,13 @@ export enum ReviewType { RequestChanges = 'requestChanges', } +export interface IssueReference { + number: number; + title: string; + state: GithubItemStateEnum; + url: string; +} + export interface DisplayLabel extends ILabel { displayName: string; } @@ -112,6 +119,7 @@ export interface PullRequest extends Issue { busy?: boolean; loadingCommit?: string; generateDescriptionTitle?: string; + closingIssues: IssueReference[]; } export interface ProjectItemsReply { diff --git a/webviews/common/common.css b/webviews/common/common.css index 76535d22a4..a00c5310b2 100644 --- a/webviews/common/common.css +++ b/webviews/common/common.css @@ -286,6 +286,35 @@ body img.avatar { fill: var(--vscode-issues-open); } +.section-icon.issue-open svg path { + fill: var(--vscode-issues-open); +} + +.section-icon.issue-closed svg path { + fill: var(--vscode-issues-closed); +} + +.issue-item { + display: flex; + align-items: center; + gap: 6px; + color: var(--vscode-foreground); + text-decoration: none; + overflow: hidden; +} + +.issue-item:hover { + text-decoration: underline; + color: var(--vscode-foreground); +} + +.issue-item-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; +} .reviewer-icons { display: flex; gap: 4px; diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index e8b3459226..e0fc3dd133 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import React, { useContext, useEffect, useRef, useState } from 'react'; -import { closeIcon, copilotIcon, settingsIcon } from './icon'; +import { closeIcon, copilotIcon, issuescon, passIcon, settingsIcon } from './icon'; import { Reviewer } from './reviewer'; import { COPILOT_LOGINS } from '../../src/common/copilot'; import { gitHubLabelColor } from '../../src/common/utils'; -import { IAccount, IMilestone, IProjectItem, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface'; -import { ChangeReviewersReply, PullRequest } from '../../src/github/views'; +import { GithubItemStateEnum, IAccount, IMilestone, IProjectItem, isITeam, reviewerId, reviewerLabel, ReviewState } from '../../src/github/interface'; +import { ChangeReviewersReply, IssueReference, PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; import { Label } from '../common/label'; import { AuthorLink, Avatar } from '../components/user'; @@ -53,7 +53,7 @@ function Section({ ); } -export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) { +export default function Sidebar({ reviewers, labels, closingIssues, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot, canRequestCopilotReview }: PullRequest) { const { addReviewers, addReviewerCopilot, @@ -268,6 +268,18 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue
No milestone
)} + + {closingIssues.length > 0 && ( +
+ {closingIssues.map(issue => ( + + ))} +
+ )} ); } @@ -577,3 +589,16 @@ function ConvertToDraft() { ); } + +function IssueItem({ issue }: { issue: IssueReference }) { + const isOpen = issue.state === GithubItemStateEnum.Open; + return ( + + + {isOpen ? issuescon : passIcon} + + #{issue.number} {issue.title} + + ); +} + diff --git a/webviews/editorWebview/test/builder/pullRequest.ts b/webviews/editorWebview/test/builder/pullRequest.ts index 5355ec2efb..21076a8adb 100644 --- a/webviews/editorWebview/test/builder/pullRequest.ts +++ b/webviews/editorWebview/test/builder/pullRequest.ts @@ -61,6 +61,7 @@ export const PullRequestBuilder = createBuilderClass()({ hasReviewDraft: { default: false }, busy: { default: undefined }, lastReviewType: { default: undefined }, + closingIssues: { default: [] }, canAssignCopilot: { default: false }, canRequestCopilotReview: { default: false }, isCopilotOnMyBehalf: { default: false },