From 1bd22611fd426a9e45a8927d5a7cba8605faa46a Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Sat, 3 Jun 2017 09:40:13 +0200
Subject: [PATCH] Fix GitHub app merge (#234)

* Add installation debug

* Update initRepo merge detection

* mergePr tests

* guess squash

* Update tests
---
 lib/api/github.js                          |  57 +++++-
 lib/helpers/github-app.js                  |   1 +
 test/api/__snapshots__/github.spec.js.snap |  38 ++--
 test/api/github.spec.js                    | 191 ++++++++++++++++++++-
 4 files changed, 258 insertions(+), 29 deletions(-)

diff --git a/lib/api/github.js b/lib/api/github.js
index d7e9033a54..c288c20e6c 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -140,8 +140,10 @@ async function initRepo(repoName, token, endpoint) {
       config.mergeMethod = 'rebase';
     } else if (res.body.allow_squash_merge) {
       config.mergeMethod = 'squash';
-    } else {
+    } else if (res.body.allow_merge_commit) {
       config.mergeMethod = 'merge';
+    } else {
+      logger.debug('Could not find allowed merge methods for repo');
     }
     logger.debug(`${repoName} default branch = ${config.defaultBranch}`);
     config.baseCommitSHA = await getBranchCommit(config.defaultBranch);
@@ -355,11 +357,54 @@ async function updatePr(prNo, title, body) {
 }
 
 async function mergePr(pr) {
-  await ghGot.put(`repos/${config.repoName}/pulls/${pr.number}/merge`, {
-    body: {
-      merge_method: config.mergeMethod,
-    },
-  });
+  const url = `repos/${config.repoName}/pulls/${pr.number}/merge`;
+  const options = {
+    body: {},
+  };
+  if (config.mergeMethod) {
+    // This path is taken if we have auto-detected the allowed merge types from the repo
+    options.body.merge_method = config.mergeMethod;
+    try {
+      logger.debug(`mergePr: ${url}, ${JSON.stringify(options)}`);
+      await ghGot.put(url, options);
+    } catch (err) {
+      logger.error(
+        `Failed to ${options.body.merge_method} PR: ${JSON.stringify(err)}`
+      );
+      return;
+    }
+  } else {
+    // We need to guess the merge method and try squash -> rebase -> merge
+    options.body.merge_method = 'rebase';
+    try {
+      logger.debug(`mergePr: ${url}, ${JSON.stringify(options)}`);
+      await ghGot.put(url, options);
+    } catch (err1) {
+      logger.debug(
+        `Failed to ${options.body.merge_method} PR: ${JSON.stringify(err1)}`
+      );
+      try {
+        options.body.merge_method = 'squash';
+        logger.debug(`mergePr: ${url}, ${JSON.stringify(options)}`);
+        await ghGot.put(url, options);
+      } catch (err2) {
+        logger.debug(
+          `Failed to ${options.body.merge_method} PR: ${JSON.stringify(err2)}`
+        );
+        try {
+          options.body.merge_method = 'merge';
+          logger.debug(`mergePr: ${url}, ${JSON.stringify(options)}`);
+          await ghGot.put(url, options);
+        } catch (err3) {
+          logger.debug(
+            `Failed to ${options.body.merge_method} PR: ${JSON.stringify(err3)}`
+          );
+          logger.error('All merge attempts failed');
+          return;
+        }
+      }
+    }
+  }
   // Delete branch
   await ghGot.delete(`repos/${config.repoName}/git/refs/heads/${pr.head.ref}`);
 }
diff --git a/lib/helpers/github-app.js b/lib/helpers/github-app.js
index 58a483e10c..92d0b5b99d 100644
--- a/lib/helpers/github-app.js
+++ b/lib/helpers/github-app.js
@@ -48,6 +48,7 @@ async function getRepositories(config) {
     const installations = await ghApi.getInstallations(appToken);
     logger.verbose(`Found installations for ${installations.length} users`);
     for (const installation of installations) {
+      logger.debug(JSON.stringify(installation));
       const installationRepos = await module.exports.getUserRepositories(
         appToken,
         installation.id
diff --git a/test/api/__snapshots__/github.spec.js.snap b/test/api/__snapshots__/github.spec.js.snap
index a20e9a5096..ee24817ab1 100644
--- a/test/api/__snapshots__/github.spec.js.snap
+++ b/test/api/__snapshots__/github.spec.js.snap
@@ -798,36 +798,36 @@ Object {
 }
 `;
 
-exports[`api/github initRepo should squash 1`] = `
+exports[`api/github initRepo should not guess at merge 1`] = `
 Object {
   "baseCommitSHA": "1234",
   "baseTreeSHA": "5678",
   "defaultBranch": "master",
-  "mergeMethod": "squash",
   "owner": "theowner",
   "repoName": "some/repo",
 }
 `;
 
-exports[`api/github mergePr(prNo) should merge the PR 1`] = `
-Array [
-  Array [
-    "repos/some/repo/pulls/1234/merge",
-    Object {
-      "body": Object {
-        "merge_method": "rebase",
-      },
-    },
-  ],
-]
+exports[`api/github initRepo should rebase 1`] = `
+Object {
+  "baseCommitSHA": "1234",
+  "baseTreeSHA": "5678",
+  "defaultBranch": "master",
+  "mergeMethod": "rebase",
+  "owner": "theowner",
+  "repoName": "some/repo",
+}
 `;
 
-exports[`api/github mergePr(prNo) should merge the PR 2`] = `
-Array [
-  Array [
-    "repos/some/repo/git/refs/heads/someref",
-  ],
-]
+exports[`api/github initRepo should squash 1`] = `
+Object {
+  "baseCommitSHA": "1234",
+  "baseTreeSHA": "5678",
+  "defaultBranch": "master",
+  "mergeMethod": "squash",
+  "owner": "theowner",
+  "repoName": "some/repo",
+}
 `;
 
 exports[`api/github updatePr(prNo, title, body) should update the PR 1`] = `
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
index 7f5ff97214..6abc21adbf 100644
--- a/test/api/github.spec.js
+++ b/test/api/github.spec.js
@@ -139,6 +139,7 @@ describe('api/github', () => {
         default_branch: 'master',
         allow_rebase_merge: true,
         allow_squash_merge: true,
+        allow_merge_commit: true,
       },
     }));
     // getBranchCommit
@@ -193,7 +194,7 @@ describe('api/github', () => {
         'No token found for GitHub repository some/repo'
       );
     });
-    it('should squash', async () => {
+    it('should rebase', async () => {
       async function squashInitRepo(...args) {
         // repo info
         ghGot.mockImplementationOnce(() => ({
@@ -202,8 +203,9 @@ describe('api/github', () => {
               login: 'theowner',
             },
             default_branch: 'master',
-            allow_rebase_merge: false,
+            allow_rebase_merge: true,
             allow_squash_merge: true,
+            allow_merge_commit: true,
           },
         }));
         // getBranchCommit
@@ -227,6 +229,41 @@ describe('api/github', () => {
       const config = await squashInitRepo('some/repo', 'token');
       expect(config).toMatchSnapshot();
     });
+    it('should squash', async () => {
+      async function mergeInitRepo(...args) {
+        // repo info
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            owner: {
+              login: 'theowner',
+            },
+            default_branch: 'master',
+            allow_rebase_merge: false,
+            allow_squash_merge: true,
+            allow_merge_commit: true,
+          },
+        }));
+        // getBranchCommit
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            object: {
+              sha: '1234',
+            },
+          },
+        }));
+        // getCommitTree
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            tree: {
+              sha: '5678',
+            },
+          },
+        }));
+        return github.initRepo(...args);
+      }
+      const config = await mergeInitRepo('some/repo', 'token');
+      expect(config).toMatchSnapshot();
+    });
     it('should merge', async () => {
       async function mergeInitRepo(...args) {
         // repo info
@@ -238,6 +275,39 @@ describe('api/github', () => {
             default_branch: 'master',
             allow_rebase_merge: false,
             allow_squash_merge: false,
+            allow_merge_commit: true,
+          },
+        }));
+        // getBranchCommit
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            object: {
+              sha: '1234',
+            },
+          },
+        }));
+        // getCommitTree
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            tree: {
+              sha: '5678',
+            },
+          },
+        }));
+        return github.initRepo(...args);
+      }
+      const config = await mergeInitRepo('some/repo', 'token');
+      expect(config).toMatchSnapshot();
+    });
+    it('should not guess at merge', async () => {
+      async function mergeInitRepo(...args) {
+        // repo info
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            owner: {
+              login: 'theowner',
+            },
+            default_branch: 'master',
           },
         }));
         // getBranchCommit
@@ -619,8 +689,121 @@ describe('api/github', () => {
         },
       };
       await github.mergePr(pr);
-      expect(ghGot.put.mock.calls).toMatchSnapshot();
-      expect(ghGot.delete.mock.calls).toMatchSnapshot();
+      expect(ghGot.put.mock.calls).toHaveLength(1);
+      expect(ghGot.delete.mock.calls).toHaveLength(1);
+    });
+  });
+  describe('mergePr(prNo)', () => {
+    it('should handle merge error', async () => {
+      await initRepo('some/repo', 'token');
+      const pr = {
+        number: 1234,
+        head: {
+          ref: 'someref',
+        },
+      };
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('merge error');
+      });
+      await github.mergePr(pr);
+      expect(ghGot.put.mock.calls).toHaveLength(1);
+      expect(ghGot.delete.mock.calls).toHaveLength(0);
+    });
+  });
+  describe('mergePr(prNo) - autodetection', () => {
+    beforeEach(async () => {
+      async function guessInitRepo(...args) {
+        // repo info
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            owner: {
+              login: 'theowner',
+            },
+            default_branch: 'master',
+          },
+        }));
+        // getBranchCommit
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            object: {
+              sha: '1234',
+            },
+          },
+        }));
+        // getCommitTree
+        ghGot.mockImplementationOnce(() => ({
+          body: {
+            tree: {
+              sha: '5678',
+            },
+          },
+        }));
+        return github.initRepo(...args);
+      }
+      await guessInitRepo('some/repo', 'token');
+      ghGot.put = jest.fn();
+    });
+    it('should try rebase first', async () => {
+      const pr = {
+        number: 1235,
+        head: {
+          ref: 'someref',
+        },
+      };
+      await github.mergePr(pr);
+      expect(ghGot.put.mock.calls).toHaveLength(1);
+      expect(ghGot.delete.mock.calls).toHaveLength(1);
+    });
+    it('should try squash after rebase', async () => {
+      const pr = {
+        number: 1236,
+        head: {
+          ref: 'someref',
+        },
+      };
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no rebasing allowed');
+      });
+      await github.mergePr(pr);
+      expect(ghGot.put.mock.calls).toHaveLength(2);
+      expect(ghGot.delete.mock.calls).toHaveLength(1);
+    });
+    it('should try merge after squash', async () => {
+      const pr = {
+        number: 1237,
+        head: {
+          ref: 'someref',
+        },
+      };
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no rebasing allowed');
+      });
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no squashing allowed');
+      });
+      await github.mergePr(pr);
+      expect(ghGot.put.mock.calls).toHaveLength(3);
+      expect(ghGot.delete.mock.calls).toHaveLength(1);
+    });
+    it('should give up', async () => {
+      const pr = {
+        number: 1237,
+        head: {
+          ref: 'someref',
+        },
+      };
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no rebasing allowed');
+      });
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no squashing allowed');
+      });
+      ghGot.put.mockImplementationOnce(() => {
+        throw new Error('no merging allowed');
+      });
+      await github.mergePr(pr);
+      expect(ghGot.put.mock.calls).toHaveLength(3);
+      expect(ghGot.delete.mock.calls).toHaveLength(0);
     });
   });
   describe('getFile(filePatch, branchName)', () => {
-- 
GitLab