From dc038b39621704c5aa4f1ad49b7bec3d8c77b37e Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@arkins.net>
Date: Tue, 11 Sep 2018 06:07:50 +0200
Subject: [PATCH] refactor(github): use graphql to retrieve open pr list

---
 lib/platform/github/index.js                  | 123 ++++++++++++++
 .../github/graphql/pullrequest-1.json         | 158 ++++++++++++++++++
 .../github/__snapshots__/index.spec.js.snap   |  14 ++
 test/platform/github/index.spec.js            |  27 +++
 4 files changed, 322 insertions(+)
 create mode 100644 test/_fixtures/github/graphql/pullrequest-1.json

diff --git a/lib/platform/github/index.js b/lib/platform/github/index.js
index 5707c3906c..07d162a2b2 100644
--- a/lib/platform/github/index.js
+++ b/lib/platform/github/index.js
@@ -828,11 +828,134 @@ async function createPr(
   return pr;
 }
 
+let openPrList;
+
+async function getOpenPrs() {
+  if (!openPrList) {
+    openPrList = {};
+    try {
+      const url = 'graphql';
+      // https://developer.github.com/v4/previews/#mergeinfopreview---more-detailed-information-about-a-pull-requests-merge-state
+      const headers = {
+        accept: 'application/vnd.github.merge-info-preview+json',
+      };
+      // prettier-ignore
+      const query = `
+      query {
+        repository(owner: "${config.repositoryOwner}", name: "${config.repositoryName}") {
+          pullRequests(states: [OPEN], first: 100, orderBy: {field: UPDATED_AT, direction: DESC}) {
+            nodes {
+              number
+              headRefName
+              title
+              mergeable
+              mergeStateStatus
+              commits(first: 2) {
+                nodes {
+                  commit {
+                    author {
+                      email
+                    }
+                    committer {
+                      email
+                    }
+                    parents(last: 1) {
+                      edges {
+                        node {
+                          abbreviatedOid
+                          oid
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+              body
+            }
+          }
+        }
+      }
+      `;
+      const options = {
+        headers,
+        body: JSON.stringify({ query }),
+        json: false,
+      };
+      const res = JSON.parse((await get.post(url, options)).body);
+      for (const pr of res.data.repository.pullRequests.nodes) {
+        // https://developer.github.com/v4/object/pullrequest/
+        pr.displayNumber = `Pull Request #${pr.number}`;
+        pr.state = 'open';
+        pr.branchName = pr.headRefName;
+        delete pr.headRefName;
+        // https://developer.github.com/v4/enum/mergeablestate
+        const canMergeStates = ['BEHIND', 'CLEAN'];
+        if (canMergeStates.includes(pr.mergeStateStatus)) {
+          pr.canMerge = true;
+        } else {
+          pr.canMerge = false;
+        }
+        // https://developer.github.com/v4/enum/mergestatestatus
+        if (pr.mergeStateStatus === 'DIRTY') {
+          pr.isUnmergeable = true;
+        } else {
+          pr.isUnmergeable = false;
+        }
+        if (pr.commits.nodes.length === 1) {
+          if (config.gitAuthor) {
+            // Check against gitAuthor
+            const commitAuthorEmail = pr.commits.nodes[0].commit.author.email;
+            if (commitAuthorEmail === config.gitAuthor.address) {
+              pr.canRebase = true;
+            } else {
+              pr.canRebase = false;
+            }
+          } else {
+            // assume the author is us
+            // istanbul ignore next
+            pr.canRebase = true;
+          }
+        } else {
+          // assume we can't rebase if more than 1
+          pr.canRebase = false;
+        }
+        pr.isStale = false;
+        if (pr.mergeStateStatus === 'BEHIND') {
+          pr.isStale = true;
+        } else {
+          const baseCommitSHA = await getBaseCommitSHA();
+          if (
+            pr.commits.nodes[0].commit.parents.edges[0].node.oid !==
+            baseCommitSHA
+          ) {
+            pr.isStale = true;
+          }
+        }
+        delete pr.mergeable;
+        delete pr.mergeStateStatus;
+        delete pr.commits;
+        openPrList[pr.number] = pr;
+      }
+    } catch (err) /* istanbul ignore next */ {
+      logger.warn({ err }, 'getOpenPrs error');
+    }
+  }
+  return openPrList;
+}
+
 // Gets details for a PR
 async function getPr(prNo) {
   if (!prNo) {
     return null;
   }
+  const openPr = (await getOpenPrs())[prNo];
+  if (openPr) {
+    return openPr;
+  }
+  logger.info(
+    { prNo },
+    'PR not found in open PRs list - trying to fetch it directly'
+  );
   const pr = (await get(
     `repos/${config.parentRepo || config.repository}/pulls/${prNo}`
   )).body;
diff --git a/test/_fixtures/github/graphql/pullrequest-1.json b/test/_fixtures/github/graphql/pullrequest-1.json
new file mode 100644
index 0000000000..c17fdd04b6
--- /dev/null
+++ b/test/_fixtures/github/graphql/pullrequest-1.json
@@ -0,0 +1,158 @@
+{
+  "data": {
+    "repository": {
+      "pullRequests": {
+        "nodes": [
+          {
+            "number": 2433,
+            "headRefName": "renovate/major-got-packages",
+            "title": "build(deps): update got packages (major)",
+            "mergeable": "MERGEABLE",
+            "mergeStateStatus": "CLEAN",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "author": {
+                      "email": "bot@renovateapp.com"
+                    },
+                    "committer": {
+                      "name": "Renovate Bot",
+                      "email": "bot@renovateapp.com"
+                    },
+                    "parents": {
+                      "edges": [
+                        {
+                          "node": {
+                            "abbreviatedOid": "1234",
+                            "oid": "1234123412341234123412341234123412341234"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          },
+          {
+            "number": 2500,
+            "headRefName": "renovate/jest-monorepo",
+            "title": "chore(deps): update dependency jest to v23.6.0",
+            "mergeable": "UNKNOWN",
+            "mergeStateStatus": "DIRTY",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "author": {
+                      "email": "bot@renovateapp.com"
+                    },
+                    "committer": {
+                      "name": "Renovate Bot",
+                      "email": "bot@renovateapp.com"
+                    },
+                    "parents": {
+                      "edges": [
+                        {
+                          "node": {
+                            "abbreviatedOid": "b08d1aa",
+                            "oid": "b08d1aa8150c31516dcdf1d50a30020612e65d04"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          },
+          {
+            "number": 2079,
+            "headRefName": "feat/nodever",
+            "title": "feat: node versioning (WIP)",
+            "mergeable": "MERGEABLE",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "author": {
+                      "email": "rhys@arkins.net"
+                    },
+                    "committer": {
+                      "name": "Rhys Arkins",
+                      "email": "rhys@arkins.net"
+                    },
+                    "parents": {
+                      "edges": [
+                        {
+                          "node": {
+                            "abbreviatedOid": "233fa20",
+                            "oid": "233fa2078104581fba6beac10f3fd6765dedb300"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          },
+          {
+            "number": 2086,
+            "headRefName": "fix/deletePRafterDeleteBranch",
+            "title": "feat(vsts): abandon pr after delete branch",
+            "mergeable": "MERGEABLE",
+            "mergeStateStatus": "BEHIND",
+            "commits": {
+              "nodes": [
+                {
+                  "commit": {
+                    "author": {
+                      "email": "SESA8879@schneider-electric.com"
+                    },
+                    "committer": {
+                      "name": "Jean-Yves COUET",
+                      "email": "SESA8879@schneider-electric.com"
+                    },
+                    "parents": {
+                      "edges": [
+                        {
+                          "node": {
+                            "abbreviatedOid": "670cfd8",
+                            "oid": "670cfd8feeda9d6236caeb8afe5d60191a275bc6"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                },
+                {
+                  "commit": {
+                    "author": {
+                      "email": "SESA8879@schneider-electric.com"
+                    },
+                    "committer": {
+                      "name": "Jean-Yves COUET",
+                      "email": "SESA8879@schneider-electric.com"
+                    },
+                    "parents": {
+                      "edges": [
+                        {
+                          "node": {
+                            "abbreviatedOid": "c696654",
+                            "oid": "c69665439d8c1bf25cafc9c2db8e87c7ab274714"
+                          }
+                        }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/test/platform/github/__snapshots__/index.spec.js.snap b/test/platform/github/__snapshots__/index.spec.js.snap
index a2978b99ac..52426c22dc 100644
--- a/test/platform/github/__snapshots__/index.spec.js.snap
+++ b/test/platform/github/__snapshots__/index.spec.js.snap
@@ -270,6 +270,20 @@ Array [
 ]
 `;
 
+exports[`platform/github getPr(prNo) should return PR from graphql result 1`] = `
+Object {
+  "branchName": "renovate/jest-monorepo",
+  "canMerge": false,
+  "canRebase": true,
+  "displayNumber": "Pull Request #2500",
+  "isStale": true,
+  "isUnmergeable": true,
+  "number": 2500,
+  "state": "open",
+  "title": "chore(deps): update dependency jest to v23.6.0",
+}
+`;
+
 exports[`platform/github getPr(prNo) should return a PR object - 0 1`] = `
 Object {
   "base": Object {
diff --git a/test/platform/github/index.spec.js b/test/platform/github/index.spec.js
index e83b28d724..c48f243cd4 100644
--- a/test/platform/github/index.spec.js
+++ b/test/platform/github/index.spec.js
@@ -1,3 +1,5 @@
+const fs = require('fs-extra');
+
 describe('platform/github', () => {
   let github;
   let get;
@@ -1124,6 +1126,31 @@ describe('platform/github', () => {
       const pr = await github.getPr(null);
       expect(pr).toBe(null);
     });
+    it('should return PR from graphql result', async () => {
+      await initRepo({
+        repository: 'some/repo',
+        token: 'token',
+        gitAuthor: 'bot@renovateapp.com',
+      });
+      const res1 = fs.readFileSync(
+        'test/_fixtures/github/graphql/pullrequest-1.json',
+        'utf8'
+      );
+      get.post.mockImplementationOnce(() => ({
+        body: res1,
+      }));
+      // getBranchCommit
+      get.mockImplementationOnce(() => ({
+        body: {
+          object: {
+            sha: '1234123412341234123412341234123412341234',
+          },
+        },
+      }));
+      const pr = await github.getPr(2500);
+      expect(pr).toBeDefined();
+      expect(pr).toMatchSnapshot();
+    });
     it('should return null if no PR is returned from GitHub', async () => {
       await initRepo({ repository: 'some/repo', token: 'token' });
       get.mockImplementationOnce(() => ({
-- 
GitLab