-
Notifications
You must be signed in to change notification settings - Fork 592
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
Changes from 6 commits
c8ec117
6a2c6f0
9efe664
3b28d7a
c7c3ded
fd8a023
0841a65
abf3048
97e4513
ce35d35
fbd8656
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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> { | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious why you use |
||
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> { | ||
|
@@ -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}`; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,12 +21,12 @@ import Logger from '../common/logger'; | |
import { EXTENSION_ID } from '../constants'; | ||
import { fromPRUri } from '../common/uri'; | ||
import { convertRESTPullRequestToRawPullRequest, convertPullRequestsGetCommentsResponseItemToComment, convertIssuesCreateCommentResponseToComment, parseGraphQLTimelineEvents, convertRESTTimelineEvents, getRelatedUsersFromTimelineEvents, parseGraphQLComment, getReactionGroup, convertRESTUserToAccount, convertRESTReviewEvent, parseGraphQLReviewEvent } from './utils'; | ||
import { PendingReviewIdResponse, TimelineEventsResponse, PullRequestCommentsResponse, AddCommentResponse, SubmitReviewResponse, DeleteReviewResponse, EditCommentResponse, DeleteReactionResponse, AddReactionResponse } from './graphql'; | ||
import { PendingReviewIdResponse, TimelineEventsResponse, PullRequestCommentsResponse, AddCommentResponse, SubmitReviewResponse, DeleteReviewResponse, EditCommentResponse, DeleteReactionResponse, AddReactionResponse, PullRequestListItem } from './graphql'; | ||
const queries = require('./queries.gql'); | ||
|
||
interface PageInformation { | ||
pullRequestPage: number; | ||
hasMorePages: boolean | null; | ||
endCursor: string | null; | ||
} | ||
|
||
interface RestErrorResult { | ||
|
@@ -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[]; | ||
} | ||
|
||
|
@@ -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 | ||
}); | ||
} | ||
} | ||
|
@@ -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 | ||
}); | ||
} | ||
} | ||
|
@@ -582,15 +582,81 @@ 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; | ||
} | ||
|
||
if (pullRequestResponseData && pullRequestResponseData.repository.pullRequests.nodes.length) { | ||
let pullRequestItems = pullRequestResponseData.repository.pullRequests.nodes; | ||
|
||
pageInformation.hasMorePages = !!pullRequestData && pullRequestData.hasMorePages; | ||
pageInformation.pullRequestPage++; | ||
if (type !== PRType.All) { | ||
|
||
let pullRequestFilter: (item: PullRequestListItem) => boolean; | ||
|
||
if (type === PRType.Mine) { | ||
pullRequestFilter = pr => pr.author.login === currentUserLogin; | ||
} else if (type === PRType.RequestReview) { | ||
pullRequestFilter = pr => pr.reviewRequests.nodes | ||
.findIndex(reviewRequest => reviewRequest.requestedReviewer.login === currentUserLogin) !== -1; | ||
} else if (type === PRType.AssignedToMe) { | ||
pullRequestFilter = pr => pr.assignees.nodes | ||
.findIndex(asignee => asignee.login === currentUserLogin) !== -1; | ||
} else { | ||
throw new Error('Unexpected pull request filter'); | ||
} | ||
|
||
pullRequestItems = pullRequestItems.filter(pullRequestFilter); | ||
StanleyGoldman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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 | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mapping the graphql object to an object the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the file There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
}; | ||
|
There was a problem hiding this comment.
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