Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Loading Pull Request List via GraphQL #1022

Closed
wants to merge 11 commits into from
134 changes: 21 additions & 113 deletions src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import * as vscode from 'vscode';
import * as Octokit from '@octokit/rest';
import Logger from '../common/logger';
import { Remote, parseRemote } from '../common/remote';
import { PRType, IGitHubRepository, IAccount, MergeMethodsAvailability } from './interface';
import { IGitHubRepository, IAccount, MergeMethodsAvailability } from './interface';
import { PullRequestModel } from './pullRequestModel';
import { CredentialStore, GitHub } from './credentials';
import { AuthenticationError } from '../common/authentication';
import { QueryOptions, MutationOptions, ApolloQueryResult, NetworkStatus, FetchResult } from 'apollo-boost';
import { PRDocumentCommentProvider, PRDocumentCommentProviderGraphQL } from '../view/prDocumentCommentProvider';
import { convertRESTPullRequestToRawPullRequest, parseGraphQLPullRequest } from './utils';
import { PullRequestResponse, MentionableUsersResponse } from './graphql';
import { PullRequestResponse, MentionableUsersResponse, PullRequestListResponse } from './graphql';
const queries = require('./queries.gql');

export const PULL_REQUEST_PAGE_SIZE = 20;
Expand Down Expand Up @@ -203,103 +203,30 @@ export class GitHubRepository implements IGitHubRepository, vscode.Disposable {
};
}

async getPullRequests(prType: PRType, page?: number): Promise<PullRequestData | undefined> {
return prType === PRType.All ? this.getAllPullRequests(page) : this.getPullRequestsForCategory(prType, page);
}

private async getAllPullRequests(page?: number): Promise<PullRequestData | undefined> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should remove this - we still need to support GHE that doesn't have GraphQL

try {
Logger.debug(`Fetch all pull requests - enter`, GitHubRepository.ID);
const { octokit, remote } = await this.ensure();
const result = await octokit.pullRequests.getAll({
owner: remote.owner,
repo: remote.repositoryName,
per_page: PULL_REQUEST_PAGE_SIZE,
page: page || 1
});

const hasMorePages = !!result.headers.link && result.headers.link.indexOf('rel="next"') > -1;
const pullRequests = result.data
.map(
pullRequest => {
if (!pullRequest.head.repo) {
Logger.appendLine(
'GitHubRepository> The remote branch for this PR was already deleted.'
);
return null;
}
async getPullRequestsGraphQL(nextCursor?: string|null):Promise<PullRequestListResponse|undefined> {
const { remote, query } = await this.ensure();

const item = convertRESTPullRequestToRawPullRequest(pullRequest);
return new PullRequestModel(this, this.remote, item);
}
)
.filter(item => item !== null) as PullRequestModel[];
const variables : {
owner: string;
name: string;
first: number;
after?: string
} = {
owner: remote.owner,
name: remote.repositoryName,
first: 30
};

Logger.debug(`Fetch all pull requests - done`, GitHubRepository.ID);
return {
pullRequests,
hasMorePages
};
} catch (e) {
Logger.appendLine(`Fetching all pull requests failed: ${e}`, GitHubRepository.ID);
if (e.code === 404) {
// not found
vscode.window.showWarningMessage(`Fetching pull requests for remote '${this.remote.remoteName}' failed, please check if the url ${this.remote.url} is valid.`);
} else {
throw e;
}
if(!!nextCursor) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious why you use if(!!condition) here, instead of if(condition)

variables.after = nextCursor;
}
}

private async getPullRequestsForCategory(prType: PRType, page?: number): Promise<PullRequestData | undefined> {
try {
Logger.debug(`Fetch pull request catogory ${PRType[prType]} - enter`, GitHubRepository.ID);
const { octokit, remote } = await this.ensure();
const user = await octokit.users.get({});
// Search api will not try to resolve repo that redirects, so get full name first
const repo = await octokit.repos.get({ owner: this.remote.owner, repo: this.remote.repositoryName });
const { data, headers } = await octokit.search.issues({
q: this.getPRFetchQuery(repo.data.full_name, user.data.login, prType),
per_page: PULL_REQUEST_PAGE_SIZE,
page: page || 1
});
let promises: Promise<Octokit.Response<Octokit.PullRequestsGetResponse>>[] = [];
data.items.forEach((item: any /** unluckily Octokit.AnyResponse */) => {
promises.push(new Promise(async (resolve, reject) => {
let prData = await octokit.pullRequests.get({
owner: remote.owner,
repo: remote.repositoryName,
number: item.number
});
resolve(prData);
}));
});

const hasMorePages = !!headers.link && headers.link.indexOf('rel="next"') > -1;
const pullRequests = await Promise.all(promises).then(values => {
return values.map(item => {
if (!item.data.head.repo) {
Logger.appendLine('GitHubRepository> The remote branch for this PR was already deleted.');
return null;
}
return new PullRequestModel(this, this.remote, convertRESTPullRequestToRawPullRequest(item.data));
}).filter(item => item !== null) as PullRequestModel[];
});
Logger.debug(`Fetch pull request catogory ${PRType[prType]} - done`, GitHubRepository.ID);
const { data } = await query<PullRequestListResponse>({
query: queries.GetPullRequests,
variables
});

return {
pullRequests,
hasMorePages
};
} catch (e) {
Logger.appendLine(`GitHubRepository> Fetching all pull requests failed: ${e}`);
if (e.code === 404) {
// not found
vscode.window.showWarningMessage(`Fetching pull requests for remote ${this.remote.remoteName}, please check if the url ${this.remote.url} is valid.`);
} else {
throw e;
}
}
return data;
}

async getPullRequest(id: number): Promise<PullRequestModel | undefined> {
Expand Down Expand Up @@ -385,23 +312,4 @@ export class GitHubRepository implements IGitHubRepository, vscode.Disposable {

return [];
}

private getPRFetchQuery(repo: string, user: string, type: PRType) {
let filter = '';
switch (type) {
case PRType.RequestReview:
filter = `review-requested:${user}`;
break;
case PRType.AssignedToMe:
filter = `assignee:${user}`;
break;
case PRType.Mine:
filter = `author:${user}`;
break;
default:
break;
}

return `is:open ${filter} type:pr repo:${repo}`;
}
}
64 changes: 64 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IAccount } from "./interface";

StanleyGoldman marked this conversation as resolved.
Show resolved Hide resolved
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
Expand Down Expand Up @@ -309,4 +311,66 @@ export interface RateLimit {
cost: number;
remaining: number;
resetAt: string;
}

export interface PullRequestListItem {
nodeId: string;
number: number;
title: string;
url: string;
author: {
login: string;
url: string;
avatarUrl: string;
};
state: string;
assignees: {
nodes: IAccount[];
};
reviewRequests: {
nodes: [{
requestedReviewer: IAccount;
}];
};
createdAt: string;
updatedAt: string;
merged: boolean;
headRef: {
StanleyGoldman marked this conversation as resolved.
Show resolved Hide resolved
name: string;
target: {
sha: string;
};
repo: {
url: string;
owner: {
login: string;
};
name: string;
};
};
baseRef: {
name: string;
target: {
sha: string;
};
repo: {
url: string;
owner: {
login: string;
};
name: string;
};
};
}

export interface PullRequestListResponse {
repository: {
pullRequests: {
nodes: PullRequestListItem[];
pageInfo: {
hasNextPage: boolean;
endCursor: string;
};
};
};
}
85 changes: 74 additions & 11 deletions src/github/pullRequestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { PendingReviewIdResponse, TimelineEventsResponse, PullRequestCommentsRes
const queries = require('./queries.gql');

interface PageInformation {
pullRequestPage: number;
hasMorePages: boolean | null;
endCursor: string | null;
}

interface RestErrorResult {
Expand Down Expand Up @@ -139,7 +139,7 @@ export class PullRequestManager {
Logger.debug(`Displaying configured remotes: ${remotesSetting.join(', ')}`, PullRequestManager.ID);

return remotesSetting
.map(remote => allGitHubRemotes.find(repo => repo.remoteName === remote))
.map(remote => allGitHubRemotes.find(repo => repo.remoteName === remote))
.filter(repo => !!repo) as Remote[];
}

Expand Down Expand Up @@ -404,8 +404,8 @@ export class PullRequestManager {
const remoteId = repository.remote.url.toString();
if (!this._repositoryPageInformation.get(remoteId)) {
this._repositoryPageInformation.set(remoteId, {
pullRequestPage: 1,
hasMorePages: null
hasMorePages: null,
endCursor: null
});
}
}
Expand Down Expand Up @@ -569,8 +569,8 @@ export class PullRequestManager {
if (!options.fetchNextPage) {
for (let repository of this._githubRepositories) {
this._repositoryPageInformation.set(repository.remote.url.toString(), {
pullRequestPage: 1,
hasMorePages: null
hasMorePages: null,
endCursor: null
});
}
}
Expand All @@ -582,15 +582,78 @@ export class PullRequestManager {

for (let i = 0; i < githubRepositories.length; i++) {
const githubRepository = githubRepositories[i];

const pageInformation = this._repositoryPageInformation.get(githubRepository.remote.url.toString())!;
const pullRequestData = await githubRepository.getPullRequests(type, pageInformation.pullRequestPage);
const pullRequestResponseData = await githubRepository.getPullRequestsGraphQL(pageInformation.endCursor);
const { remote, octokit } = await githubRepository.ensure();
const currentUser = octokit && (octokit as any).currentUser;
const currentUserLogin: string = currentUser.login;

if(!!pullRequestResponseData) {
pageInformation.hasMorePages = pullRequestResponseData.repository.pullRequests.pageInfo.hasNextPage;
pageInformation.endCursor = pullRequestResponseData.repository.pullRequests.pageInfo.endCursor;
} else {
pageInformation.hasMorePages = false;
pageInformation.endCursor = null;
}

pageInformation.hasMorePages = !!pullRequestData && pullRequestData.hasMorePages;
pageInformation.pullRequestPage++;
if (pullRequestResponseData && pullRequestResponseData.repository.pullRequests.nodes.length) {
let pullRequestItems = pullRequestResponseData.repository.pullRequests.nodes;

if (type !== PRType.All) {
if (type === PRType.Mine) {
pullRequestItems = pullRequestItems.filter(pr => pr.author.login === currentUserLogin);
} else if (type === PRType.RequestReview) {
pullRequestItems = pullRequestItems
.filter(pr => pr.reviewRequests.nodes
.findIndex(reviewRequest => reviewRequest.requestedReviewer.login === currentUserLogin) !== -1);
} else if (type === PRType.AssignedToMe) {
pullRequestItems = pullRequestItems
.filter(pr => pr.assignees.nodes
.findIndex(asignee => asignee.login === currentUserLogin) !== -1);
} else {
throw new Error('Unexpected pull request filter');
}
}

const pullRequests: PullRequestModel[] = pullRequestItems.map(pullRequestItem => {
let assignee: IAccount | undefined;
if(!!pullRequestItem.assignees.nodes && pullRequestItem.assignees.nodes.length) {
assignee = pullRequestItem.assignees.nodes[0];
}

const pullRequest: PullRequest = {
url: pullRequestItem.url,
assignee,
base: {
label: `${pullRequestItem.baseRef.repo.owner}:${pullRequestItem.baseRef.name}`,
ref: pullRequestItem.baseRef.name,
repo: {cloneUrl: pullRequestItem.baseRef.repo.url },
sha: pullRequestItem.baseRef.target.sha
},
head: {
label: `${pullRequestItem.headRef.repo.owner}:${pullRequestItem.headRef.name}`,
ref: pullRequestItem.headRef.name,
repo: {cloneUrl: pullRequestItem.headRef.repo.url },
sha: pullRequestItem.baseRef.target.sha
},
labels: [],
body: '',
createdAt: pullRequestItem.createdAt,
updatedAt: pullRequestItem.updatedAt,
merged: pullRequestItem.merged,
nodeId: pullRequestItem.nodeId,
number: pullRequestItem.number,
state: pullRequestItem.state,
title: pullRequestItem.title,
user: pullRequestItem.author
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mapping the graphql object to an object the PullRequestModel consumes. Not thrilled about doing this here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the file src/github/utils.ts has functions for mapping response types to normalized types

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, i'll move the code.


return new PullRequestModel(githubRepository, remote, pullRequest);
});

if (pullRequestData && pullRequestData.pullRequests.length) {
return {
pullRequests: pullRequestData.pullRequests,
pullRequests: pullRequests,
hasMorePages: pageInformation.hasMorePages,
hasUnsearchedRepositories: i < githubRepositories.length - 1
};
Expand Down
Loading