From c1a39200799cf9313e8ecae10b5c5f3e541b5a7d Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 5 May 2022 13:13:11 +0200 Subject: [PATCH 001/954] 0.42.0 (#3519) --- ThirdPartyNotices.txt | 13 ++++++++----- package.json | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 27b8255064..61a75191cc 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -1438,8 +1438,8 @@ SOFTWARE. is-plain-object 5.0.0 - MIT https://github.com/jonschlinkert/is-plain-object -Copyright (c) 2014-2017, Jon Schlinkert. -Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert). +Copyright (c) 2014-2017, Jon Schlinkert +Copyright (c) 2019, Jon Schlinkert (https://github.com/jonschlinkert) The MIT License (MIT) @@ -1763,8 +1763,9 @@ SOFTWARE. react-dom 16.14.0 - MIT https://reactjs.org/ +(c) Db (c) Copyright (c) 2013-present, Facebook, Inc. -Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) Facebook, Inc. and its affiliates MIT License @@ -1796,7 +1797,7 @@ SOFTWARE. react-is 16.13.1 - MIT https://reactjs.org/ -Copyright (c) Facebook, Inc. and its affiliates. +Copyright (c) Facebook, Inc. and its affiliates MIT License @@ -1894,6 +1895,7 @@ https://github.com/blesh/symbol-observable#readme Copyright (c) Ben Lesh Copyright (c) Sindre Sorhus (sindresorhus.com) +(c) Sindre Sorhus (https://sindresorhus.com) and Ben Lesh (https://github.com/benlesh) The MIT License (MIT) @@ -2028,6 +2030,7 @@ uuid 8.3.2 - MIT https://github.com/uuidjs/uuid#readme Copyright 2011, Sebastian Tschan https://blueimp.net +Copyright (c) 2010-2020 Robert Kieffer and other contributors Copyright (c) Paul Johnston 1999 - 2009 Other contributors Greg Holt, Andrew Kepert, Ydnar, Lostinet The MIT License (MIT) @@ -2100,7 +2103,7 @@ THE SOFTWARE. zen-observable 0.8.15 - MIT https://github.com/zenparsing/zen-observable -Copyright (c) 2018 +Copyright (c) 2018 zenparsing Kevin Copyright (c) 2018 zenparsing (Kevin Smith) diff --git a/package.json b/package.json index cce3ba21bc..482f4e85fc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "tokenInformation", "commentsResolvedState" ], - "version": "0.40.0", + "version": "0.42.0", "publisher": "GitHub", "engines": { "vscode": "^1.67.0" From 3051a868c3f8dc5b3ba20fe29b721bd4a0994a91 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 5 May 2022 13:46:08 +0200 Subject: [PATCH 002/954] Pull requests created from a fork on a topic branch aren't discovered. (#3512) Fixes #3511 --- src/github/folderRepositoryManager.ts | 31 ++--- src/github/githubRepository.ts | 32 +++-- src/github/graphql.ts | 8 ++ src/github/queries.gql | 168 ++++++++++++++------------ src/github/utils.ts | 4 +- src/test/view/prsTree.test.ts | 4 +- 6 files changed, 129 insertions(+), 118 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 3ab0a1bb70..f7bcefd9b8 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1800,25 +1800,18 @@ export class FolderRepositoryManager implements vscode.Disposable { return null; } - const headGitHubRepo = this.gitHubRepositories.find( - repo => repo.remote.remoteName === this.repository.state.HEAD?.upstream?.remote, - ); - - // Find the github repo that matches the upstream + // Search through each github repo to see if it has a PR with this head branch. for (const repo of this.gitHubRepositories) { - if (repo.remote.remoteName === this.repository.state.HEAD.upstream.remote) { - const matchingPullRequest = await repo.getPullRequestForBranch( - `${headGitHubRepo?.remote.owner}:${this.repository.state.HEAD.upstream.name}`, - ); - if (matchingPullRequest && matchingPullRequest.length > 0) { - return { - owner: repo.remote.owner, - repositoryName: repo.remote.repositoryName, - prNumber: matchingPullRequest[0].number, - model: matchingPullRequest[0], - }; - } - break; + const matchingPullRequest = await repo.getPullRequestForBranch( + this.repository.state.HEAD.upstream.name, + ); + if (matchingPullRequest) { + return { + owner: repo.remote.owner, + repositoryName: repo.remote.repositoryName, + prNumber: matchingPullRequest.number, + model: matchingPullRequest, + }; } } return null; @@ -1905,7 +1898,7 @@ export class FolderRepositoryManager implements vscode.Disposable { } createGitHubRepositoryFromOwnerName(owner: string, repositoryName: string): GitHubRepository { - const existing = this.findExistingGitHubRepository({owner, repositoryName}); + const existing = this.findExistingGitHubRepository({ owner, repositoryName }); if (existing) { return existing; } diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 76f80eef54..f1cbd9a119 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -25,6 +25,7 @@ import { MentionableUsersResponse, MilestoneIssuesResponse, PullRequestResponse, + PullRequestsResponse, ViewerPermissionResponse, } from './graphql'; import { IAccount, IMilestone, Issue, PullRequest, RepoAccessAndMergeMethods } from './interface'; @@ -364,26 +365,23 @@ export class GitHubRepository implements vscode.Disposable { return undefined; } - async getPullRequestForBranch(remoteAndBranch: string): Promise { + async getPullRequestForBranch(branch: string): Promise { try { Logger.debug(`Fetch pull requests for branch - enter`, GitHubRepository.ID); - const { octokit, remote } = await this.ensure(); - const result = await octokit.pulls.list({ - owner: remote.owner, - repo: remote.repositoryName, - head: remoteAndBranch + const { query, remote, schema } = await this.ensure(); + const { data } = await query({ + query: schema.PullRequestForHead, + variables: { + owner: remote.owner, + name: remote.repositoryName, + headRefName: branch, + }, }); - - const pullRequests = result.data - .map(pullRequest => { - return this.createOrUpdatePullRequestModel( - convertRESTPullRequestToRawPullRequest(pullRequest, this), - ); - }) - .filter(item => item !== null) as PullRequestModel[]; - Logger.debug(`Fetch pull requests for branch - done`, GitHubRepository.ID); - return pullRequests; + + if (data.repository.pullRequests.nodes.length > 0) { + return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.repository.pullRequests.nodes[0], this)); + } } catch (e) { Logger.appendLine(`Fetching pull requests for branch failed: ${e}`, GitHubRepository.ID); if (e.code === 404) { @@ -702,7 +700,7 @@ export class GitHubRepository implements vscode.Disposable { }, }); Logger.debug(`Fetch pull request ${id} - done`, GitHubRepository.ID); - return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data, this)); + return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.repository.pullRequest, this)); } catch (e) { Logger.appendLine(`GithubRepository> Unable to fetch PR: ${e}`); return; diff --git a/src/github/graphql.ts b/src/github/graphql.ts index cb669f7c75..51b16f39b4 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -504,6 +504,14 @@ export interface IssuesResponse { }; } +export interface PullRequestsResponse { + repository: { + pullRequests: { + nodes: PullRequest[] + } + } +} + export interface MaxIssueResponse { repository: { issues: { diff --git a/src/github/queries.gql b/src/github/queries.gql index 0ed41dad4c..cc4caad855 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -158,6 +158,86 @@ fragment ReviewThread on PullRequestReviewThread { } } +fragment PullRequestFragment on PullRequest { + number + url + state + body + bodyHTML + title + author { + login + url + avatarUrl + ... on User { + email + } + ... on Organization { + email + } + } + createdAt + updatedAt + headRef { + ...Ref + } + headRefName + headRefOid + headRepository { + owner { + login + } + url + } + baseRef { + ...Ref + } + baseRefName + baseRefOid + baseRepository { + owner { + login + } + url + } + labels(first: 50) { + nodes { + name + } + } + merged + mergeable + mergeStateStatus + id + databaseId + isDraft + milestone { + title + dueOn + createdAt + id + } + assignees(first: 10) { + nodes { + login + name + avatarUrl + url + email + } + } + suggestedReviewers { + isAuthor + isCommenter + reviewer { + login + avatarUrl + name + url + } + } +} + query TimelineEvents($owner: String!, $name: String!, $number: Int!, $last: Int = 150) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { @@ -290,83 +370,7 @@ query PullRequestComments($owner: String!, $name: String!, $number: Int!, $first query PullRequest($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { - number - url - state - body - bodyHTML - title - author { - login - url - avatarUrl - ... on User { - email - } - ... on Organization { - email - } - } - createdAt - updatedAt - headRef { - ...Ref - } - headRefName - headRefOid - headRepository { - owner { - login - } - url - } - baseRef { - ...Ref - } - baseRefName - baseRefOid - baseRepository { - owner { - login - } - url - } - labels(first: 50) { - nodes { - name - } - } - merged - mergeable - mergeStateStatus - id - databaseId - isDraft - milestone { - title - dueOn - createdAt - id - } - assignees(first: 10) { - nodes { - login - name - avatarUrl - url - email - } - } - suggestedReviewers { - isAuthor - isCommenter - reviewer { - login - avatarUrl - name - url - } - } + ...PullRequestFragment } } rateLimit { @@ -528,6 +532,16 @@ query PullRequestState($owner: String!, $name: String!, $number: Int!) { } } +query PullRequestForHead($owner: String!, $name: String!, $headRefName: String!) { + repository(owner: $owner, name: $name) { + pullRequests(first: 1, headRefName: $headRefName) { + nodes { + ...PullRequestFragment + } + } + } +} + mutation AddComment($input: AddPullRequestReviewCommentInput!) { addPullRequestReviewComment(input: $input) { comment { diff --git a/src/github/utils.ts b/src/github/utils.ts index faa6395534..4ffc581e3b 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -530,11 +530,9 @@ export function parseMergeability(mergeability: 'UNKNOWN' | 'MERGEABLE' | 'CONFL } export function parseGraphQLPullRequest( - pullRequest: GraphQL.PullRequestResponse, + graphQLPullRequest: GraphQL.PullRequest, githubRepository: GitHubRepository, ): PullRequest { - const graphQLPullRequest = pullRequest.repository.pullRequest; - return { id: graphQLPullRequest.databaseId, graphNodeId: graphQLPullRequest.id, diff --git a/src/test/view/prsTree.test.ts b/src/test/view/prsTree.test.ts index 7a09e3166d..095ced73f6 100644 --- a/src/test/view/prsTree.test.ts +++ b/src/test/view/prsTree.test.ts @@ -151,7 +151,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem0 = parseGraphQLPullRequest(pr0, gitHubRepository); + const prItem0 = parseGraphQLPullRequest(pr0.repository.pullRequest, gitHubRepository); const pullRequest0 = new PullRequestModel(telemetry, gitHubRepository, remote, prItem0); const pr1 = gitHubRepository.addGraphQLPullRequest(builder => { @@ -167,7 +167,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem1 = parseGraphQLPullRequest(pr1, gitHubRepository); + const prItem1 = parseGraphQLPullRequest(pr1.repository.pullRequest, gitHubRepository); const pullRequest1 = new PullRequestModel(telemetry, gitHubRepository, remote, prItem1); const repository = new MockRepository(); From 730f9dd8833a14717922cb7b4d39a390983eb307 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 5 May 2022 13:47:51 +0200 Subject: [PATCH 003/954] Formalize GitHub Authentication dependency (#3520) * Formalize GitHub Authentication dependency Fixes #3469 * Bump VS Code version --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 482f4e85fc..5d91ed24cc 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,14 @@ "version": "0.42.0", "publisher": "GitHub", "engines": { - "vscode": "^1.67.0" + "vscode": "^1.68.0" }, "categories": [ "Other" ], + "extensionDependencies": [ + "vscode.github-authentication" + ], "activationEvents": [ "*", "onCommand:github.api.preloadPullRequest", From c73f912351748f86204193f9882ed77643b229fc Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 5 May 2022 15:24:19 +0200 Subject: [PATCH 004/954] The current repository does not have a push remote for... (#3518) Fixes #3517 --- src/github/createPRViewProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 58c2b0e418..09ff02e5a9 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -349,7 +349,7 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs return false; } const testRemote = new Remote(localRemote.name, localRemote.pushUrl, new Protocol(localRemote.pushUrl)); - if ((testRemote.owner === compareOwner) && (testRemote.repositoryName === compareRepositoryName)) { + if ((testRemote.owner.toLowerCase() === compareOwner.toLowerCase()) && (testRemote.repositoryName.toLowerCase() === compareRepositoryName.toLowerCase())) { createdPushRemote = testRemote; return true; } From dc45665de7bce35d9f268e5ec06a932ca06be87b Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 5 May 2022 17:27:31 +0200 Subject: [PATCH 005/954] Clear validation error when initializing create view (#3521) --- src/github/createPRViewProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 09ff02e5a9..67f22b607e 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -268,6 +268,7 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs defaultTitle, defaultDescription, isDraft: false, + createError: '' }; this._compareBranch = this.defaultCompareBranch.name ?? ''; From a783bb8873b25e561242021f9162a6365a849d6f Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 May 2022 12:40:38 +0200 Subject: [PATCH 006/954] Creating a pull request doesn't commit message for PR description when the base branch has more commits (#3523) Fixes #3350 --- src/github/createPRViewProvider.ts | 92 ++++++++++++------------------ 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 67f22b607e..b101aeac0e 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -97,7 +97,7 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs super.show(); } - private async getTotalCommits(compareBranch: Branch, baseBranchName: string): Promise { + private async getTotalGitHubCommits(compareBranch: Branch, baseBranchName: string): Promise { const origin = await this._folderRepositoryManager.getOrigin(compareBranch); if (compareBranch.upstream) { @@ -110,19 +110,12 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs return total_commits; } - } else if (compareBranch.commit) { - // We can use the git API instead of the GitHub API - const baseBranch = await this._folderRepositoryManager.repository.getBranch(baseBranchName); - if (baseBranch.commit) { - const changes = await this._folderRepositoryManager.repository.diffBetween(baseBranch.commit, compareBranch.commit); - return changes.length; - } } - return 0; + return undefined; } - private async getTitle(compareBranch: Branch, baseBranch: string): Promise { + private async getTitleAndDescription(compareBranch: Branch, baseBranch: string): Promise<{ title: string, description: string }> { // Use same default as GitHub, if there is only one commit, use the commit, otherwise use the branch name, as long as it is not the default branch. // By default, the base branch we use for comparison is the base branch of origin. Compare this to the // compare branch if it has a GitHub remote. @@ -131,9 +124,12 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs let useBranchName = this._pullRequestDefaults.base === compareBranch.name; Logger.debug(`Compare branch name: ${compareBranch.name}, Base branch name: ${this._pullRequestDefaults.base}`, 'CreatePullRequestViewProvider'); try { - const totalCommits = await this.getTotalCommits(compareBranch, baseBranch); + const totalCommits = await this.getTotalGitHubCommits(compareBranch, baseBranch); Logger.debug(`Total commits: ${totalCommits}`, 'CreatePullRequestViewProvider'); - if (totalCommits > 1) { + if (totalCommits === undefined) { + // There is no upstream branch. Use the last commit as the title and description. + useBranchName = false; + } else if (totalCommits > 1) { const defaultBranch = await origin.getDefaultBranch(); useBranchName = defaultBranch !== compareBranch.name; } @@ -142,17 +138,29 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs Logger.debug(`Error while getting total commits: ${e}`, 'CreatePullRequestViewProvider'); } - if (useBranchName) { - const name = compareBranch.name; - return name - ? `${name.charAt(0).toUpperCase()}${name.slice(1)}` - : ''; - } else { - return compareBranch.name - ? titleAndBodyFrom(await this._folderRepositoryManager.getTipCommitMessage(compareBranch.name)) - .title - : ''; + const name = compareBranch.name; + const lastCommit = name ? titleAndBodyFrom(await this._folderRepositoryManager.getTipCommitMessage(name)) : undefined; + let title: string = ''; + let description: string = ''; + + // Set title + if (useBranchName && name) { + title = `${name.charAt(0).toUpperCase()}${name.slice(1)}`; + } else if (name && lastCommit) { + title = lastCommit.title; } + + // Set description + const pullRequestTemplate = await this.getPullRequestTemplate(); + if (pullRequestTemplate && lastCommit?.body) { + description = `${lastCommit.body}\n\n${pullRequestTemplate}`; + } else if (pullRequestTemplate) { + description = pullRequestTemplate; + } else if (lastCommit?.body && (this._pullRequestDefaults.base !== compareBranch.name)) { + description = lastCommit.body; + } + + return { title, description }; } private async getPullRequestTemplate(): Promise { @@ -170,32 +178,6 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs return undefined; } - private async getDescription(compareBranch: Branch, baseBranch: string): Promise { - // Try to match github's default, first look for template, then use commit body if available. - let commitMessage: string | undefined; - try { - const totalCommits = await this.getTotalCommits(compareBranch, baseBranch); - - // If there's just a single commit - if (totalCommits === 1 && compareBranch.name) { - commitMessage = titleAndBodyFrom(await this._folderRepositoryManager.getTipCommitMessage(compareBranch.name)).body; - } - } catch (e) { - // Ignore and show nothing for the commit message. - } - - const pullRequestTemplate = await this.getPullRequestTemplate(); - if (pullRequestTemplate && commitMessage) { - return `${commitMessage}\n\n${pullRequestTemplate}`; - } else if (pullRequestTemplate) { - return pullRequestTemplate; - } else if (commitMessage && (this._pullRequestDefaults.base !== compareBranch.name)) { - return commitMessage; - } else { - return ''; - } - } - public async initializeParams(reset: boolean = false): Promise { if (!this.defaultCompareBranch) { throw new DetachedHeadError(this._folderRepositoryManager.repository); @@ -214,12 +196,11 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs const defaultBaseBranch = this._pullRequestDefaults.base; - const [configuredGitHubRemotes, allGitHubRemotes, branchesForRemote, defaultTitle, defaultDescription] = await Promise.all([ + const [configuredGitHubRemotes, allGitHubRemotes, branchesForRemote, defaultTitleAndDescription] = await Promise.all([ this._folderRepositoryManager.getGitHubRemotes(), this._folderRepositoryManager.getAllGitHubRemotes(), defaultOrigin.listBranches(this._pullRequestDefaults.owner, this._pullRequestDefaults.repo), - this.getTitle(this.defaultCompareBranch, defaultBaseBranch), - this.getDescription(this.defaultCompareBranch, defaultBaseBranch), + this.getTitleAndDescription(this.defaultCompareBranch, defaultBaseBranch), ]); const configuredRemotes: RemoteInfo[] = configuredGitHubRemotes.map(remote => { @@ -265,8 +246,8 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs defaultCompareBranch: this.defaultCompareBranch.name ?? '', branchesForRemote, branchesForCompare, - defaultTitle, - defaultDescription, + defaultTitle: defaultTitleAndDescription.title, + defaultDescription: defaultTitleAndDescription.description, isDraft: false, createError: '' }; @@ -411,9 +392,8 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs } compareBranch = compareBranch ?? await this._folderRepositoryManager.repository.getBranch(this._compareBranch); - const title = await this.getTitle(compareBranch, this._baseBranch); - const description = await this.getDescription(compareBranch, this._baseBranch); - return this._replyMessage(message, { title, description }); + const titleAndDescription = await this.getTitleAndDescription(compareBranch, this._baseBranch); + return this._replyMessage(message, { title: titleAndDescription.title, description: titleAndDescription.description }); } protected async _onDidReceiveMessage(message: IRequestMessage) { From 4722017179d954286d57d71da8257eda5f86f89a Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 May 2022 15:33:21 +0200 Subject: [PATCH 007/954] Cannot exit review mode when default branch has same name as head branch of a PR (#3526) Fixes #3525 --- src/common/githubRef.ts | 3 ++- src/github/folderRepositoryManager.ts | 7 ++++++- src/github/interface.ts | 2 ++ src/github/pullRequestModel.ts | 4 ++-- src/github/queries.gql | 2 +- src/github/utils.ts | 8 +++++++- src/view/reviewManager.ts | 1 + 7 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/common/githubRef.ts b/src/common/githubRef.ts index dff9bf55d6..89ad60b069 100644 --- a/src/common/githubRef.ts +++ b/src/common/githubRef.ts @@ -7,7 +7,8 @@ import { Protocol } from './protocol'; export class GitHubRef { public repositoryCloneUrl: Protocol; - constructor(public ref: string, public label: string, public sha: string, repositoryCloneUrl: string) { + constructor(public ref: string, public label: string, public sha: string, repositoryCloneUrl: string, + public readonly owner: string, public readonly name: string) { this.repositoryCloneUrl = new Protocol(repositoryCloneUrl); } } diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index f7bcefd9b8..1052fd2af3 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1800,12 +1800,17 @@ export class FolderRepositoryManager implements vscode.Disposable { return null; } + const headGitHubRepo = this.gitHubRepositories.find( + repo => repo.remote.remoteName === this.repository.state.HEAD?.upstream?.remote, + ); + // Search through each github repo to see if it has a PR with this head branch. for (const repo of this.gitHubRepositories) { const matchingPullRequest = await repo.getPullRequestForBranch( this.repository.state.HEAD.upstream.name, ); - if (matchingPullRequest) { + if (matchingPullRequest && (matchingPullRequest.head?.owner === headGitHubRepo?.remote.owner) + && (matchingPullRequest.head?.name === headGitHubRepo?.remote.repositoryName)) { return { owner: repo.remote.owner, repositoryName: repo.remote.repositoryName, diff --git a/src/github/interface.ts b/src/github/interface.ts index c2babc13a4..1b6d5154c9 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -62,6 +62,8 @@ export interface MergePullRequest { export interface IRepository { cloneUrl: string; + owner: string; + name: string; } export interface IGitHubRef { diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index dfbe4ab259..28c04c174a 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -205,14 +205,14 @@ export class PullRequestModel extends IssueModel implements IPullRe this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; } if (item.head) { - this.head = new GitHubRef(item.head.ref, item.head.label, item.head.sha, item.head.repo.cloneUrl); + this.head = new GitHubRef(item.head.ref, item.head.label, item.head.sha, item.head.repo.cloneUrl, item.head.repo.owner, item.head.repo.name); } if (item.isRemoteBaseDeleted != null) { this.isRemoteBaseDeleted = item.isRemoteBaseDeleted; } if (item.base) { - this.base = new GitHubRef(item.base.ref, item.base!.label, item.base!.sha, item.base!.repo.cloneUrl); + this.base = new GitHubRef(item.base.ref, item.base!.label, item.base!.sha, item.base!.repo.cloneUrl, item.base.repo.owner, item.base.repo.name); } } diff --git a/src/github/queries.gql b/src/github/queries.gql index cc4caad855..01b2223ca6 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -534,7 +534,7 @@ query PullRequestState($owner: String!, $name: String!, $number: Int!) { query PullRequestForHead($owner: String!, $name: String!, $headRefName: String!) { repository(owner: $owner, name: $name) { - pullRequests(first: 1, headRefName: $headRefName) { + pullRequests(first: 1, headRefName: $headRefName, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { ...PullRequestFragment } diff --git a/src/github/utils.ts b/src/github/utils.ts index 4ffc581e3b..d0ac5a5e1f 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -193,7 +193,11 @@ export function convertRESTHeadToIGitHubRef(head: OctokitCommon.PullsListRespons label: head.label, ref: head.ref, sha: head.sha, - repo: { cloneUrl: head.repo.clone_url }, + repo: { + cloneUrl: head.repo.clone_url, + owner: head.repo.owner!.login, + name: head.repo.name + }, }; } @@ -472,6 +476,8 @@ function parseRef(refName: string, oid: string, repository?: GraphQL.RefReposito sha: oid, repo: { cloneUrl: repository.url, + owner: repository.owner.login, + name: refName }, }; } diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index d6e1c00e55..e692d7c07d 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -314,6 +314,7 @@ export class ReviewManager { matchingPullRequestMetadata.prNumber, ); if (!pr || !pr.isResolved()) { + this.clear(true); this._prNumber = undefined; Logger.appendLine('Review> This PR is no longer valid'); return; From 86bdeefca9da28c05c4a6886f33e3242086976be Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 May 2022 16:12:29 +0200 Subject: [PATCH 008/954] Don't pick the default branch in the "delete branches" picker (#3527) --- src/github/folderRepositoryManager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 1052fd2af3..e1bbad16be 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1607,8 +1607,12 @@ export class FolderRepositoryManager implements vscode.Disposable { // Check local branches const results = await this.getBranchDeletionItems(); + const defaults = await this.getPullRequestDefaults(); quickPick.items = results; - quickPick.selectedItems = results.filter(result => result.picked); + quickPick.selectedItems = results.filter(result => { + // Do not pick the default branch for the repo. + return result.picked && !((result.label === defaults.base) && (result.metadata.owner === defaults.owner) && (result.metadata.repositoryName === defaults.repo)); + }); quickPick.busy = false; let firstStep = true; From ca3322d5247bd290ed4d65c6ff425e83ddad5fd9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 6 May 2022 16:22:44 +0200 Subject: [PATCH 009/954] Support auto merge (#3524) * Support auto merge Fixes #3085 * Fix merge error * mergeMethod not always getting set --- common/views.ts | 10 +++++++ src/github/activityBarViewProvider.ts | 7 ++--- src/github/createPRViewProvider.ts | 23 +++++++++----- src/github/githubRepository.ts | 6 ++-- src/github/interface.ts | 1 + src/github/pullRequestModel.ts | 26 ++++++++++++++++ src/github/pullRequestOverview.ts | 10 +++---- src/github/queries.gql | 8 +++++ webviews/common/createContext.ts | 27 +++++++++-------- webviews/components/automergeSelect.tsx | 38 ++++++++++++++++++++++++ webviews/components/merge.tsx | 10 +++---- webviews/createPullRequestView/app.tsx | 3 ++ webviews/createPullRequestView/index.css | 16 +++++++++- 13 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 webviews/components/automergeSelect.tsx diff --git a/common/views.ts b/common/views.ts index 8aef8e5cf6..d1726b1d4e 100644 --- a/common/views.ts +++ b/common/views.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MergeMethod, MergeMethodsAvailability } from '../src/github/interface'; + export interface RemoteInfo { owner: string; repositoryName: string; @@ -32,6 +34,12 @@ export interface CreateParams { validate?: boolean; showTitleValidationError?: boolean; createError?: string; + + autoMerge?: boolean; + mergeMethod?: MergeMethod; + allowAutoMerge?: boolean; + defaultMergeMethod?: MergeMethod; + mergeMethodsAvailability?: MergeMethodsAvailability; } export interface ScrollPosition { @@ -49,4 +57,6 @@ export interface CreatePullRequest { compareOwner: string; compareRepo: string; draft: boolean; + autoMerge: boolean; + mergeMethod: MergeMethod; } \ No newline at end of file diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index 986b6b9254..65d558c084 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -10,7 +10,7 @@ import { ReviewEvent as CommonReviewEvent } from '../common/timelineEvent'; import { formatError } from '../common/utils'; import { getNonce, IRequestMessage, WebviewViewBase } from '../common/webview'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GithubItemStateEnum, MergeMethod, ReviewEvent, ReviewState } from './interface'; +import { GithubItemStateEnum, ReviewEvent, ReviewState } from './interface'; import { PullRequestModel } from './pullRequestModel'; import { getDefaultMergeMethod } from './pullRequestOverview'; import { PullRequestView } from './pullRequestOverviewCommon'; @@ -146,10 +146,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W const hasWritePermission = repositoryAccess!.hasWritePermission; const mergeMethodsAvailability = repositoryAccess!.mergeMethodsAvailability; const canEdit = hasWritePermission || this._item.canEdit(); - const preferredMergeMethod = vscode.workspace - .getConfiguration('githubPullRequests') - .get('defaultMergeMethod'); - const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability, preferredMergeMethod); + const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability); const currentUser = this._folderRepositoryManager.getCurrentUser(this._item); this._existingReviewers = parseReviewers( requestedReviewers ?? [], diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index b101aeac0e..5e8cc32e46 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -17,8 +17,9 @@ import { PullRequestDefaults, titleAndBodyFrom, } from './folderRepositoryManager'; -import { PullRequestGitHelper } from './pullRequestGitHelper'; +import { RepoAccessAndMergeMethods } from './interface'; import { PullRequestModel } from './pullRequestModel'; +import { getDefaultMergeMethod } from './pullRequestOverview'; export class CreatePullRequestViewProvider extends WebviewViewBase implements vscode.WebviewViewProvider { public readonly viewType = 'github:createPullRequest'; @@ -178,6 +179,11 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs return undefined; } + private async getMergeConfiguration(owner: string, name: string): Promise { + const repo = this._folderRepositoryManager.createGitHubRepositoryFromOwnerName(owner, name); + return repo.getRepoAccessAndMergeMethods(); + } + public async initializeParams(reset: boolean = false): Promise { if (!this.defaultCompareBranch) { throw new DetachedHeadError(this._folderRepositoryManager.repository); @@ -196,11 +202,12 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs const defaultBaseBranch = this._pullRequestDefaults.base; - const [configuredGitHubRemotes, allGitHubRemotes, branchesForRemote, defaultTitleAndDescription] = await Promise.all([ + const [configuredGitHubRemotes, allGitHubRemotes, branchesForRemote, defaultTitleAndDescription, mergeConfiguration] = await Promise.all([ this._folderRepositoryManager.getGitHubRemotes(), this._folderRepositoryManager.getAllGitHubRemotes(), defaultOrigin.listBranches(this._pullRequestDefaults.owner, this._pullRequestDefaults.repo), this.getTitleAndDescription(this.defaultCompareBranch, defaultBaseBranch), + this.getMergeConfiguration(defaultBaseRemote.owner, defaultBaseRemote.repositoryName) ]); const configuredRemotes: RemoteInfo[] = configuredGitHubRemotes.map(remote => { @@ -249,6 +256,9 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs defaultTitle: defaultTitleAndDescription.title, defaultDescription: defaultTitleAndDescription.description, isDraft: false, + defaultMergeMethod: getDefaultMergeMethod(mergeConfiguration.mergeMethodsAvailability), + allowAutoMerge: mergeConfiguration.viewerCanAutoMerge, + mergeMethodsAvailability: mergeConfiguration.mergeMethodsAvailability, createError: '' }; @@ -300,6 +310,7 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs this._onDidChangeCompareRemote.fire({ owner, repositoryName }); } + // TODO: if base is change need to update auto merge return this._replyMessage(message, { branches: newBranches, defaultBranch: newBranch }); } @@ -363,12 +374,10 @@ export class CreatePullRequestViewProvider extends WebviewViewBase implements vs if (!createdPR) { this._throwError(message, 'There must be a difference in commits to create a pull request.'); } else { + if (message.args.autoMerge) { + await createdPR.enableAutoMerge(message.args.mergeMethod); + } await this._replyMessage(message, {}); - await PullRequestGitHelper.associateBranchWithPullRequest( - this._folderRepositoryManager.repository, - createdPR, - compareBranchName, - ); this._onDone.fire(createdPR); } } catch (e) { diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index f1cbd9a119..a6a91d0ce3 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -284,16 +284,17 @@ export class GitHubRepository implements vscode.Disposable { repo: remote.repositoryName, }); Logger.debug(`Fetch repo permissions and available merge methods - done`, GitHubRepository.ID); - + const hasWritePermission = data.permissions?.push ?? false; return { // Users with push access to repo have rights to merge/close PRs, // edit title/description, assign reviewers/labels etc. - hasWritePermission: data.permissions?.push ?? false, + hasWritePermission, mergeMethodsAvailability: { merge: data.allow_merge_commit ?? false, squash: data.allow_squash_merge ?? false, rebase: data.allow_rebase_merge ?? false, }, + viewerCanAutoMerge: ((data as any).allow_auto_merge && hasWritePermission) ?? false }; } catch (e) { Logger.appendLine(`GitHubRepository> Fetching repo permissions and available merge methods failed: ${e}`); @@ -306,6 +307,7 @@ export class GitHubRepository implements vscode.Disposable { squash: true, rebase: true, }, + viewerCanAutoMerge: false }; } diff --git a/src/github/interface.ts b/src/github/interface.ts index 1b6d5154c9..30f34d8281 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -144,6 +144,7 @@ export type MergeMethodsAvailability = { export type RepoAccessAndMergeMethods = { hasWritePermission: boolean; mergeMethodsAvailability: MergeMethodsAvailability; + viewerCanAutoMerge: boolean; }; export interface User extends IAccount { diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 28c04c174a..365224e1c0 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -49,6 +49,7 @@ import { IAccount, IRawFileChange, ISuggestedReviewer, + MergeMethod, PullRequest, PullRequestChecks, PullRequestMergeability, @@ -1377,6 +1378,31 @@ export class PullRequestModel extends IssueModel implements IPullRe } } + async enableAutoMerge(mergeMethod: MergeMethod): Promise { + try { + const { mutate, schema } = await this.githubRepository.ensure(); + const { data } = await mutate({ + mutation: schema.EnablePullRequestAutoMerge, + variables: { + input: { + mergeMethod: mergeMethod.toUpperCase(), + pullRequestId: this.graphNodeId + } + } + }); + + if (!data) { + throw new Error('Enable auto-merge failed.'); + } + } catch (e) { + if (e.message === 'GraphQL error: ["Pull request Pull request is in clean status"]') { + vscode.window.showWarningMessage('Unable to enable auto-merge. Pull request status checks are already green.'); + } else { + throw e; + } + } + } + async initializePullRequestFileViewState(): Promise { const { query, schema, remote } = await this.githubRepository.ensure(); diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 249bd42290..146878ea4e 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -189,10 +189,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel('defaultMergeMethod'); - const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability, preferredMergeMethod); + + const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability); this._existingReviewers = parseReviewers(requestedReviewers!, timelineEvents!, pullRequest.author); const currentUser = this._folderRepositoryManager.getCurrentUser(this._item); @@ -769,13 +767,13 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel('defaultMergeMethod'); // Use default merge method specified by user if it is available if (userPreferred && methodsAvailability.hasOwnProperty(userPreferred) && methodsAvailability[userPreferred]) { return userPreferred; } const methods: MergeMethod[] = ['merge', 'squash', 'rebase']; - // GitHub requires to have at leas one merge method to be enabled; use first available as default + // GitHub requires to have at least one merge method to be enabled; use first available as default return methods.find(method => methodsAvailability[method])!; } diff --git a/src/github/queries.gql b/src/github/queries.gql index 01b2223ca6..544db461fe 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -989,6 +989,14 @@ mutation UnresolveReviewThread($input: UnresolveReviewThreadInput!) { } } +mutation EnablePullRequestAutoMerge($input: EnablePullRequestAutoMergeInput!) { + enablePullRequestAutoMerge(input: $input) { + pullRequest { + id + } + } +} + mutation MarkFileAsViewed($input: MarkFileAsViewedInput!) { markFileAsViewed(input: $input) { pullRequest { diff --git a/webviews/common/createContext.ts b/webviews/common/createContext.ts index bd24608e49..5c229fbb24 100644 --- a/webviews/common/createContext.ts +++ b/webviews/common/createContext.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createContext } from 'react'; -import { CreateParams, ScrollPosition } from '../../common/views'; +import { CreateParams, CreatePullRequest, ScrollPosition } from '../../common/views'; import { getMessageHandler, MessageHandler, vscode } from './message'; const defaultCreateParams: CreateParams = { @@ -108,19 +108,22 @@ export class CreatePRContext { public submit = async (): Promise => { try { + const args: CreatePullRequest = { + title: this.createParams.pendingTitle, + body: this.createParams.pendingDescription, + owner: this.createParams.baseRemote.owner, + repo: this.createParams.baseRemote.repositoryName, + base: this.createParams.baseBranch, + compareBranch: this.createParams.compareBranch, + compareOwner: this.createParams.compareRemote.owner, + compareRepo: this.createParams.compareRemote.repositoryName, + draft: this.createParams.isDraft, + autoMerge: this.createParams.autoMerge, + mergeMethod: this.createParams.mergeMethod + }; await this.postMessage({ command: 'pr.create', - args: { - title: this.createParams.pendingTitle, - body: this.createParams.pendingDescription, - owner: this.createParams.baseRemote.owner, - repo: this.createParams.baseRemote.repositoryName, - base: this.createParams.baseBranch, - compareBranch: this.createParams.compareBranch, - compareOwner: this.createParams.compareRemote.owner, - compareRepo: this.createParams.compareRemote.repositoryName, - draft: this.createParams.isDraft, - }, + args, }); vscode.setState(defaultCreateParams); } catch (e) { diff --git a/webviews/components/automergeSelect.tsx b/webviews/components/automergeSelect.tsx new file mode 100644 index 0000000000..b5107aa7d1 --- /dev/null +++ b/webviews/components/automergeSelect.tsx @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as React from 'react'; +import { CreateParams } from '../../common/views'; +import { MergeMethod } from '../../src/github/interface'; +import PullRequestContext from '../common/createContext'; +import { MergeSelect } from './merge'; + +export const AutoMerge = (createParams: CreateParams) => { + if (!createParams.allowAutoMerge) { + return null; + } + const ctx = React.useContext(PullRequestContext); + const select = React.useRef(); + + return
+
+ ctx.updateState({ autoMerge: !createParams.autoMerge, mergeMethod: select.current.value as MergeMethod })} + > +
+ +
+ { + ctx.updateState({ mergeMethod: select.current.value as MergeMethod }); + }}/> +
+
; +}; diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 5f6a0c88f1..d2c9e4ecee 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React, { useCallback, useContext, useEffect, useReducer, useRef, useState } from 'react'; +import React, { ChangeEventHandler, useCallback, useContext, useEffect, useReducer, useRef, useState } from 'react'; import { groupBy } from '../../src/common/utils'; import { GithubItemStateEnum, MergeMethod, PullRequestMergeability } from '../../src/github/interface'; import { PullRequest } from '../common/cache'; @@ -344,11 +344,11 @@ const MERGE_METHODS = { rebase: 'Rebase and Merge', }; -type MergeSelectProps = Pick & Pick; +type MergeSelectProps = Pick & Pick & {onChange?: ChangeEventHandler}; -const MergeSelect = React.forwardRef( - ({ defaultMergeMethod, mergeMethodsAvailability: avail }: MergeSelectProps, ref) => ( - {Object.entries(MERGE_METHODS).map(([method, text]) => (