Skip to content

Commit 1be7acd

Browse files
Jackson KearlRachel Macfarlane
authored andcommitted
Provide "pending" mergeability state, wherein updates are polled, fixes #1412
1 parent 5a5ddb7 commit 1be7acd

12 files changed

Lines changed: 131 additions & 22 deletions

File tree

preview-src/cache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { vscode } from './message';
7-
import { PullRequestStateEnum, IAccount, ReviewState, ILabel, MergeMethod, MergeMethodsAvailability } from '../src/github/interface';
7+
import { PullRequestStateEnum, IAccount, ReviewState, ILabel, MergeMethod, MergeMethodsAvailability, PullRequestMergeability } from '../src/github/interface';
88
import { TimelineEvent } from '../src/common/timelineEvent';
99
import { ReposGetCombinedStatusForRefResponse } from '@octokit/rest';
1010

@@ -33,7 +33,7 @@ export interface PullRequest {
3333
pendingCommentText?: string;
3434
pendingCommentDrafts?: { [key: string]: string; };
3535
status: ReposGetCombinedStatusForRefResponse;
36-
mergeable: boolean;
36+
mergeable: PullRequestMergeability;
3737
defaultMergeMethod: MergeMethod;
3838
mergeMethodsAvailability: MergeMethodsAvailability;
3939
supportsGraphQl: boolean;

preview-src/context.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ export class PRContext {
3535
public refresh = () =>
3636
this.postMessage({ command: 'pr.refresh' })
3737

38+
public checkMergeability = () =>
39+
this.postMessage({ command: 'pr.checkMergeability' })
40+
3841
public merge = (args: { title: string, description: string, method: MergeMethod }) =>
3942
this.postMessage({ command: 'pr.merge', args })
4043

preview-src/merge.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import * as React from 'react';
22
import { PullRequest } from './cache';
33
import PullRequestContext from './context';
44
import { useContext, useReducer, useRef, useState, useEffect, useCallback } from 'react';
5-
import { PullRequestStateEnum, MergeMethod } from '../src/github/interface';
5+
import { PullRequestStateEnum, MergeMethod, PullRequestMergeability } from '../src/github/interface';
66
import { checkIcon, deleteIcon, pendingIcon, alertIcon } from './icon';
77
import { Avatar, } from './user';
88
import { nbsp } from './space';
99
import { groupBy } from '../src/common/utils';
1010

1111
export const StatusChecks = (pr: PullRequest) => {
12-
const { state, status, mergeable } = pr;
12+
const { state, status, mergeable: _mergeable } = pr;
1313
const [showDetails, toggleDetails] = useReducer(
1414
show => !show,
1515
status.statuses.some(s => s.state === 'failure')) as [boolean, () => void];
@@ -22,6 +22,18 @@ export const StatusChecks = (pr: PullRequest) => {
2222
}
2323
}, status.statuses);
2424

25+
const [mergeable, setMergeability] = useState(_mergeable);
26+
const { checkMergeability } = useContext(PullRequestContext);
27+
28+
useEffect(() => {
29+
const handle = setInterval(async () => {
30+
if (mergeable === PullRequestMergeability.Unknown) {
31+
setMergeability(await checkMergeability());
32+
}
33+
}, 3000);
34+
return () => clearInterval(handle);
35+
});
36+
2537
return <div id='status-checks'>{
2638
state === PullRequestStateEnum.Merged
2739
?
@@ -56,22 +68,26 @@ export const StatusChecks = (pr: PullRequest) => {
5668
: null
5769
}
5870
<MergeStatus mergeable={mergeable} />
59-
<PrActions {...pr} />
71+
<PrActions {...{...pr, mergeable}} />
6072
</>
6173
}</div>;
6274
};
6375

6476
export default StatusChecks;
6577

66-
export const MergeStatus = ({ mergeable }: Pick<PullRequest, 'mergeable'>) =>
67-
<div className='status-item status-section'>
68-
{mergeable ? checkIcon : deleteIcon}
78+
export const MergeStatus = ({ mergeable }: Pick<PullRequest, 'mergeable'>) => {
79+
return <div className='status-item status-section'>
80+
{mergeable === PullRequestMergeability.Mergeable ? checkIcon :
81+
mergeable === PullRequestMergeability.NotMergeable ? deleteIcon : pendingIcon}
6982
<div>{
70-
mergeable
83+
mergeable === PullRequestMergeability.Mergeable
7184
? 'This branch has no conflicts with the base branch'
72-
: 'This branch has conflicts that must be resolved'
85+
: mergeable === PullRequestMergeability.NotMergeable
86+
? 'This branch has conflicts that must be resolved'
87+
: 'Checking if this branch can be merged...'
7388
}</div>
7489
</div>;
90+
};
7591

7692
export const ReadyForReview = () => {
7793
const [isBusy, setBusy] = useState(false);
@@ -120,7 +136,7 @@ export const PrActions = (pr: PullRequest) => {
120136
? hasWritePermission || canEdit
121137
? <ReadyForReview/>
122138
: null
123-
: mergeable && hasWritePermission
139+
: mergeable === PullRequestMergeability.Mergeable && hasWritePermission
124140
? <Merge {...pr} />
125141
: null;
126142
};

preview-src/test/builder/pullRequest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createBuilderClass } from '../../../src/test/builders/base';
22
import { PullRequest } from '../../cache';
3-
import { PullRequestStateEnum } from '../../../src/github/interface';
3+
import { PullRequestStateEnum, PullRequestMergeability } from '../../../src/github/interface';
44
import { CombinedStatusBuilder } from '../../../src/test/builders/rest/combinedStatusBuilder';
55

66
import { AccountBuilder } from './account';
@@ -26,7 +26,7 @@ export const PullRequestBuilder = createBuilderClass<PullRequest>()({
2626
pendingCommentText: {default: null},
2727
pendingCommentDrafts: {default: null},
2828
status: {linked: CombinedStatusBuilder},
29-
mergeable: {default: true},
29+
mergeable: {default: PullRequestMergeability.Mergeable},
3030
defaultMergeMethod: {default: 'merge'},
3131
mergeMethodsAvailability: {default: {merge: true, squash: true, rebase: true}},
3232
supportsGraphQl: {default: true},

resources/icons/dot.svg

Lines changed: 1 addition & 1 deletion
Loading

src/github/enterprise.gql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,20 @@ query PullRequest($owner: String!, $name: String!, $number: Int!) {
258258
}
259259
}
260260

261+
query PullRequestMergeability($owner: String!, $name: String!, $number: Int!) {
262+
repository(owner: $owner, name: $name) {
263+
pullRequest(number: $number) {
264+
mergeable
265+
}
266+
}
267+
rateLimit {
268+
limit
269+
cost
270+
remaining
271+
resetAt
272+
}
273+
}
274+
261275
query PullRequestState($owner: String!, $name: String!, $number: Int!) {
262276
repository(owner: $owner, name: $name) {
263277
pullRequest(number: $number) {

src/github/githubRepository.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import * as vscode from 'vscode';
77
import Octokit = require('@octokit/rest');
88
import Logger from '../common/logger';
99
import { Remote, parseRemote } from '../common/remote';
10-
import { IAccount, RepoAccessAndMergeMethods } from './interface';
10+
import { IAccount, RepoAccessAndMergeMethods, PullRequestMergeability } from './interface';
1111
import { PullRequestModel } from './pullRequestModel';
1212
import { CredentialStore, GitHub } from './credentials';
1313
import { AuthenticationError } from '../common/authentication';
1414
import { QueryOptions, MutationOptions, ApolloQueryResult, NetworkStatus, FetchResult } from 'apollo-boost';
1515
import { PRCommentController } from '../view/prCommentController';
16-
import { convertRESTPullRequestToRawPullRequest, parseGraphQLPullRequest } from './utils';
16+
import { convertRESTPullRequestToRawPullRequest, parseGraphQLPullRequest, parseMergeability } from './utils';
1717
import { PullRequestResponse, MentionableUsersResponse } from './graphql';
1818

1919
export const PULL_REQUEST_PAGE_SIZE = 20;
@@ -356,6 +356,31 @@ export class GitHubRepository implements vscode.Disposable {
356356
}
357357
}
358358

359+
async getPullRequestMergeability(id: number): Promise<PullRequestMergeability> {
360+
try {
361+
Logger.debug(`Fetch pull request mergeability ${id} - enter`, GitHubRepository.ID);
362+
const { supportsGraphQl, query, remote, schema } = await this.ensure();
363+
364+
if (supportsGraphQl) {
365+
const { data } = await query<PullRequestResponse>({
366+
query: schema.PullRequestMergeability,
367+
variables: {
368+
owner: remote.owner,
369+
name: remote.repositoryName,
370+
number: id
371+
}
372+
});
373+
Logger.debug(`Fetch pull request mergeability ${id} - done`, GitHubRepository.ID);
374+
return parseMergeability(data.repository.pullRequest.mergeable);
375+
} else {
376+
throw Error('GitHub repos without v4 API support (GraphQL) are no longer supported.');
377+
}
378+
} catch (e) {
379+
Logger.appendLine(`GithubRepository> Unable to fetch PR Mergeability: ${e}`);
380+
return PullRequestMergeability.Unknown;
381+
}
382+
}
383+
359384
async deleteBranch(pullRequestModel: PullRequestModel): Promise<void> {
360385
const { octokit } = await this.ensure();
361386

src/github/interface.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export enum PullRequestStateEnum {
2121
Closed,
2222
}
2323

24+
export enum PullRequestMergeability {
25+
Mergeable,
26+
NotMergeable,
27+
Unknown
28+
}
29+
2430
export interface ReviewState {
2531
reviewer: IAccount;
2632
state: string;
@@ -72,7 +78,7 @@ export interface PullRequest {
7278
user: IAccount;
7379
labels: ILabel[];
7480
merged: boolean;
75-
mergeable?: boolean;
81+
mergeable: PullRequestMergeability;
7682
isDraft?: boolean;
7783
}
7884

src/github/pullRequestManager.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IComment } from '../common/comment';
1111
import { Remote, parseRepositoryRemotes } from '../common/remote';
1212
import { TimelineEvent, EventType, ReviewEvent as CommonReviewEvent, isReviewEvent, isCommitEvent } from '../common/timelineEvent';
1313
import { GitHubRepository, PullRequestData } from './githubRepository';
14-
import { IPullRequestsPagingOptions, PRType, ReviewEvent, IPullRequestEditData, PullRequest, IRawFileChange, IAccount, ILabel, RepoAccessAndMergeMethods } from './interface';
14+
import { IPullRequestsPagingOptions, PRType, ReviewEvent, IPullRequestEditData, PullRequest, IRawFileChange, IAccount, ILabel, RepoAccessAndMergeMethods, PullRequestMergeability } from './interface';
1515
import { PullRequestGitHelper, PullRequestMetadata } from './pullRequestGitHelper';
1616
import { PullRequestModel, IResolvedPullRequestModel } from './pullRequestModel';
1717
import { GitHubManager } from '../authentication/githubServer';
@@ -1922,6 +1922,19 @@ export class PullRequestManager implements vscode.Disposable {
19221922
return pr;
19231923
}
19241924

1925+
async resolvePullRequestMergeability(owner: string, repositoryName: string, pullRequestNumber: number): Promise<PullRequestMergeability> {
1926+
const githubRepo = this._githubRepositories.find(repo =>
1927+
repo.remote.owner.toLowerCase() === owner.toLowerCase() && repo.remote.repositoryName.toLowerCase() === repositoryName.toLowerCase()
1928+
);
1929+
1930+
if (!githubRepo) {
1931+
return PullRequestMergeability.Unknown;
1932+
}
1933+
1934+
return githubRepo.getPullRequestMergeability(pullRequestNumber);
1935+
1936+
}
1937+
19251938
async getMatchingPullRequestMetadataForBranch() {
19261939
if (!this.repository || !this.repository.state.HEAD || !this.repository.state.HEAD.name) {
19271940
return null;

src/github/pullRequestOverview.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import * as path from 'path';
88
import * as vscode from 'vscode';
99
import Octokit = require('@octokit/rest');
10-
import { PullRequestStateEnum, ReviewEvent, ReviewState, ILabel, IAccount, MergeMethodsAvailability, MergeMethod } from './interface';
10+
import { PullRequestStateEnum, ReviewEvent, ReviewState, ILabel, IAccount, MergeMethodsAvailability, MergeMethod, PullRequestMergeability } from './interface';
1111
import { onDidUpdatePR } from '../commands';
1212
import { formatError } from '../common/utils';
1313
import { GitErrorCodes } from '../api/api';
@@ -124,6 +124,14 @@ export class PullRequestOverviewPanel {
124124
}, null, this._disposables);
125125
}
126126

127+
private async checkMergeability(): Promise<PullRequestMergeability> {
128+
return this._pullRequestManager.resolvePullRequestMergeability(
129+
this._pullRequest.remote.owner,
130+
this._pullRequest.remote.repositoryName,
131+
this._pullRequest.prNumber
132+
);
133+
}
134+
127135
public async refreshPanel(): Promise<void> {
128136
if (this._panel && this._panel.visible) {
129137
this.update(this._pullRequest, this._descriptionNode);
@@ -324,6 +332,8 @@ export class PullRequestOverviewPanel {
324332
return this.openDiff(message);
325333
case 'pr.edit-title':
326334
return this.editTitle(message);
335+
case 'pr.checkMergeability':
336+
return this._replyMessage(message, await this.checkMergeability());
327337
case 'pr.refresh':
328338
this.refreshPanel();
329339
return;

0 commit comments

Comments
 (0)