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
140 changes: 31 additions & 109 deletions src/github/githubRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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,44 @@ 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);
}
async getPullRequestsGraphQL(type: PRType, nextCursor?: string|null):Promise<PullRequestListResponse|undefined> {
const { remote, query, octokit } = await this.ensure();
const currentUser = octokit && (octokit as any).currentUser;

Choose a reason for hiding this comment

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

just out of curiosity, why octokit as any? Do we not have ts bindings for octokit or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a similar question as I'm pretty sure the bindings exist.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, we do have bindings for octokit, but currentUser is something we've added ourselves

#600 (review)

We also added an octokit.ts file a while ago that modifies the typings slightly, there was something that should have been nullable that wasn't. currentUser can be added there to get rid of our any casts. But I think we should ultimately try to get rid of that file by correcting the typings upstream

const currentUserLogin: string = currentUser.login;

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;
}

const item = convertRESTPullRequestToRawPullRequest(pullRequest);
return new PullRequestModel(this, this.remote, item);
}
)
.filter(item => item !== null) as PullRequestModel[];
let filter = `type:pr is:open repo:${remote.owner}/${remote.repositoryName}`;

Choose a reason for hiding this comment

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

Might be a little edge casey, but do we need to guard against the remote being null here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would say yes, but looking at all the other calls to ensure to get remote seem to make the assumption that remote will not be null.


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.`);
if (type !== PRType.All) {
if (type === PRType.Mine) {
filter += ` author:${currentUserLogin}`;
} else if (type === PRType.RequestReview) {
filter += ` review-requested:${currentUserLogin}`;
} else if (type === PRType.AssignedToMe) {
filter += ` assignee:${currentUserLogin}`;
} else {
throw e;
throw new Error('Unexpected pull request filter');
StanleyGoldman marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

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 variables : {
query: string;
first: number;
after?: string;
} = {
query: filter,
first: 30
};

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;
}
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;
}

const { data } = await query<PullRequestListResponse>({
query: queries.GetPullRequests,
variables
});

return data;
}

async getPullRequest(id: number): Promise<PullRequestModel | undefined> {
Expand Down Expand Up @@ -385,23 +326,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}`;
}
}
29 changes: 29 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IAccount } from './interface';

export interface MergedEvent {
__typename: string;
id: string;
Expand Down Expand Up @@ -309,4 +311,31 @@ export interface RateLimit {
cost: number;
remaining: number;
resetAt: string;
}

export interface PullRequestListItem {
nodeId: string;
number: number;
title: string;
url: string;
author: IAccount;
state: string;
assignees: {
nodes: IAccount[];
};
createdAt: string;
updatedAt: string;
merged: boolean;
headRef: Ref;
baseRef: Ref;
}

export interface PullRequestListResponse {
search: {
nodes: PullRequestListItem[];
pageInfo: {
hasNextPage: boolean;
endCursor: string;
};
};
}
67 changes: 56 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,60 @@ 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(type, pageInformation.endCursor);
const { remote } = await githubRepository.ensure();

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

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

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.repository.owner}:${pullRequestItem.baseRef.name}`,
ref: pullRequestItem.baseRef.name,
repo: {cloneUrl: pullRequestItem.baseRef.repository.url },
sha: pullRequestItem.baseRef.target.oid
},
head: {
label: `${pullRequestItem.headRef.repository.owner}:${pullRequestItem.headRef.name}`,
ref: pullRequestItem.headRef.name,
repo: {cloneUrl: pullRequestItem.headRef.repository.url },
sha: pullRequestItem.baseRef.target.oid
},
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
44 changes: 44 additions & 0 deletions src/github/queries.gql
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ fragment Reactable on Reactable {
}
}

fragment Account on Actor{
... on User {
name
}
login
url
avatarUrl
}

query TimelineEvents($owner: String!, $name: String!, $number: Int!, $last: Int = 100) {
repository(owner: $owner, name: $name) {
pullRequest(number: $number) {
Expand Down Expand Up @@ -337,4 +346,39 @@ query GetMentionableUsers($owner: String!, $name: String!, $first: Int!, $after:
remaining
resetAt
}
}

query GetPullRequests($query: String!, $first: Int!, $after: String) {
search(query: $query, first: $first, after:$after, type: ISSUE) {
nodes {
... on PullRequest {
nodeId: id
number
title
url
author {
...Account
}
state
assignees(first: 30) {
nodes {
...Account
}
}
createdAt
updatedAt
merged
headRef {
...Ref
}
baseRef {
...Ref
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Loading