Path: blob/main/extensions/copilot/src/extension/prompts/node/test/fixtures/pullRequestModel.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as buffer from 'buffer';6import equals from 'fast-deep-equal';7import * as path from 'path';8import * as vscode from 'vscode';9import { DiffSide, IComment, IReviewThread, ViewedState } from '../common/comment';10import { parseDiff } from '../common/diffHunk';11import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file';12import { GitHubRef } from '../common/githubRef';13import Logger from '../common/logger';14import { Remote } from '../common/remote';15import { ITelemetry } from '../common/telemetry';16import { ReviewEvent as CommonReviewEvent, EventType, TimelineEvent } from '../common/timelineEvent';17import { resolvePath, toPRUri, toReviewUri } from '../common/uri';18import { formatError } from '../common/utils';19import { OctokitCommon } from './common';20import { FolderRepositoryManager } from './folderRepositoryManager';21import { GitHubRepository } from './githubRepository';22import {23AddCommentResponse,24AddReactionResponse,25AddReviewThreadResponse,26DeleteReactionResponse,27DeleteReviewResponse,28EditCommentResponse,29LatestReviewCommitResponse,30LatestReviewsResponse,31MarkPullRequestReadyForReviewResponse,32PendingReviewIdResponse,33PullRequestCommentsResponse,34PullRequestFilesResponse,35PullRequestMergabilityResponse,36ReactionGroup,37ResolveReviewThreadResponse,38StartReviewResponse,39SubmitReviewResponse,40TimelineEventsResponse,41UnresolveReviewThreadResponse,42UpdatePullRequestResponse,43} from './graphql';44import {45CheckState,46GithubItemStateEnum,47IAccount,48IRawFileChange,49ISuggestedReviewer,50MergeMethod,51PullRequest,52PullRequestChecks,53PullRequestMergeability,54ReviewEvent,55} from './interface';56import { IssueModel } from './issueModel';57import {58convertRESTPullRequestToRawPullRequest,59convertRESTReviewEvent,60convertRESTUserToAccount,61getReactionGroup,62insertNewCommitsSinceReview,63parseGraphQLComment,64parseGraphQLReaction,65parseGraphQLReviewEvent,66parseGraphQLReviewThread,67parseGraphQLTimelineEvents,68parseMergeability,69restPaginate,70} from './utils';7172interface IPullRequestModel {73head: GitHubRef | null;74}7576export interface IResolvedPullRequestModel extends IPullRequestModel {77head: GitHubRef;78}7980export interface ReviewThreadChangeEvent {81added: IReviewThread[];82changed: IReviewThread[];83removed: IReviewThread[];84}8586export interface FileViewedStateChangeEvent {87changed: {88fileName: string;89viewed: ViewedState;90}[];91}9293export const REVIEW_REQUIRED_CHECK_ID = 'reviewRequired';9495export type FileViewedState = { [key: string]: ViewedState };9697export class PullRequestModel extends IssueModel<PullRequest> implements IPullRequestModel {98static ID = 'PullRequestModel';99100public isDraft?: boolean;101public localBranchName?: string;102public mergeBase?: string;103public suggestedReviewers?: ISuggestedReviewer[];104public hasChangesSinceLastReview?: boolean;105private _showChangesSinceReview: boolean;106private _hasPendingReview: boolean = false;107private _onDidChangePendingReviewState: vscode.EventEmitter<boolean> = new vscode.EventEmitter<boolean>();108public onDidChangePendingReviewState = this._onDidChangePendingReviewState.event;109110private _reviewThreadsCache: IReviewThread[] = [];111private _reviewThreadsCacheInitialized = false;112private _onDidChangeReviewThreads = new vscode.EventEmitter<ReviewThreadChangeEvent>();113public onDidChangeReviewThreads = this._onDidChangeReviewThreads.event;114115private _fileChangeViewedState: FileViewedState = {};116private _viewedFiles: Set<string> = new Set();117private _unviewedFiles: Set<string> = new Set();118private _onDidChangeFileViewedState = new vscode.EventEmitter<FileViewedStateChangeEvent>();119public onDidChangeFileViewedState = this._onDidChangeFileViewedState.event;120121private _onDidChangeChangesSinceReview = new vscode.EventEmitter<void>();122public onDidChangeChangesSinceReview = this._onDidChangeChangesSinceReview.event;123124private _comments: IComment[] | undefined;125private _onDidChangeComments: vscode.EventEmitter<void> = new vscode.EventEmitter();126public readonly onDidChangeComments: vscode.Event<void> = this._onDidChangeComments.event;127128// Whether the pull request is currently checked out locally129private _isActive: boolean;130public get isActive(): boolean {131return this._isActive;132}133public set isActive(isActive: boolean) {134this._isActive = isActive;135}136137_telemetry: ITelemetry;138139constructor(140telemetry: ITelemetry,141githubRepository: GitHubRepository,142remote: Remote,143item: PullRequest,144isActive?: boolean,145) {146super(githubRepository, remote, item, true);147148this._telemetry = telemetry;149this.isActive = !!isActive;150151this._showChangesSinceReview = false;152153this.update(item);154}155156public clear() {157this.comments = [];158this._reviewThreadsCacheInitialized = false;159this._reviewThreadsCache = [];160}161162public async initializeReviewThreadCache(): Promise<void> {163await this.getReviewThreads();164this._reviewThreadsCacheInitialized = true;165}166167public get reviewThreadsCache(): IReviewThread[] {168return this._reviewThreadsCache;169}170171public get reviewThreadsCacheReady(): boolean {172return this._reviewThreadsCacheInitialized;173}174175public get isMerged(): boolean {176return this.state === GithubItemStateEnum.Merged;177}178179public get hasPendingReview(): boolean {180return this._hasPendingReview;181}182183public set hasPendingReview(hasPendingReview: boolean) {184if (this._hasPendingReview !== hasPendingReview) {185this._hasPendingReview = hasPendingReview;186this._onDidChangePendingReviewState.fire(this._hasPendingReview);187}188}189190public get showChangesSinceReview() {191return this._showChangesSinceReview;192}193194public set showChangesSinceReview(isChangesSinceReview: boolean) {195this._showChangesSinceReview = isChangesSinceReview;196this._onDidChangeChangesSinceReview.fire();197}198199get comments(): IComment[] {200return this._comments ?? [];201}202203set comments(comments: IComment[]) {204this._comments = comments;205this._onDidChangeComments.fire();206}207208get fileChangeViewedState(): FileViewedState {209return this._fileChangeViewedState;210}211212public isRemoteHeadDeleted?: boolean;213public head: GitHubRef | null;214public isRemoteBaseDeleted?: boolean;215public base: GitHubRef;216217protected updateState(state: string) {218if (state.toLowerCase() === 'open') {219this.state = GithubItemStateEnum.Open;220} else {221this.state = this.item.merged ? GithubItemStateEnum.Merged : GithubItemStateEnum.Closed;222}223}224225update(item: PullRequest): void {226super.update(item);227this.isDraft = item.isDraft;228this.suggestedReviewers = item.suggestedReviewers;229230if (item.isRemoteHeadDeleted != null) {231this.isRemoteHeadDeleted = item.isRemoteHeadDeleted;232}233if (item.head) {234this.head = new GitHubRef(item.head.ref, item.head.label, item.head.sha, item.head.repo.cloneUrl, item.head.repo.owner, item.head.repo.name);235}236237if (item.isRemoteBaseDeleted != null) {238this.isRemoteBaseDeleted = item.isRemoteBaseDeleted;239}240if (item.base) {241this.base = new GitHubRef(item.base.ref, item.base!.label, item.base!.sha, item.base!.repo.cloneUrl, item.base.repo.owner, item.base.repo.name);242}243}244245/**246* Validate if the pull request has a valid HEAD.247* Use only when the method can fail silently, otherwise use `validatePullRequestModel`248*/249isResolved(): this is IResolvedPullRequestModel {250return !!this.head;251}252253/**254* Validate if the pull request has a valid HEAD. Show a warning message to users when the pull request is invalid.255* @param message Human readable action execution failure message.256*/257validatePullRequestModel(message?: string): this is IResolvedPullRequestModel {258if (!!this.head) {259return true;260}261262const reason = vscode.l10n.t('There is no upstream branch for Pull Request #{0}. View it on GitHub for more details', this.number);263264if (message) {265message += `: ${reason}`;266} else {267message = reason;268}269270const openString = vscode.l10n.t('Open on GitHub');271vscode.window.showWarningMessage(message, openString).then(action => {272if (action && action === openString) {273vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(this.html_url));274}275});276277return false;278}279280/**281* Approve the pull request.282* @param message Optional approval comment text.283*/284async approve(message?: string): Promise<CommonReviewEvent> {285const action: Promise<CommonReviewEvent> = (await this.getPendingReviewId())286? this.submitReview(ReviewEvent.Approve, message)287: this.createReview(ReviewEvent.Approve, message);288289return action.then(x => {290this._onDidChangeComments.fire();291return x;292});293}294295/**296* Request changes on the pull request.297* @param message Optional comment text to leave with the review.298*/299async requestChanges(message?: string): Promise<CommonReviewEvent> {300const action: Promise<CommonReviewEvent> = (await this.getPendingReviewId())301? this.submitReview(ReviewEvent.RequestChanges, message)302: this.createReview(ReviewEvent.RequestChanges, message);303304return action.then(x => {305this._onDidChangeComments.fire();306return x;307});308}309310/**311* Close the pull request.312*/313async close(): Promise<PullRequest> {314const { octokit, remote } = await this.githubRepository.ensure();315const ret = await octokit.call(octokit.api.pulls.update, {316owner: remote.owner,317repo: remote.repositoryName,318pull_number: this.number,319state: 'closed',320});321322return convertRESTPullRequestToRawPullRequest(ret.data, this.githubRepository);323}324325/**326* Create a new review.327* @param event The type of review to create, an approval, request for changes, or comment.328* @param message The summary comment text.329*/330private async createReview(event: ReviewEvent, message?: string): Promise<CommonReviewEvent> {331const { octokit, remote } = await this.githubRepository.ensure();332333const { data } = await octokit.call(octokit.api.pulls.createReview, {334owner: remote.owner,335repo: remote.repositoryName,336pull_number: this.number,337event: event,338body: message,339});340341return convertRESTReviewEvent(data, this.githubRepository);342}343344/**345* Submit an existing review.346* @param event The type of review to create, an approval, request for changes, or comment.347* @param body The summary comment text.348*/349async submitReview(event?: ReviewEvent, body?: string): Promise<CommonReviewEvent> {350let pendingReviewId = await this.getPendingReviewId();351const { mutate, schema } = await this.githubRepository.ensure();352353if (!pendingReviewId && (event === ReviewEvent.Comment)) {354// Create a new review so that we can comment on it.355pendingReviewId = await this.startReview();356}357358if (pendingReviewId) {359const { data } = await mutate<SubmitReviewResponse>({360mutation: schema.SubmitReview,361variables: {362id: pendingReviewId,363event: event || ReviewEvent.Comment,364body,365},366});367368this.hasPendingReview = false;369await this.updateDraftModeContext();370const reviewEvent = parseGraphQLReviewEvent(data!.submitPullRequestReview.pullRequestReview, this.githubRepository);371372const threadWithComment = this._reviewThreadsCache.find(thread =>373thread.comments.length ? (thread.comments[0].pullRequestReviewId === reviewEvent.id) : undefined,374);375if (threadWithComment) {376threadWithComment.comments = reviewEvent.comments;377threadWithComment.viewerCanResolve = true;378this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] });379}380return reviewEvent;381} else {382throw new Error(`Submitting review failed, no pending review for current pull request: ${this.number}.`);383}384}385386async updateMilestone(id: string): Promise<void> {387const { mutate, schema } = await this.githubRepository.ensure();388const finalId = id === 'null' ? null : id;389390try {391await mutate<UpdatePullRequestResponse>({392mutation: schema.UpdatePullRequest,393variables: {394input: {395pullRequestId: this.item.graphNodeId,396milestoneId: finalId,397},398},399});400} catch (err) {401Logger.error(err, PullRequestModel.ID);402}403}404405async addAssignees(assignees: string[]): Promise<void> {406const { octokit, remote } = await this.githubRepository.ensure();407await octokit.call(octokit.api.issues.addAssignees, {408owner: remote.owner,409repo: remote.repositoryName,410issue_number: this.number,411assignees,412});413}414415/**416* Query to see if there is an existing review.417*/418async getPendingReviewId(): Promise<string | undefined> {419const { query, schema } = await this.githubRepository.ensure();420const currentUser = await this.githubRepository.getAuthenticatedUser();421try {422const { data } = await query<PendingReviewIdResponse>({423query: schema.GetPendingReviewId,424variables: {425pullRequestId: this.item.graphNodeId,426author: currentUser,427},428});429return data.node.reviews.nodes.length > 0 ? data.node.reviews.nodes[0].id : undefined;430} catch (error) {431return;432}433}434435async getViewerLatestReviewCommit(): Promise<{ sha: string } | undefined> {436Logger.debug(`Fetch viewers latest review commit`, IssueModel.ID);437const { query, remote, schema } = await this.githubRepository.ensure();438439try {440const { data } = await query<LatestReviewCommitResponse>({441query: schema.LatestReviewCommit,442variables: {443owner: remote.owner,444name: remote.repositoryName,445number: this.number,446},447});448449return data.repository.pullRequest.viewerLatestReview ? {450sha: data.repository.pullRequest.viewerLatestReview.commit.oid,451} : undefined;452}453catch (e) {454return undefined;455}456}457458/**459* Delete an existing in progress review.460*/461async deleteReview(): Promise<{ deletedReviewId: number; deletedReviewComments: IComment[] }> {462const pendingReviewId = await this.getPendingReviewId();463const { mutate, schema } = await this.githubRepository.ensure();464const { data } = await mutate<DeleteReviewResponse>({465mutation: schema.DeleteReview,466variables: {467input: { pullRequestReviewId: pendingReviewId },468},469});470471const { comments, databaseId } = data!.deletePullRequestReview.pullRequestReview;472473this.hasPendingReview = false;474await this.updateDraftModeContext();475476this.getReviewThreads();477478return {479deletedReviewId: databaseId,480deletedReviewComments: comments.nodes.map(comment => parseGraphQLComment(comment, false, this.githubRepository)),481};482}483484/**485* Start a new review.486* @param initialComment The comment text and position information to begin the review with487* @param commitId The optional commit id to start the review on. Defaults to using the current head commit.488*/489async startReview(commitId?: string): Promise<string> {490const { mutate, schema } = await this.githubRepository.ensure();491const { data } = await mutate<StartReviewResponse>({492mutation: schema.StartReview,493variables: {494input: {495body: '',496pullRequestId: this.item.graphNodeId,497commitOID: commitId || this.head?.sha,498},499},500});501502if (!data) {503throw new Error('Failed to start review');504}505this.hasPendingReview = true;506this._onDidChangeComments.fire();507return data.addPullRequestReview.pullRequestReview.id;508}509510/**511* Creates a new review thread, either adding it to an existing pending review, or creating512* a new review.513* @param body The body of the thread's first comment.514* @param commentPath The path to the file being commented on.515* @param startLine The start line on which to add the comment.516* @param endLine The end line on which to add the comment.517* @param side The side the comment should be deleted on, i.e. the original or modified file.518* @param suppressDraftModeUpdate If a draft mode change should event should be suppressed. In the519* case of a single comment add, the review is created and then immediately submitted, so this prevents520* a "Pending" label from flashing on the comment.521* @returns The new review thread object.522*/523async createReviewThread(524body: string,525commentPath: string,526startLine: number,527endLine: number,528side: DiffSide,529suppressDraftModeUpdate?: boolean,530): Promise<IReviewThread | undefined> {531if (!this.validatePullRequestModel('Creating comment failed')) {532return;533}534const pendingReviewId = await this.getPendingReviewId();535536const { mutate, schema } = await this.githubRepository.ensure();537const { data } = await mutate<AddReviewThreadResponse>({538mutation: schema.AddReviewThread,539variables: {540input: {541path: commentPath,542body,543pullRequestId: this.graphNodeId,544pullRequestReviewId: pendingReviewId,545startLine: startLine === endLine ? undefined : startLine,546line: endLine,547side,548},549},550});551552if (!data) {553throw new Error('Creating review thread failed.');554}555556if (!data.addPullRequestReviewThread.thread) {557throw new Error('File has been deleted.');558}559560if (!suppressDraftModeUpdate) {561this.hasPendingReview = true;562await this.updateDraftModeContext();563}564565const thread = data.addPullRequestReviewThread.thread;566const newThread = parseGraphQLReviewThread(thread, this.githubRepository);567this._reviewThreadsCache.push(newThread);568this._onDidChangeReviewThreads.fire({ added: [newThread], changed: [], removed: [] });569return newThread;570}571572/**573* Creates a new comment in reply to an existing comment574* @param body The text of the comment to be created575* @param inReplyTo The id of the comment this is in reply to576* @param isSingleComment Whether this is a single comment, i.e. one that577* will be immediately submitted and so should not show a pending label578* @param commitId The commit id the comment was made on579* @returns The new comment580*/581async createCommentReply(582body: string,583inReplyTo: string,584isSingleComment: boolean,585commitId?: string,586): Promise<IComment | undefined> {587if (!this.validatePullRequestModel('Creating comment failed')) {588return;589}590591let pendingReviewId = await this.getPendingReviewId();592if (!pendingReviewId) {593pendingReviewId = await this.startReview(commitId);594}595596const { mutate, schema } = await this.githubRepository.ensure();597const { data } = await mutate<AddCommentResponse>({598mutation: schema.AddComment,599variables: {600input: {601pullRequestReviewId: pendingReviewId,602body,603inReplyTo,604commitOID: commitId || this.head?.sha,605},606},607});608609if (!data) {610throw new Error('Creating comment reply failed.');611}612613const { comment } = data.addPullRequestReviewComment;614const newComment = parseGraphQLComment(comment, false, this.githubRepository);615616if (isSingleComment) {617newComment.isDraft = false;618}619620const threadWithComment = this._reviewThreadsCache.find(thread =>621thread.comments.some(comment => comment.graphNodeId === inReplyTo),622);623if (threadWithComment) {624threadWithComment.comments.push(newComment);625this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] });626}627628return newComment;629}630631/**632* Check whether there is an existing pending review and update the context key to control what comment actions are shown.633*/634async validateDraftMode(): Promise<boolean> {635const inDraftMode = !!(await this.getPendingReviewId());636if (inDraftMode !== this.hasPendingReview) {637this.hasPendingReview = inDraftMode;638}639640await this.updateDraftModeContext();641642return inDraftMode;643}644645private async updateDraftModeContext() {646if (this.isActive) {647await vscode.commands.executeCommand('setContext', 'reviewInDraftMode', this.hasPendingReview);648}649}650651/**652* Edit an existing review comment.653* @param comment The comment to edit654* @param text The new comment text655*/656async editReviewComment(comment: IComment, text: string): Promise<IComment> {657const { mutate, schema } = await this.githubRepository.ensure();658let threadWithComment = this._reviewThreadsCache.find(thread =>659thread.comments.some(c => c.graphNodeId === comment.graphNodeId),660);661662if (!threadWithComment) {663return this.editIssueComment(comment, text);664}665666const { data } = await mutate<EditCommentResponse>({667mutation: schema.EditComment,668variables: {669input: {670pullRequestReviewCommentId: comment.graphNodeId,671body: text,672},673},674});675676if (!data) {677throw new Error('Editing review comment failed.');678}679680const newComment = parseGraphQLComment(681data.updatePullRequestReviewComment.pullRequestReviewComment,682!!comment.isResolved,683this.githubRepository684);685if (threadWithComment) {686const index = threadWithComment.comments.findIndex(c => c.graphNodeId === comment.graphNodeId);687threadWithComment.comments.splice(index, 1, newComment);688this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] });689}690691return newComment;692}693694/**695* Deletes a review comment.696* @param commentId The comment id to delete697*/698async deleteReviewComment(commentId: string): Promise<void> {699try {700const { octokit, remote } = await this.githubRepository.ensure();701const id = Number(commentId);702const threadIndex = this._reviewThreadsCache.findIndex(thread => thread.comments.some(c => c.id === id));703704if (threadIndex === -1) {705this.deleteIssueComment(commentId);706} else {707await octokit.call(octokit.api.pulls.deleteReviewComment, {708owner: remote.owner,709repo: remote.repositoryName,710comment_id: id,711});712713if (threadIndex > -1) {714const threadWithComment = this._reviewThreadsCache[threadIndex];715const index = threadWithComment.comments.findIndex(c => c.id === id);716threadWithComment.comments.splice(index, 1);717if (threadWithComment.comments.length === 0) {718this._reviewThreadsCache.splice(threadIndex, 1);719this._onDidChangeReviewThreads.fire({ added: [], changed: [], removed: [threadWithComment] });720} else {721this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] });722}723}724}725} catch (e) {726throw new Error(formatError(e));727}728}729730/**731* Get existing requests to review.732*/733async getReviewRequests(): Promise<IAccount[]> {734const githubRepository = this.githubRepository;735const { remote, octokit } = await githubRepository.ensure();736const result = await octokit.call(octokit.api.pulls.listRequestedReviewers, {737owner: remote.owner,738repo: remote.repositoryName,739pull_number: this.number,740});741742return result.data.users.map((user: any) => convertRESTUserToAccount(user, githubRepository));743}744745/**746* Add reviewers to a pull request747* @param reviewers A list of GitHub logins748*/749async requestReview(reviewers: string[]): Promise<void> {750const { octokit, remote } = await this.githubRepository.ensure();751await octokit.call(octokit.api.pulls.requestReviewers, {752owner: remote.owner,753repo: remote.repositoryName,754pull_number: this.number,755reviewers,756});757}758759/**760* Remove a review request that has not yet been completed761* @param reviewer A GitHub Login762*/763async deleteReviewRequest(reviewers: string[]): Promise<void> {764const { octokit, remote } = await this.githubRepository.ensure();765await octokit.call(octokit.api.pulls.removeRequestedReviewers, {766owner: remote.owner,767repo: remote.repositoryName,768pull_number: this.number,769reviewers,770});771}772773async deleteAssignees(assignees: string[]): Promise<void> {774const { octokit, remote } = await this.githubRepository.ensure();775await octokit.call(octokit.api.issues.removeAssignees, {776owner: remote.owner,777repo: remote.repositoryName,778issue_number: this.number,779assignees,780});781}782783private diffThreads(oldReviewThreads: IReviewThread[], newReviewThreads: IReviewThread[]): void {784const added: IReviewThread[] = [];785const changed: IReviewThread[] = [];786const removed: IReviewThread[] = [];787788newReviewThreads.forEach(thread => {789const existingThread = oldReviewThreads.find(t => t.id === thread.id);790if (existingThread) {791if (!equals(thread, existingThread)) {792changed.push(thread);793}794} else {795added.push(thread);796}797});798799oldReviewThreads.forEach(thread => {800if (!newReviewThreads.find(t => t.id === thread.id)) {801removed.push(thread);802}803});804805this._onDidChangeReviewThreads.fire({806added,807changed,808removed,809});810}811812async getReviewThreads(): Promise<IReviewThread[]> {813const { remote, query, schema } = await this.githubRepository.ensure();814try {815const { data } = await query<PullRequestCommentsResponse>({816query: schema.PullRequestComments,817variables: {818owner: remote.owner,819name: remote.repositoryName,820number: this.number,821},822});823824const reviewThreads = data.repository.pullRequest.reviewThreads.nodes.map(node => {825return parseGraphQLReviewThread(node, this.githubRepository);826});827828const oldReviewThreads = this._reviewThreadsCache;829this._reviewThreadsCache = reviewThreads;830this.diffThreads(oldReviewThreads, reviewThreads);831return reviewThreads;832} catch (e) {833Logger.error(`Failed to get pull request review comments: ${e}`, PullRequestModel.ID);834return [];835}836}837838/**839* Get all review comments.840*/841async initializeReviewComments(): Promise<void> {842const { remote, query, schema } = await this.githubRepository.ensure();843try {844const { data } = await query<PullRequestCommentsResponse>({845query: schema.PullRequestComments,846variables: {847owner: remote.owner,848name: remote.repositoryName,849number: this.number,850},851});852853const comments = data.repository.pullRequest.reviewThreads.nodes854.map(node => node.comments.nodes.map(comment => parseGraphQLComment(comment, node.isResolved, this.githubRepository), remote))855.reduce((prev, curr) => prev.concat(curr), [])856.sort((a: IComment, b: IComment) => {857return a.createdAt > b.createdAt ? 1 : -1;858});859860this.comments = comments;861} catch (e) {862Logger.error(`Failed to get pull request review comments: ${e}`, PullRequestModel.ID);863}864}865866/**867* Get a list of the commits within a pull request.868*/869async getCommits(): Promise<OctokitCommon.PullsListCommitsResponseData> {870try {871Logger.debug(`Fetch commits of PR #${this.number} - enter`, PullRequestModel.ID);872const { remote, octokit } = await this.githubRepository.ensure();873const commitData = await octokit.call(octokit.api.pulls.listCommits, {874pull_number: this.number,875owner: remote.owner,876repo: remote.repositoryName,877});878Logger.debug(`Fetch commits of PR #${this.number} - done`, PullRequestModel.ID);879880return commitData.data;881} catch (e) {882vscode.window.showErrorMessage(`Fetching commits failed: ${formatError(e)}`);883return [];884}885}886887/**888* Get all changed files within a commit889* @param commit The commit890*/891async getCommitChangedFiles(892commit: OctokitCommon.PullsListCommitsResponseData[0],893): Promise<OctokitCommon.ReposGetCommitResponseFiles> {894try {895Logger.debug(896`Fetch file changes of commit ${commit.sha} in PR #${this.number} - enter`,897PullRequestModel.ID,898);899const { octokit, remote } = await this.githubRepository.ensure();900const fullCommit = await octokit.call(octokit.api.repos.getCommit, {901owner: remote.owner,902repo: remote.repositoryName,903ref: commit.sha,904});905Logger.debug(906`Fetch file changes of commit ${commit.sha} in PR #${this.number} - done`,907PullRequestModel.ID,908);909910return fullCommit.data.files ?? [];911} catch (e) {912vscode.window.showErrorMessage(`Fetching commit file changes failed: ${formatError(e)}`);913return [];914}915}916917/**918* Gets file content for a file at the specified commit919* @param filePath The file path920* @param commit The commit921*/922async getFile(filePath: string, commit: string) {923const { octokit, remote } = await this.githubRepository.ensure();924const fileContent = await octokit.call(octokit.api.repos.getContent, {925owner: remote.owner,926repo: remote.repositoryName,927path: filePath,928ref: commit,929});930931if (Array.isArray(fileContent.data)) {932throw new Error(`Unexpected array response when getting file ${filePath}`);933}934935const contents = (fileContent.data as any).content ?? '';936const buff = buffer.Buffer.from(contents, (fileContent.data as any).encoding);937return buff.toString();938}939940/**941* Get the timeline events of a pull request, including comments, reviews, commits, merges, deletes, and assigns.942*/943async getTimelineEvents(): Promise<TimelineEvent[]> {944Logger.debug(`Fetch timeline events of PR #${this.number} - enter`, PullRequestModel.ID);945const { query, remote, schema } = await this.githubRepository.ensure();946947try {948const [{ data }, latestReviewCommitInfo, currentUser, reviewThreads] = await Promise.all([949query<TimelineEventsResponse>({950query: schema.TimelineEvents,951variables: {952owner: remote.owner,953name: remote.repositoryName,954number: this.number,955},956}),957this.getViewerLatestReviewCommit(),958this.githubRepository.getAuthenticatedUser(),959this.getReviewThreads()960]);961962const ret = data.repository.pullRequest.timelineItems.nodes;963const events = parseGraphQLTimelineEvents(ret, this.githubRepository);964965this.addReviewTimelineEventComments(events, reviewThreads);966insertNewCommitsSinceReview(events, latestReviewCommitInfo?.sha, currentUser, this.head);967968return events;969} catch (e) {970console.log(e);971return [];972}973}974975private addReviewTimelineEventComments(events: TimelineEvent[], reviewThreads: IReviewThread[]): void {976interface CommentNode extends IComment {977childComments?: CommentNode[];978}979980const reviewEvents = events.filter((e): e is CommonReviewEvent => e.event === EventType.Reviewed);981const reviewComments = reviewThreads.reduce((previous, current) => (previous as IComment[]).concat(current.comments), []);982983const reviewEventsById = reviewEvents.reduce((index, evt) => {984index[evt.id] = evt;985evt.comments = [];986return index;987}, {} as { [key: number]: CommonReviewEvent });988989const commentsById = reviewComments.reduce((index, evt) => {990index[evt.id] = evt;991return index;992}, {} as { [key: number]: CommentNode });993994const roots: CommentNode[] = [];995let i = reviewComments.length;996while (i-- > 0) {997const c: CommentNode = reviewComments[i];998if (!c.inReplyToId) {999roots.unshift(c);1000continue;1001}1002const parent = commentsById[c.inReplyToId];1003parent.childComments = parent.childComments || [];1004parent.childComments = [c, ...(c.childComments || []), ...parent.childComments];1005}10061007roots.forEach(c => {1008const review = reviewEventsById[c.pullRequestReviewId!];1009if (review) {1010review.comments = review.comments.concat(c).concat(c.childComments || []);1011}1012});10131014reviewThreads.forEach(thread => {1015if (!thread.prReviewDatabaseId || !reviewEventsById[thread.prReviewDatabaseId]) {1016return;1017}1018const prReviewThreadEvent = reviewEventsById[thread.prReviewDatabaseId];1019prReviewThreadEvent.reviewThread = {1020threadId: thread.id,1021canResolve: thread.viewerCanResolve,1022canUnresolve: thread.viewerCanUnresolve,1023isResolved: thread.isResolved1024};10251026});10271028const pendingReview = reviewEvents.filter(r => r.state.toLowerCase() === 'pending')[0];1029if (pendingReview) {1030// Ensures that pending comments made in reply to other reviews are included for the pending review1031pendingReview.comments = reviewComments.filter(c => c.isDraft);1032}1033}10341035private async _getReviewRequiredCheck() {1036const { query, remote, octokit, schema } = await this.githubRepository.ensure();10371038const [branch, reviewStates] = await Promise.all([1039octokit.call(octokit.api.repos.getBranch, { branch: this.base.ref, owner: remote.owner, repo: remote.repositoryName }),1040query<LatestReviewsResponse>({1041query: schema.LatestReviews,1042variables: {1043owner: remote.owner,1044name: remote.repositoryName,1045number: this.number,1046}1047})1048]);1049if (branch.data.protected && branch.data.protection.required_status_checks && branch.data.protection.required_status_checks.enforcement_level !== 'off') {1050// We need to add the "review required" check manually.1051return {1052id: REVIEW_REQUIRED_CHECK_ID,1053context: 'Branch Protection',1054description: vscode.l10n.t('Other requirements have not been met.'),1055state: (reviewStates.data as LatestReviewsResponse).repository.pullRequest.latestReviews.nodes.every(node => node.state !== 'CHANGES_REQUESTED') ? CheckState.Neutral : CheckState.Failure,1056target_url: this.html_url1057};1058}1059return undefined;1060}10611062/**1063* Get the status checks of the pull request, those for the last commit.1064*/1065async getStatusChecks(): Promise<PullRequestChecks | undefined> {1066let checks = await this.githubRepository.getStatusChecks(this.number);10671068// Fun info: The checks don't include whether a review is required.1069// Also, unless you're an admin on the repo, you can't just do octokit.repos.getBranchProtection1070if ((this.item.mergeable === PullRequestMergeability.NotMergeable) && (!checks || checks.statuses.every(status => status.state === CheckState.Success))) {1071const reviewRequiredCheck = await this._getReviewRequiredCheck();1072if (reviewRequiredCheck) {1073if (!checks) {1074checks = {1075state: CheckState.Failure,1076statuses: []1077};1078}1079checks.statuses.push(reviewRequiredCheck);1080checks.state = CheckState.Failure;1081}1082}10831084return checks;1085}10861087static async openDiffFromComment(1088folderManager: FolderRepositoryManager,1089pullRequestModel: PullRequestModel,1090comment: IComment,1091): Promise<void> {1092const contentChanges = await pullRequestModel.getFileChangesInfo();1093const change = contentChanges.find(1094fileChange => fileChange.fileName === comment.path || fileChange.previousFileName === comment.path,1095);1096if (!change) {1097throw new Error(`Can't find matching file`);1098}10991100const pathSegments = comment.path!.split('/');1101this.openDiff(folderManager, pullRequestModel, change, pathSegments[pathSegments.length - 1]);1102}11031104static async openFirstDiff(1105folderManager: FolderRepositoryManager,1106pullRequestModel: PullRequestModel,1107) {1108const contentChanges = await pullRequestModel.getFileChangesInfo();1109if (!contentChanges.length) {1110return;1111}11121113const firstChange = contentChanges[0];1114this.openDiff(folderManager, pullRequestModel, firstChange, firstChange.fileName);1115}11161117static async openDiff(1118folderManager: FolderRepositoryManager,1119pullRequestModel: PullRequestModel,1120change: SlimFileChange | InMemFileChange,1121diffTitle: string1122): Promise<void> {112311241125let headUri, baseUri: vscode.Uri;1126if (!pullRequestModel.equals(folderManager.activePullRequest)) {1127const headCommit = pullRequestModel.head!.sha;1128const parentFileName = change.status === GitChangeType.RENAME ? change.previousFileName! : change.fileName;1129headUri = toPRUri(1130vscode.Uri.file(resolvePath(folderManager.repository.rootUri, change.fileName)),1131pullRequestModel,1132change.baseCommit,1133headCommit,1134change.fileName,1135false,1136change.status,1137change.previousFileName1138);1139baseUri = toPRUri(1140vscode.Uri.file(resolvePath(folderManager.repository.rootUri, parentFileName)),1141pullRequestModel,1142change.baseCommit,1143headCommit,1144change.fileName,1145true,1146change.status,1147change.previousFileName1148);1149} else {1150const uri = vscode.Uri.file(path.resolve(folderManager.repository.rootUri.fsPath, change.fileName));11511152headUri =1153change.status === GitChangeType.DELETE1154? toReviewUri(1155uri,1156undefined,1157undefined,1158'',1159false,1160{ base: false },1161folderManager.repository.rootUri,1162)1163: uri;11641165const mergeBase = pullRequestModel.mergeBase || pullRequestModel.base.sha;1166baseUri = toReviewUri(1167uri,1168change.status === GitChangeType.RENAME ? change.previousFileName : change.fileName,1169undefined,1170change.status === GitChangeType.ADD ? '' : mergeBase,1171false,1172{ base: true },1173folderManager.repository.rootUri,1174);1175}11761177vscode.commands.executeCommand(1178'vscode.diff',1179baseUri,1180headUri,1181`${diffTitle} (Pull Request)`,1182{},1183);1184}11851186private _fileChanges: Map<string, SlimFileChange | InMemFileChange> = new Map();1187get fileChanges(): Map<string, SlimFileChange | InMemFileChange> {1188return this._fileChanges;1189}11901191async getFileChangesInfo() {1192this._fileChanges.clear();1193const data = await this.getRawFileChangesInfo();1194const mergebase = this.mergeBase || this.base.sha;1195const parsed = await parseDiff(data, mergebase);1196parsed.forEach(fileChange => {1197this._fileChanges.set(fileChange.fileName, fileChange);1198});1199return parsed;1200}12011202/**1203* List the changed files in a pull request.1204*/1205private async getRawFileChangesInfo(): Promise<IRawFileChange[]> {1206Logger.debug(1207`Fetch file changes, base, head and merge base of PR #${this.number} - enter`,1208PullRequestModel.ID,1209);1210const githubRepository = this.githubRepository;1211const { octokit, remote } = await githubRepository.ensure();12121213if (!this.base) {1214const info = await octokit.call(octokit.api.pulls.get, {1215owner: remote.owner,1216repo: remote.repositoryName,1217pull_number: this.number,1218});1219this.update(convertRESTPullRequestToRawPullRequest(info.data, githubRepository));1220}12211222let compareWithBaseRef = this.base.sha;1223const latestReview = await this.getViewerLatestReviewCommit();1224const oldHasChangesSinceReview = this.hasChangesSinceLastReview;1225this.hasChangesSinceLastReview = latestReview !== undefined && this.head?.sha !== latestReview.sha;12261227if (this._showChangesSinceReview && this.hasChangesSinceLastReview && latestReview != undefined) {1228compareWithBaseRef = latestReview.sha;1229}12301231if (this.item.merged) {1232const response = await restPaginate<typeof octokit.api.pulls.listFiles, IRawFileChange>(octokit.api.pulls.listFiles, {1233repo: remote.repositoryName,1234owner: remote.owner,1235pull_number: this.number,1236});12371238// Use the original base to compare against for merged PRs1239this.mergeBase = this.base.sha;12401241return response;1242}12431244const { data } = await octokit.call(octokit.api.repos.compareCommits, {1245repo: remote.repositoryName,1246owner: remote.owner,1247base: `${this.base.repositoryCloneUrl.owner}:${compareWithBaseRef}`,1248head: `${this.head!.repositoryCloneUrl.owner}:${this.head!.sha}`,1249});12501251this.mergeBase = data.merge_base_commit.sha;12521253const MAX_FILE_CHANGES_IN_COMPARE_COMMITS = 100;1254let files: IRawFileChange[] = [];12551256if (data.files && data.files.length >= MAX_FILE_CHANGES_IN_COMPARE_COMMITS) {1257// compareCommits will return a maximum of 100 changed files1258// If we have (maybe) more than that, we'll need to fetch them with listFiles API call1259Logger.debug(1260`More than ${MAX_FILE_CHANGES_IN_COMPARE_COMMITS} files changed, fetching all file changes of PR #${this.number}`,1261PullRequestModel.ID,1262);1263files = await restPaginate<typeof octokit.api.pulls.listFiles, IRawFileChange>(octokit.api.pulls.listFiles, {1264owner: this.base.repositoryCloneUrl.owner,1265pull_number: this.number,1266repo: remote.repositoryName,1267});1268} else {1269// if we're under the limit, just use the result from compareCommits, don't make additional API calls.1270files = data.files ? data.files as IRawFileChange[] : [];1271}12721273if (oldHasChangesSinceReview !== undefined && oldHasChangesSinceReview !== this.hasChangesSinceLastReview && this.hasChangesSinceLastReview && this._showChangesSinceReview) {1274this._onDidChangeChangesSinceReview.fire();1275}12761277Logger.debug(1278`Fetch file changes and merge base of PR #${this.number} - done, total files ${files.length} `,1279PullRequestModel.ID,1280);1281return files;1282}12831284get autoMerge(): boolean {1285return !!this.item.autoMerge;1286}12871288get autoMergeMethod(): MergeMethod | undefined {1289return this.item.autoMergeMethod;1290}12911292get allowAutoMerge(): boolean {1293return !!this.item.allowAutoMerge;1294}12951296/**1297* Get the current mergeability of the pull request.1298*/1299async getMergeability(): Promise<PullRequestMergeability> {1300try {1301Logger.debug(`Fetch pull request mergeability ${this.number} - enter`, PullRequestModel.ID);1302const { query, remote, schema } = await this.githubRepository.ensure();13031304const { data } = await query<PullRequestMergabilityResponse>({1305query: schema.PullRequestMergeability,1306variables: {1307owner: remote.owner,1308name: remote.repositoryName,1309number: this.number,1310},1311});1312Logger.debug(`Fetch pull request mergeability ${this.number} - done`, PullRequestModel.ID);1313const mergeability = parseMergeability(data.repository.pullRequest.mergeable, data.repository.pullRequest.mergeStateStatus);1314this.item.mergeable = mergeability;1315return mergeability;1316} catch (e) {1317Logger.error(`Unable to fetch PR Mergeability: ${e}`, PullRequestModel.ID);1318return PullRequestMergeability.Unknown;1319}1320}13211322/**1323* Set a draft pull request as ready to be reviewed.1324*/1325async setReadyForReview(): Promise<any> {1326try {1327const { mutate, schema } = await this.githubRepository.ensure();13281329const { data } = await mutate<MarkPullRequestReadyForReviewResponse>({1330mutation: schema.ReadyForReview,1331variables: {1332input: {1333pullRequestId: this.graphNodeId,1334},1335},1336});13371338return data!.markPullRequestReadyForReview.pullRequest.isDraft;1339} catch (e) {1340throw e;1341}1342}13431344private updateCommentReactions(graphNodeId: string, reactionGroups: ReactionGroup[]) {1345const reviewThread = this._reviewThreadsCache.find(thread =>1346thread.comments.some(c => c.graphNodeId === graphNodeId),1347);1348if (reviewThread) {1349const updatedComment = reviewThread.comments.find(c => c.graphNodeId === graphNodeId);1350if (updatedComment) {1351updatedComment.reactions = parseGraphQLReaction(reactionGroups);1352this._onDidChangeReviewThreads.fire({ added: [], changed: [reviewThread], removed: [] });1353}1354}1355}13561357async addCommentReaction(graphNodeId: string, reaction: vscode.CommentReaction): Promise<AddReactionResponse | undefined> {1358const reactionEmojiToContent = getReactionGroup().reduce((prev, curr) => {1359prev[curr.label] = curr.title;1360return prev;1361}, {} as { [key: string]: string });1362const { mutate, schema } = await this.githubRepository.ensure();1363const { data } = await mutate<AddReactionResponse>({1364mutation: schema.AddReaction,1365variables: {1366input: {1367subjectId: graphNodeId,1368content: reactionEmojiToContent[reaction.label!],1369},1370},1371});13721373if (!data) {1374throw new Error('Add comment reaction failed.');1375}13761377const reactionGroups = data.addReaction.subject.reactionGroups;1378this.updateCommentReactions(graphNodeId, reactionGroups);13791380return data;1381}13821383async deleteCommentReaction(1384graphNodeId: string,1385reaction: vscode.CommentReaction,1386): Promise<DeleteReactionResponse | undefined> {1387const reactionEmojiToContent = getReactionGroup().reduce((prev, curr) => {1388prev[curr.label] = curr.title;1389return prev;1390}, {} as { [key: string]: string });1391const { mutate, schema } = await this.githubRepository.ensure();1392const { data } = await mutate<DeleteReactionResponse>({1393mutation: schema.DeleteReaction,1394variables: {1395input: {1396subjectId: graphNodeId,1397content: reactionEmojiToContent[reaction.label!],1398},1399},1400});14011402if (!data) {1403throw new Error('Delete comment reaction failed.');1404}14051406const reactionGroups = data.removeReaction.subject.reactionGroups;1407this.updateCommentReactions(graphNodeId, reactionGroups);14081409return data;1410}14111412async resolveReviewThread(threadId: string): Promise<void> {1413const { mutate, schema } = await this.githubRepository.ensure();14141415// optimistically update1416const oldThread = this._reviewThreadsCache.find(thread => thread.id === threadId);1417if (oldThread && oldThread.viewerCanResolve) {1418oldThread.isResolved = true;1419oldThread.viewerCanResolve = false;1420oldThread.viewerCanUnresolve = true;1421this._onDidChangeReviewThreads.fire({ added: [], changed: [oldThread], removed: [] });1422}14231424const { data } = await mutate<ResolveReviewThreadResponse>({1425mutation: schema.ResolveReviewThread,1426variables: {1427input: {1428threadId,1429},1430},1431});14321433if (!data) {1434// Undo optimistic update1435if (oldThread && oldThread.viewerCanUnresolve) {1436oldThread.isResolved = false;1437oldThread.viewerCanResolve = true;1438oldThread.viewerCanUnresolve = false;1439this._onDidChangeReviewThreads.fire({ added: [], changed: [oldThread], removed: [] });1440}1441throw new Error('Resolve review thread failed.');1442}14431444const index = this._reviewThreadsCache.findIndex(thread => thread.id === threadId);1445if (index > -1) {1446const thread = parseGraphQLReviewThread(data.resolveReviewThread.thread, this.githubRepository);1447this._reviewThreadsCache.splice(index, 1, thread);1448this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] });1449}1450}14511452async unresolveReviewThread(threadId: string): Promise<void> {1453const { mutate, schema } = await this.githubRepository.ensure();14541455// optimistically update1456const oldThread = this._reviewThreadsCache.find(thread => thread.id === threadId);1457if (oldThread && oldThread.viewerCanUnresolve) {1458oldThread.isResolved = false;1459oldThread.viewerCanUnresolve = false;1460oldThread.viewerCanResolve = true;1461this._onDidChangeReviewThreads.fire({ added: [], changed: [oldThread], removed: [] });1462}14631464const { data } = await mutate<UnresolveReviewThreadResponse>({1465mutation: schema.UnresolveReviewThread,1466variables: {1467input: {1468threadId,1469},1470},1471});14721473if (!data) {1474// Undo optimistic update1475if (oldThread && oldThread.viewerCanResolve) {1476oldThread.isResolved = true;1477oldThread.viewerCanUnresolve = true;1478oldThread.viewerCanResolve = false;1479this._onDidChangeReviewThreads.fire({ added: [], changed: [oldThread], removed: [] });1480}1481throw new Error('Unresolve review thread failed.');1482}14831484const index = this._reviewThreadsCache.findIndex(thread => thread.id === threadId);1485if (index > -1) {1486const thread = parseGraphQLReviewThread(data.unresolveReviewThread.thread, this.githubRepository);1487this._reviewThreadsCache.splice(index, 1, thread);1488this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] });1489}1490}14911492async enableAutoMerge(mergeMethod: MergeMethod): Promise<void> {1493try {1494const { mutate, schema } = await this.githubRepository.ensure();1495const { data } = await mutate({1496mutation: schema.EnablePullRequestAutoMerge,1497variables: {1498input: {1499mergeMethod: mergeMethod.toUpperCase(),1500pullRequestId: this.graphNodeId1501}1502}1503});15041505if (!data) {1506throw new Error('Enable auto-merge failed.');1507}1508this.item.autoMerge = true;1509this.item.autoMergeMethod = mergeMethod;1510} catch (e) {1511if (e.message === 'GraphQL error: ["Pull request Pull request is in clean status"]') {1512vscode.window.showWarningMessage(vscode.l10n.t('Unable to enable auto-merge. Pull request status checks are already green.'));1513} else {1514throw e;1515}1516}1517}15181519async disableAutoMerge(): Promise<void> {1520try {1521const { mutate, schema } = await this.githubRepository.ensure();1522const { data } = await mutate({1523mutation: schema.DisablePullRequestAutoMerge,1524variables: {1525input: {1526pullRequestId: this.graphNodeId1527}1528}1529});15301531if (!data) {1532throw new Error('Disable auto-merge failed.');1533}1534this.item.autoMerge = false;1535} catch (e) {1536if (e.message === 'GraphQL error: ["Pull request Pull request is in clean status"]') {1537vscode.window.showWarningMessage(vscode.l10n.t('Unable to enable auto-merge. Pull request status checks are already green.'));1538} else {1539throw e;1540}1541}1542}15431544async initializePullRequestFileViewState(): Promise<void> {1545const { query, schema, remote } = await this.githubRepository.ensure();15461547const changed: { fileName: string, viewed: ViewedState }[] = [];1548let after: string | null = null;1549let hasNextPage = false;15501551do {1552const { data } = await query<PullRequestFilesResponse>({1553query: schema.PullRequestFiles,1554variables: {1555owner: remote.owner,1556name: remote.repositoryName,1557number: this.number,1558after: after,1559},1560});15611562data.repository.pullRequest.files.nodes.forEach(n => {1563if (this._fileChangeViewedState[n.path] !== n.viewerViewedState) {1564changed.push({ fileName: n.path, viewed: n.viewerViewedState });1565}1566// No event for setting the file viewed state here.1567// Instead, wait until all the changes have been made and set the context at the end.1568this.setFileViewedState(n.path, n.viewerViewedState, false);1569});15701571hasNextPage = data.repository.pullRequest.files.pageInfo.hasNextPage;1572after = data.repository.pullRequest.files.pageInfo.endCursor;1573} while (hasNextPage);15741575if (changed.length) {1576this._onDidChangeFileViewedState.fire({ changed });1577}1578}15791580async markFileAsViewed(filePathOrSubpath: string): Promise<void> {1581const { mutate, schema } = await this.githubRepository.ensure();1582const fileName = filePathOrSubpath.startsWith(this.githubRepository.rootUri.path) ?1583filePathOrSubpath.substring(this.githubRepository.rootUri.path.length + 1) : filePathOrSubpath;1584await mutate<void>({1585mutation: schema.MarkFileAsViewed,1586variables: {1587input: {1588path: fileName,1589pullRequestId: this.graphNodeId,1590},1591},1592});15931594this.setFileViewedState(fileName, ViewedState.VIEWED, true);1595}15961597async unmarkFileAsViewed(filePathOrSubpath: string): Promise<void> {1598const { mutate, schema } = await this.githubRepository.ensure();1599const fileName = filePathOrSubpath.startsWith(this.githubRepository.rootUri.path) ?1600filePathOrSubpath.substring(this.githubRepository.rootUri.path.length + 1) : filePathOrSubpath;1601await mutate<void>({1602mutation: schema.UnmarkFileAsViewed,1603variables: {1604input: {1605path: fileName,1606pullRequestId: this.graphNodeId,1607},1608},1609});16101611this.setFileViewedState(fileName, ViewedState.UNVIEWED, true);1612}16131614async unmarkAllFilesAsViewed(): Promise<void[]> {1615return Promise.all(Array.from(this.fileChanges.keys()).map(change => this.unmarkFileAsViewed(change)));1616}16171618private setFileViewedState(fileSubpath: string, viewedState: ViewedState, event: boolean) {1619const filePath = vscode.Uri.joinPath(this.githubRepository.rootUri, fileSubpath).fsPath;1620switch (viewedState) {1621case ViewedState.DISMISSED: {1622this._viewedFiles.delete(filePath);1623this._unviewedFiles.delete(filePath);1624break;1625}1626case ViewedState.UNVIEWED: {1627this._viewedFiles.delete(filePath);1628this._unviewedFiles.add(filePath);1629break;1630}1631case ViewedState.VIEWED: {1632this._viewedFiles.add(filePath);1633this._unviewedFiles.delete(filePath);1634}1635}1636this._fileChangeViewedState[fileSubpath] = viewedState;1637if (event) {1638this._onDidChangeFileViewedState.fire({ changed: [{ fileName: fileSubpath, viewed: viewedState }] });1639}1640}16411642public getViewedFileStates() {1643return {1644viewed: this._viewedFiles,1645unviewed: this._unviewedFiles1646};1647}1648}164916501651