From 5480420793979b9ae4b53117c6d5d757f12013bb Mon Sep 17 00:00:00 2001 From: Boxuan Li Date: Sat, 28 Jul 2018 11:10:26 +0800 Subject: [PATCH] Use new graphql client to sync issues Closes https://github.com/coala/gh-board/issues/26 --- script/queries/export.js | 4 ++ script/queries/github_issue_info.js | 53 ++++++++++++++ script/queries/github_label_info.js | 19 +++++ script/queries/github_pr_info.js | 98 ++++++++++++++++++++++++++ script/queries/github_reaction_info.js | 49 +++++++++++++ src/github-client.js | 22 ++++++ src/issue-store.js | 25 ++++++- 7 files changed, 268 insertions(+), 2 deletions(-) create mode 100644 script/queries/export.js create mode 100644 script/queries/github_issue_info.js create mode 100644 script/queries/github_label_info.js create mode 100644 script/queries/github_pr_info.js create mode 100644 script/queries/github_reaction_info.js diff --git a/script/queries/export.js b/script/queries/export.js new file mode 100644 index 00000000..f5862ce8 --- /dev/null +++ b/script/queries/export.js @@ -0,0 +1,4 @@ +export { GITHUB_ISSUE_INFO_QUERY } from './github_issue_info'; +export { GITHUB_PR_INFO_QUERY } from './github_pr_info'; +export { GITHUB_LABEL_INFO_QUERY } from './github_label_info'; +export { GITHUB_REACTION_INFO_QUERY } from './github_reaction_info'; diff --git a/script/queries/github_issue_info.js b/script/queries/github_issue_info.js new file mode 100644 index 00000000..c3b8d578 --- /dev/null +++ b/script/queries/github_issue_info.js @@ -0,0 +1,53 @@ +export const GITHUB_ISSUE_INFO_QUERY = ` + query($owner: String!, $name: String!, $perPage: Int!, $before: String, $orderBy: IssueOrder!) { + rateLimit { + limit + remaining + resetAt + } + repository(owner:$owner, name:$name) { + issues(last:$perPage, before:$before, orderBy:$orderBy) { + pageInfo { + startCursor + hasPreviousPage + } + nodes { + title + bodyText + url + number + state + comments { + totalCount + } + milestone { + title + createdAt + dueOn + state + url + description + } + createdAt + updatedAt + closedAt + author { + login + avatarUrl + } + assignees(first:1) { + nodes { + login + avatarUrl + } + } + labels(first:100) { + nodes { + color + name + } + } + } + } + } + }`; diff --git a/script/queries/github_label_info.js b/script/queries/github_label_info.js new file mode 100644 index 00000000..551a0d9e --- /dev/null +++ b/script/queries/github_label_info.js @@ -0,0 +1,19 @@ +export const GITHUB_LABEL_INFO_QUERY = ` + query($owner: String!, $name: String!) { + rateLimit { + limit + remaining + resetAt + } + repository(owner:$owner, name:$name) { + labels(first:100){ + nodes { + id + url + name + color + isDefault + } + } + } + }`; diff --git a/script/queries/github_pr_info.js b/script/queries/github_pr_info.js new file mode 100644 index 00000000..57445789 --- /dev/null +++ b/script/queries/github_pr_info.js @@ -0,0 +1,98 @@ +export const GITHUB_PR_INFO_QUERY = ` + query($owner: String!, $name: String!, $perPage: Int!, $before: String, $orderBy: IssueOrder!) { + rateLimit { + limit + remaining + resetAt + } + repository(owner:$owner, name:$name) { + pullRequests(last:$perPage, before:$before, orderBy:$orderBy) { + pageInfo { + startCursor + hasPreviousPage + } + nodes { + title + bodyText + url + number + state + reviews(first: 20) { + totalCount + nodes { + comments(first: 100) { + totalCount + nodes { + id + url + bodyText + diffHunk + createdAt + lastEditedAt + author { + login + avatarUrl + ... on User { + name + } + } + reactionGroups { + content + createdAt + } + } + } + } + } + comments(first:100) { + totalCount + nodes { + id + url + bodyText + createdAt + lastEditedAt + author { + login + avatarUrl + ... on User { + name + } + } + reactionGroups { + content + createdAt + } + } + } + milestone { + title + createdAt + dueOn + state + url + description + } + createdAt + updatedAt + closedAt + author { + login + avatarUrl + } + assignees(first:1) { + nodes { + login + avatarUrl + } + } + labels(first:100) { + nodes { + color + name + } + } + } + } + } + }`; diff --git a/script/queries/github_reaction_info.js b/script/queries/github_reaction_info.js new file mode 100644 index 00000000..7dd2e196 --- /dev/null +++ b/script/queries/github_reaction_info.js @@ -0,0 +1,49 @@ +export const GITHUB_REACTION_INFO_QUERY = ` + query($owner: String!, $name: String!, $number: Int!, + $reviewsCount: Int!, $discussionsPerReview: Int!, $commentsCount: Int!) { + rateLimit { + limit + remaining + resetAt + } + repository(owner:$owner, name:$name) { + pullRequest(number: $number) { + reviews(first: $reviewsCount) { + nodes { + comments(first: $discussionsPerReview) { + nodes { + id + reactions(first: 100) { + nodes { + id + createdAt + content + user { + login + name + } + } + } + } + } + } + } + comments(first:$commentsCount) { + nodes { + id + reactions(first: 100) { + nodes { + id + createdAt + content + user { + login + name + } + } + } + } + } + } + } + }`; diff --git a/src/github-client.js b/src/github-client.js index cb88bb12..1dbaafd5 100644 --- a/src/github-client.js +++ b/src/github-client.js @@ -20,6 +20,8 @@ import FetchAllPlugin from 'octokat/dist/node/plugins/fetch-all'; import PaginationPlugin from 'octokat/dist/node/plugins/pagination'; import toQueryString from 'octokat/dist/node/helpers/querystring'; +import GraphQLClient from './github-graphql'; + const MAX_CACHED_URLS = 2000; let cachedClient = null; @@ -191,6 +193,17 @@ class Client extends EventEmitter { let {token, password} = this.getCredentials(); return !!token || !!password; } + useGraphQL() { + // use GraphQL when applicable, provided `hasCredentials()` is true + // developers need to change the behaviour here in case they + // want to switch back to use GitHub REST API (Octocat uses REST API) + if (this.hasCredentials()) { + // GitHub GraphQL API (API V4) requires token + return true; + } else { + return false; + } + } getOcto() { if (!cachedClient) { let credentials = this.getCredentials(); @@ -203,6 +216,15 @@ class Client extends EventEmitter { } return cachedClient; } + getGraphQLClient() { + // GitHub GraphQL API requires token + const { token, emitter } = this.getCredentials(); + // it is a must to return a new instance as there is no concurrency support + // see github-graphql.js for more details + const ignoreAuthor = 'gitmate-bot rultor TravisBuddy'; + const ignoreContent = '@gitmate-bot @rultor /^(unack|ack)/g'; + return new GraphQLClient(token, ignoreAuthor, ignoreContent, emitter); + } getAnonymousOcto() { return new Octo(); } diff --git a/src/issue-store.js b/src/issue-store.js index b4446c35..773674d3 100644 --- a/src/issue-store.js +++ b/src/issue-store.js @@ -187,7 +187,7 @@ const issueStore = new class IssueStore extends EventEmitter { }); }); } - _fetchLastSeenUpdatesForRepo(repoOwner, repoName, progress, lastSeenAt, isPrivate, didLabelsChange) { + async _fetchLastSeenUpdatesForRepo(repoOwner, repoName, progress, lastSeenAt, isPrivate, didLabelsChange) { const opts = { per_page: 100, sort: 'updated', @@ -199,7 +199,28 @@ const issueStore = new class IssueStore extends EventEmitter { } let fetchPromise; if (Client.canCacheLots()) { - fetchPromise = Client.getOcto().repos(repoOwner, repoName).issues.fetchAll(opts); + if (Client.useGraphQL()) { + const [issues, pullRequests] = await Promise.all([ + Client.getGraphQLClient() + .repo(repoOwner, repoName) + .issues({sort: 'UPDATED_AT', earliestDate: lastSeenAt}) + .fetchAll({per_page: 100}), + Client.getGraphQLClient() + .repo(repoOwner, repoName) + .pullRequests({sort: 'UPDATED_AT', earliestDate: lastSeenAt}) + .fetchAll({per_page: 30}) + ]); + let result = []; + if (issues) { + result = result.concat(issues); + } + if (pullRequests) { + result = result.concat(pullRequests); + } + fetchPromise = Promise.resolve(result.map((item) => item.issue)); + } else { + fetchPromise = Client.getOcto().repos(repoOwner, repoName).issues.fetchAll(opts); + } } else { fetchPromise = Client.getOcto().repos(repoOwner, repoName).issues.fetchOne(opts); }