From bcc3171245ce776094f39f1f02414875bd10bc16 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Wed, 4 Jan 2017 18:48:55 +0100
Subject: [PATCH] Refactor to use GitHub API instead of git

Closes #8, Closes #1

commit 02eefd2ec70bf8a07e667d13a4e33dc70dd9db96
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Wed Jan 4 18:25:30 2017 +0100

    Refactor updates

commit e9330e41a3388879ef300f40d8843210c70e2b31
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Wed Jan 4 18:25:17 2017 +0100

    Improve commenting

commit 2feb32f218a83ec765732280af8b0d9e569fb313
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Wed Jan 4 13:34:36 2017 +0100

    Refactor token input

commit 28b4428bae8cdafffe0227e794e8f77a5be2fcfd
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Wed Jan 4 13:28:09 2017 +0100

    Rename files

commit 2fe98be1b31b27f625023ffb748f36c3a0eefee6
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Wed Jan 4 13:21:52 2017 +0100

    Improve error log

commit e6f0e691945e561c458147f52b02903ba82373d7
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 19:20:53 2016 +0100

    Support custom package.json path

commit 5f971746d3abe2a40b94cae3b8592ec97b21358e
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 19:20:40 2016 +0100

    Handle null dependencies or devDependencies

commit 9eac59859626bc7d40cacb1e93e645973667208c
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 18:23:14 2016 +0100

    Split per branch

commit 61d7337e813b86d186511fdb6ad0655b6110942f
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 18:22:59 2016 +0100

    Ignore unstable

commit d4d8bcf0895046b5d13f8dea93dbb30121f9be7c
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 18:22:10 2016 +0100

    Pin

commit 4b9306b8072726b2eed74a0f56a4687c865539a4
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Mon Dec 19 11:55:47 2016 +0100

    Add new
---
 package.json |  12 +-
 src/index.js | 388 +++++++++++++++++++++------------------------------
 2 files changed, 166 insertions(+), 234 deletions(-)

diff --git a/package.json b/package.json
index 0981094d09..19eea08437 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,12 @@
 {
   "private": true,
   "dependencies": {
-    "got": "^6.6.3",
-    "mkdirp": "^0.5.1",
-    "nodegit": "^0.16.0",
-    "rimraf": "^2.5.4",
-    "semver": "^5.3.0"
+    "gh-got": "5.0.0",
+    "got": "6.6.3",
+    "mkdirp": "0.5.1",
+    "nodegit": "0.16.0",
+    "rimraf": "2.5.4",
+    "semver": "5.3.0",
+    "semver-stable": "2.0.4"
   }
 }
diff --git a/src/index.js b/src/index.js
index ffbba12b6d..6ebf41b201 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,252 +1,182 @@
-'use strict';
-
-const Git = require('nodegit');
+const ghGot = require('gh-got');
 const got = require('got');
 const semver = require('semver');
-const fs = require('fs');
-const mkdirp = require('mkdirp');
-const rimraf = require('rimraf');
-
-const authorName = 'Renovate Bot'; // commit credentials
-const authorEmail = 'renovate-bot@keylocation.sg'; // commit credentials
-
-const sshPublicKeyPath = `${process.env.HOME}/.ssh/id_rsa.pub`;
-const sshPrivateKeyPath = `${process.env.HOME}/.ssh/id_rsa`;
-
-if (!module.parent) {
-  // https://github.com/settings/tokens/new
-  const token = process.argv[2];
-  const repoName = process.argv[3];
-  let packageFile = process.argv[4];
-
-  if (!token || !repoName) {
-    console.error(`Usage: node index.js <token> <repo>`);
-    process.exit(1);
-  }
-  
-  if (!packageFile) {
-  	packageFile = 'package.json';
+const stable = require('semver-stable');
+
+const token = process.env.RENOVATE_TOKEN;
+const repoName = process.argv[2];
+const userName = repoName.split('/')[0];
+const packageFile = process.argv[3] || 'package.json';
+
+let masterSHA;
+let masterPackageJson;
+
+ghGot(`repos/${repoName}/git/refs/head`, {token: token}).then(res => {
+  // First, get the SHA for master branch
+  res.body.forEach(function(branch) {
+    // Loop through all branches because master may not be the first
+    if (branch.ref === 'refs/heads/master') {
+      // This is the SHA we will create new branches from
+      masterSHA = branch.object.sha;
+    }
+  });
+  // Now, retrieve the master package.json
+  ghGot(`repos/${repoName}/contents/${packageFile}`, {token: token}).then(res => {
+    masterPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString());
+    // Iterate through dependencies and then devDependencies
+    return iterateDependencies('dependencies')
+      .then(() => iterateDependencies('devDependencies'));
+  }).catch(err => {
+    console.log('Error reading master package.json');
+  });
+});
+
+function iterateDependencies(depType) {
+  const deps = masterPackageJson[depType];
+  if (!deps) {
+    return;
   }
+  return Object.keys(deps).reduce((total, depName) => {
+    return total.then(() => {
+      const currentVersion = deps[depName].replace(/[^\d.]/g, '');
 
-  updateRepo({ token, repoName, packageFile })
-    .catch(err => console.log(err.stack || err));
-}
-
-function updateRepo({ token, repoName, packageFile }) {
-  const repoPath = `tmp/${repoName}`;
-  rimraf.sync(repoPath);
-  mkdirp.sync(repoPath);
-
-  let repo;
-  let headCommit;
-
-  return Git
-    .Clone(`git@github.com:${repoName}.git`, repoPath, {
-      fetchOpts: {
-        callbacks: {
-          credentials: getCredentials,
-          certificateCheck: () => 1
-        }
+      if (!semver.valid(currentVersion)) {
+        console.log('Invalid current version');
+        return;
       }
-    })
-    .then(_repo => {
-      repo = _repo;
-      return repo.fetch('origin', {
-        callbacks: {
-          credentials: getCredentials
-        }
-      });
-    })
-    .then(() => {
-      return repo.getHeadCommit();
-    })
-    .then(commit => {
-      headCommit = commit;
-      return readFile(headCommit, packageFile);
-    })
-    .then(blob => {
-      const pkg = JSON.parse(blob);
-      return iterateDependencies(pkg, 'dependencies')
-        .then(() => iterateDependencies(pkg, 'devDependencies'));
-    })
-    .then(() => {
-      rimraf.sync(repoPath);
-    });
-
-  function iterateDependencies(pkg, depType) {
-    const deps = pkg[depType];
-
-    return Object.keys(deps).reduce((total, depName) => {
-      return total.then(() => {
-        const currentVersion = deps[depName].replace(/[^\d.]/g, '');
 
-        if (!semver.valid(currentVersion)) {
-          return;
-        }
-
-        // supports scoped packages, e.g. @user/package
-        return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true })
-          .then(res => {
-            const latestAvailable = res.body['dist-tags'].latest;
-
-            if (semver.gt(latestAvailable, currentVersion)) {
-              let majorUpgrade = false;
-              if (semver.major(latestAvailable) !== semver.major(currentVersion)) {
-                majorUpgrade = true;
+      // supports scoped packages, e.g. @user/package
+      return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true })
+        .then(res => {
+          let allUpgrades = {};
+          Object.keys(res.body['versions']).forEach(function(version) {
+            if (stable.is(currentVersion) && !stable.is(version)) {
+              return;
+            }
+            if (semver.gt(version, currentVersion)) {
+              var thisMajor = semver.major(version);
+              if (!allUpgrades[thisMajor] || semver.gt(version, allUpgrades[thisMajor])) {
+                allUpgrades[thisMajor] = version;
               }
-              return updateDependency(depType, depName, latestAvailable, majorUpgrade)
             }
           });
-      });
-    }, Promise.resolve());
-  }
-
-  function updateDependency(depType, depName, nextVersion, majorUpgrade) {
-    let branchName = `upgrade/${depName}`;
-    if (majorUpgrade) {
-      branchName += '-major';
-	}
-    // try to checkout remote branche
-    try {
-      nativeCall(`git checkout ${branchName}`);
-    } catch (e) {
-      nativeCall(`git checkout -b ${branchName}`);
-    }
 
-    return updateBranch(branchName, depType, depName, nextVersion, majorUpgrade)
-      .then(() => nativeCall(`git checkout master`));
-  }
+          let upgradePromises = [];
 
-  function updateBranch(branchName, depType, depName, nextVersion, majorUpgrade) {
-    let commit;
+          Object.keys(allUpgrades).forEach(function(upgrade) {
+            const nextVersion = allUpgrades[upgrade];
+            upgradePromises.push(updateDependency(depType, depName, currentVersion, nextVersion));
+          });
 
-    return repo.getBranchCommit(branchName)
-      .then(_commit => {
-        commit = _commit;
-        return readFile(commit, packageFile);
-      })
-      .then(blob => {
-        const pkg = JSON.parse(String(blob));
+          return Promise.all(upgradePromises);
+        });
+    });
+  }, Promise.resolve());
+}
 
-        if (pkg[depType][depName] === nextVersion) {
-          return;
+function updateDependency(depType, depName, currentVersion, nextVersion) {
+  const nextVersionMajor = semver.major(nextVersion);
+  const branchName = `upgrade/${depName}-${nextVersionMajor}.x`;
+  let prName = '';
+  if (nextVersionMajor > semver.major(currentVersion)) {
+    prName = `Upgrade dependency ${depName} to version ${nextVersionMajor}.x`;
+    // Check if PR was already closed previously
+    ghGot(`repos/${repoName}/pulls?state=closed&head=${userName}:${branchName}`, { token: token })
+      .then(res => {
+        if (res.body.length > 0) {
+          console.log(`Dependency ${depName} upgrade to ${nextVersionMajor}.x PR already existed, so skipping`);
+        } else {
+          writeUpdates(depType, depName, branchName, prName, nextVersion);
         }
-
-        pkg[depType][depName] = nextVersion;
-        fs.writeFileSync(`${repoPath}/${packageFile}`, JSON.stringify(pkg, null, 2) + '\n');
-
-        return commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade);
       });
+  } else {
+    prName = `Upgrade dependency ${depName} to version ${nextVersion}`;
+    writeUpdates(depType, depName, branchName, prName, nextVersion);
   }
+}
 
-  function commitAndPush(commit, depName, nextVersion, branchName, majorUpgrade) {
-    let updateMessage = `Update ${depName} to version ${nextVersion}`;
-    if (majorUpgrade) {
-      updateMessage += ' (MAJOR)';
+function writeUpdates(depType, depName, branchName, prName, nextVersion) {
+  const commitMessage = `Upgrade dependency ${depName} to version ${nextVersion}`;
+  // Try to create branch
+  const body = {
+    ref: `refs/heads/${branchName}`,
+    sha: masterSHA
+  };
+  ghGot.post(`repos/${repoName}/git/refs`, {
+    token: token,
+    body: body
+  }).catch(error => {
+    if (error.response.body.message !== 'Reference already exists') {
+      console.log('Error creating branch' + branchName);
+      console.log(error.response.body);
     }
-    console.log(updateMessage);
-
-    let index;
-
-    return repo
-      .refreshIndex()
-      .then(indexResult => {
-        index = indexResult;
-        return index.addByPath(packageFile);
-      })
-      .then(() => index.write())
-      .then(() => index.writeTree())
-      .then(oid => {
-        let author;
-
-        if (authorName && authorEmail) {
-          const date = new Date();
+  }).then(res => {
+    ghGot(`repos/${repoName}/contents/${packageFile}?ref=${branchName}`, { token: token })
+    .then(res => {
+      const oldFileSHA = res.body.sha;
+      let branchPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString());
+      if (branchPackageJson[depType][depName] !== nextVersion) {
+        // Branch is new, or needs version updated
+        console.log(`Dependency ${depName} needs upgrading to ${nextVersion}`);
+        branchPackageJson[depType][depName] = nextVersion;
+        branchPackageString = JSON.stringify(branchPackageJson, null, 2) + '\n';
+
+        ghGot.put(`repos/${repoName}/contents/${packageFile}`, {
+          token: token,
+          body: {
+            branch: branchName,
+            sha: oldFileSHA,
+            message: commitMessage,
+            content: new Buffer(branchPackageString).toString('base64')
+          }
+        }).then(res => {
+          return createOrUpdatePullRequest(branchName, prName);
+        });
+      }
+    });
+  })
+  .catch(error => {
+    console.log('Promise catch');
+  });
+}
 
-          author = Git.Signature.create(
-            authorName,
-            authorEmail,
-            Math.floor(date.getTime() / 1000),
-            -date.getTimezoneOffset()
-          );
-        } else {
-          author = repo.defaultSignature();
+function createOrUpdatePullRequest(branchName, title) {
+  return ghGot.post(`repos/${repoName}/pulls`, {
+    token: token,
+    body: {
+      title: title,
+      head: branchName,
+      base: 'master',
+      body: ''
+    }
+  }).then(res => {
+    console.log('Created Pull Request: ' + title);
+  }).catch(error => {
+    if (error.response.body.errors[0].message.indexOf('A pull request already exists') === 0) {
+      // Pull Request already exists
+      // Now we need to find the Pull Request number
+      return ghGot(`repos/${repoName}/pulls?base=master&head=${userName}:${branchName}`, {
+        token: token,
+      }).then(res => {
+        // TODO iterate through list and confirm branch
+        if (res.body.length !== 1) {
+          console.error('Could not find matching PR');
+          return;
         }
-
-        return repo.createCommit('HEAD', author, author, updateMessage, oid, [commit]);
-      })
-      .then(() => Git.Remote.lookup(repo, 'origin'))
-      .then(origin => {
-        return origin.push(
-          [`refs/heads/${branchName}:refs/heads/${branchName}`], {
-            callbacks: {
-              credentials: getCredentials
-            }
+        const existingPrNo = res.body[0].number;
+        return ghGot.patch(`repos/${repoName}/pulls/${existingPrNo}`, {
+          token: token,
+          body: {
+            title: title
           }
-        );
-      })
-      .then(() => {
-        let prTitle = `Update ${depName}`;
-        if (majorUpgrade) {
-        	prTitle += ' (MAJOR)';
-        }
-        return createPullRequest(branchName, prTitle);
+        }).then(res => {
+          console.log('Updated Pull Request: ' + title);
+        });
       });
-  }
-
-  function createPullRequest(branchName, updateMessage) {
-    const head = `${branchName}`;
-    const options = {
-      method: 'POST',
-      json: true,
-      headers: {
-        Authorization: `token ${token}`
-      },
-      body: JSON.stringify({
-        title: updateMessage,
-        body: '',
-        head,
-        base: 'master'
-      })
-    };
-
-    return got(`https://api.github.com/repos/${repoName}/pulls`, options)
-      .then(
-        null,
-        err => {
-          let logError = true;
-
-          try {
-            if (err.response.body.errors.find(e => e.message.indexOf('A pull request already exists') === 0)) {
-              logError = false;
-            }
-          } catch (e) {
-          }
-
-          if (logError) {
-            console.log(err);
-          }
-        }
-      );
-  }
-
-  function readFile(commit, filename) {
-    return commit
-      .getEntry(packageFile)
-      .then(entry => entry.getBlob())
-      .then(blob => String(blob));
-  }
-
-  function getCredentials(url, userName) {
-    // https://github.com/nodegit/nodegit/issues/1133#issuecomment-261779939
-    return Git.Cred.sshKeyNew(
-      userName,
-      sshPublicKeyPath,
-      sshPrivateKeyPath,
-      ''
-    );
-  }
-
-  function nativeCall(cmd) {
-    return require('child_process').execSync(cmd, { cwd: repoPath, stdio: [null, null, null] });
-  }
+    } else {
+      console.log('Error creating Pull Request:');
+      console.log(error.response.body);
+      Promise.reject();
+    }
+  });
 }
-- 
GitLab