From b4aaa65b5cec2008620022719ddfb0fc83975c43 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Sat, 7 Jan 2017 08:22:48 +0100
Subject: [PATCH] Move code into github and npm helper libraries

commit 8e84875bd5f7e4584d707d88d6850565bb02c79c
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Sat Jan 7 08:22:21 2017 +0100

    Synchronous

commit 0f24ea192bcf54aae1264e91a4b6eb98fea55448
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Sat Jan 7 07:12:20 2017 +0100

    externalise more npm

commit 458d60975fc967f1373c81cd0fa28a9717dd9b0b
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 15:45:08 2017 +0100

    Externalise npm

commit 5d4f39e72d2977af1fec12d7a0a39d3877e4ad02
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 15:35:16 2017 +0100

    Remove ghGot

commit 06898801c1e591d6db9e6ac1e565233af5e9be7e
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 15:34:43 2017 +0100

    Externalise PR functions

commit 0b0e0f781b3384ad57a1df3df7d1089b2c72079a
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 15:34:25 2017 +0100

    Enable verbose

commit 4cebf1e0a80d7e14b9704c5fd7e5d0b036b9661a
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 14:23:12 2017 +0100

    verbose

commit 5a984b91e099cccb5c9dff857a6be07b3b4dedd5
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 14:22:59 2017 +0100

    Change default branch naming

commit ab9bc952c81d16be9be57227382dff8d05e73f54
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 13:05:08 2017 +0100

    Fix branch matching

commit eeecf17e196245964aed5247cf1703619d42b0d4
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 11:15:16 2017 +0100

    Update message

commit d27b345c5eb51dcb7e32b903beafe0728e24bfdb
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 11:09:39 2017 +0100

    Refactor file write

commit 7f12ef69f456ecd064be5d9851157131222f7700
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 10:59:24 2017 +0100

    Refactor writeFile

commit 8c7cc9e6a6c7e398aa60cb828c16ff51f36f2efa
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 10:39:27 2017 +0100

    Refactor getFile

commit b4338ade6d29b830ead657267248c93216c2f91d
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 10:15:02 2017 +0100

    refactor

commit dc4aeb39dad367844836da7f93e9f167864f6030
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 10:14:34 2017 +0100

    createBranch

commit d6a357f609de55d7b934652f30592219391a9884
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 10:04:04 2017 +0100

    Add createBranch

commit 11ba4e9f6c2153d7b783670944570cb4968ff718
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 07:27:08 2017 +0100

    Rename

commit 7a4be0fde0e070e2149bc4c34397c4903096ac51
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 07:17:31 2017 +0100

    Externalise some github functions

commit e393e92bcc9cb548fac3637644b0330a136f3611
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 07:17:19 2017 +0100

    Fix error message

commit 59fb50656d84491780bc31bab4cb9263a7912c03
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Fri Jan 6 07:16:59 2017 +0100

    Improve error checks

commit bc44b3a0d820ab5756c3b3c746402329e5b52703
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Thu Jan 5 15:34:04 2017 +0100

    Make base branch configurable

commit b9d31776814723d991a226d1ca1b2f39d0d2af85
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Thu Jan 5 15:33:44 2017 +0100

    Reorder early lines

commit b75f9f25cfb86f029b73445aae67b7889ff09b3e
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Thu Jan 5 15:26:47 2017 +0100

    Error if RENOVATE_TOKEN is undefined

    Closes #11

commit 34e13a70326a71b3ee7f18c12ec3de55b78bcaa1
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Thu Jan 5 14:43:42 2017 +0100

    arrow functions

commit 6006db2deae887938bc20a07c93d1a59bd8cd74e
Author: Rhys Arkins <rhys@keylocation.sg>
Date:   Thu Jan 5 14:39:30 2017 +0100

    Refactor templates
---
 src/config.js |  21 +++++
 src/github.js | 119 ++++++++++++++++++++++++++
 src/index.js  | 227 +++++++++++++++++++++-----------------------------
 src/npm.js    |  37 ++++++++
 4 files changed, 274 insertions(+), 130 deletions(-)
 create mode 100644 src/config.js
 create mode 100644 src/github.js
 create mode 100644 src/npm.js

diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000000..d88a4dcb7c
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,21 @@
+module.exports = {
+  verbose: false,
+  baseBranch: 'master',
+  templates: {
+    branchName: (params) => {
+      return `renovate/${params.depName}-${params.nextVersionMajor}.x`;
+    },
+    commitMessage: (params) => {
+      return `Upgrade dependency ${params.depName} to version ${params.nextVersion}`;
+    },
+    prBody: (params) => {
+      return `This Pull Request updates dependency ${params.depName} from version ${params.currentVersion} to ${params.nextVersion}.`;
+    },
+    prTitleMajor: (params) => {
+      return `Upgrade dependency ${params.depName} to version ${params.nextVersionMajor}.x`;
+    },
+    prTitleMinor: (params) => {
+      return `Upgrade dependency ${params.depName} to version ${params.nextVersion}`;
+    },
+  }
+};
diff --git a/src/github.js b/src/github.js
new file mode 100644
index 0000000000..109c9858d3
--- /dev/null
+++ b/src/github.js
@@ -0,0 +1,119 @@
+const ghGot = require('gh-got');
+
+var config = {};
+
+module.exports = {
+  // Initialize GitHub by getting base branch SHA
+  init: function(token, repoName, baseBranch, verbose = false) {
+    config.token = token;
+    config.repoName = repoName;
+    config.baseBranch = baseBranch;
+    config.verbose = verbose;
+
+    config.userName = repoName.split('/')[0];
+
+    return ghGot(`repos/${config.repoName}/git/refs/head`, {token: config.token}).then(res => {
+      // First, get the SHA for base branch
+      res.body.forEach(function(branch) {
+        // Loop through all branches because the base branch may not be the first
+        if (branch.ref === `refs/heads/${config.baseBranch}`) {
+          // This is the SHA we will create new branches from
+          config.baseSHA = branch.object.sha;
+        }
+      });
+    }).catch(function(err) {
+      console.log('init error: ' + err);
+    });
+  },
+  createBranch: function(branchName) {
+    return ghGot.post(`repos/${config.repoName}/git/refs`, {
+      token: config.token,
+      body: {
+        ref: `refs/heads/${branchName}`,
+        sha: config.baseSHA,
+      },
+    });
+  },
+  createPr: function(branchName, title, body) {
+    return ghGot.post(`repos/${config.repoName}/pulls`, {
+      token: config.token,
+      body: {
+        title: title,
+        head: branchName,
+        base: config.baseBranch,
+        body: body,
+      }
+    });
+  },
+  checkPrExists(branchName, prTitle) {
+    if (config.verbose) {
+      console.log(`Checking if branch/PR exists: ${branchName} / ${prTitle}`);
+    }
+    return ghGot(`repos/${config.repoName}/pulls?state=closed&head=${config.userName}:${branchName}`, { token: config.token })
+      .then(res => {
+        if (config.verbose) {
+          console.log(`Got ${res.body.length} results for ${branchName}`);
+        }
+        let prAlreadyExists = false;
+        res.body.forEach(function(result) {
+          if (result.title === prTitle && result.head.label === `${config.userName}:${branchName}`) {
+            prAlreadyExists = true;
+          }
+        });
+        if (config.verbose) {
+          if (prAlreadyExists) {
+            console.log(`PR already exists for ${branchName}`);
+          } else {
+            console.log(`PR doesn't exist for ${branchName}`);
+          }
+        }
+        return prAlreadyExists;
+      }).catch((err) => {
+        console.error('Error checking if PR already existed');
+      });
+  },
+  getFile: function(filePath, branchName) {
+    branchName = branchName || config.baseBranch;
+    return ghGot(`repos/${config.repoName}/contents/${filePath}?ref=${branchName}`, {
+      token: config.token,
+    });
+  },
+  getFileContents: function(filePath, branchName) {
+    return this.getFile(filePath, branchName).then(res => {
+      return JSON.parse(new Buffer(res.body.content, 'base64').toString());
+    });
+  },
+  getPrNo: function(branchName) {
+    return ghGot(`repos/${config.repoName}/pulls?base=${config.baseBranch}&head=${config.userName}:${branchName}`, {
+      token: config.token,
+    }).then(res => {
+      let prNo = 0;
+      res.body.forEach(function(result) {
+        if (result.state === 'open' && result.head.label === `${config.userName}:${branchName}`) {
+          prNo = result.number;
+        }
+      });
+      return prNo;
+    });
+  },
+  writeFile: function(branchName, oldFileSHA, filePath, fileContents, message) {
+    return ghGot.put(`repos/${config.repoName}/contents/${filePath}`, {
+      token: config.token,
+      body: {
+        branch: branchName,
+        sha: oldFileSHA,
+        message: message,
+        content: new Buffer(fileContents).toString('base64')
+      }
+    });
+  },
+  updatePr: function(prNo, title, body) {
+    return ghGot.patch(`repos/${config.repoName}/pulls/${prNo}`, {
+      token: config.token,
+      body: {
+        title: title,
+        body: body,
+      },
+    });
+  },
+};
diff --git a/src/index.js b/src/index.js
index 374ca1f9c2..1b975ea056 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,138 +1,127 @@
-const ghGot = require('gh-got');
-const got = require('got');
 const semver = require('semver');
 const stable = require('semver-stable');
 
+const config = require('./config');
+const github = require('./github');
+const npm = require('./npm');
+
 const token = process.env.RENOVATE_TOKEN;
+// token must be defined
+if (typeof token === 'undefined') {
+  console.error('Error: Environment variable RENOVATE_TOKEN must be defined');
+  process.exit(1);
+}
+
+if (process.argv.length < 3 || process.argv.length > 4) {
+  console.error('Error: You must specify the GitHub repository and optionally path.');
+  console.log('Example: node src singapore/renovate');
+  console.log('Example: node src foo/bar baz/package.json');
+  process.exit(1);
+}
+
+// Process command line arguments
 const repoName = process.argv[2];
 const userName = repoName.split('/')[0];
 const packageFile = process.argv[3] || 'package.json';
 
-let masterSHA;
-let masterPackageJson;
+npm.init(config.verbose);
 
-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');
-  });
+let basePackageJson;
+
+github.init(token, repoName, config.baseBranch, config.verbose).then(() => {
+  return github.getFileContents(packageFile);
+}).then((packageContents) => {
+  basePackageJson = packageContents;
+  return iterateDependencies('dependencies');
+}).then(() => {
+  iterateDependencies('devDependencies');
+}).catch(err => {
+  console.log('Error: ' + err);
 });
 
 function iterateDependencies(depType) {
-  const deps = masterPackageJson[depType];
+  const deps = basePackageJson[depType];
   if (!deps) {
     return;
   }
+  console.log(`Checking ${Object.keys(deps).length} ${depType}`);
   return Object.keys(deps).reduce((total, depName) => {
     return total.then(() => {
+      if (config.verbose) {
+        console.log(' * ' + depName);
+      }
       const currentVersion = deps[depName].replace(/[^\d.]/g, '');
-
       if (!semver.valid(currentVersion)) {
-        console.log('Invalid current version');
+        console.log(`${depName}: Invalid current version`);
         return;
       }
 
-      // 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;
-              }
-            }
-          });
-
-          let upgradePromises = [];
-
-          Object.keys(allUpgrades).forEach(function(upgrade) {
-            const nextVersion = allUpgrades[upgrade];
-            upgradePromises.push(updateDependency(depType, depName, currentVersion, nextVersion));
+      return npm.getDependencyUpgrades(depName, currentVersion)
+      .then(allUpgrades => {
+        if (config.verbose) {
+          console.log(`All upgrades for ${depName}: ${JSON.stringify(allUpgrades)}`);
+        }
+        return Object.keys(allUpgrades).reduce((promiseChain, upgrade) => {
+          return promiseChain.then(() => {
+            return updateDependency(depType, depName, currentVersion, allUpgrades[upgrade]);
           });
-
-          return Promise.all(upgradePromises);
-        });
+        }, Promise.resolve());
+      });
     });
   }, Promise.resolve());
 }
 
 function updateDependency(depType, depName, currentVersion, nextVersion) {
   const nextVersionMajor = semver.major(nextVersion);
-  const branchName = `upgrade/${depName}-${nextVersionMajor}.x`;
-  let prName = '';
+  const branchName = config.templates.branchName({depType, depName, currentVersion, nextVersion, nextVersionMajor});
+  let prTitle = '';
   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, currentVersion, nextVersion);
-        }
-      });
+    prTitle = config.templates.prTitleMajor({ depType, depName, currentVersion, nextVersion, nextVersionMajor });
   } else {
-    prName = `Upgrade dependency ${depName} to version ${nextVersion}`;
-    writeUpdates(depType, depName, branchName, prName, currentVersion, nextVersion);
+    prTitle = config.templates.prTitleMinor({ depType, depName, currentVersion, nextVersion, nextVersionMajor });
   }
+  // Check if same PR already exists or existed
+  return github.checkPrExists(branchName, prTitle).then((prExisted) => {
+    if (!prExisted) {
+      return writeUpdates(depType, depName, branchName, prTitle, currentVersion, nextVersion);
+    } else {
+      console.log(`${depName}: Skipping due to existing PR found.`);
+    }
+  });
 }
 
-function writeUpdates(depType, depName, branchName, prName, currentVersion, nextVersion) {
-  const commitMessage = `Upgrade dependency ${depName} to version ${nextVersion}`;
-  const prBody = `This Pull Request updates dependency ${depName} from version ${currentVersion} to ${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 => {
+function writeUpdates(depType, depName, branchName, prTitle, currentVersion, nextVersion) {
+  const prBody = config.templates.prBody({ depName, currentVersion, nextVersion });
+  return github.createBranch(branchName).catch(error => {
     if (error.response.body.message !== 'Reference already exists') {
-      console.log('Error creating branch' + branchName);
+      console.log('Error creating branch: ' + branchName);
       console.log(error.response.body);
     }
   }).then(res => {
-    ghGot(`repos/${repoName}/contents/${packageFile}?ref=${branchName}`, { token: token })
-    .then(res => {
+    if (config.verbose) {
+      console.log(`Branch exists (${branchName}), now writing file`);
+    }
+    return github.getFile(packageFile, branchName).then(res => {
       const oldFileSHA = res.body.sha;
-      let branchPackageJson = JSON.parse(new Buffer(res.body.content, 'base64').toString());
-      if (branchPackageJson[depType][depName] !== nextVersion) {
+      let currentFileContent = JSON.parse(new Buffer(res.body.content, 'base64').toString());
+      if (currentFileContent[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';
+        currentFileContent[depType][depName] = nextVersion;
+        const newPackageString = JSON.stringify(currentFileContent, 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, prBody);
+        var commitMessage = config.templates.commitMessage({ depName, currentVersion, nextVersion });
+
+        return github.writeFile(branchName, oldFileSHA, packageFile, newPackageString, commitMessage)
+        .then(() => {
+          return createOrUpdatePullRequest(branchName, prTitle, prBody);
+        })
+        .catch(err => {
+          console.error('Error writing new package file for ' + depName);
+          console.log(err);
         });
+      } else {
+        // File was up to date. Ensure PR
+        return createOrUpdatePullRequest(branchName, prTitle, prBody);
       }
     });
   })
@@ -141,44 +130,22 @@ function writeUpdates(depType, depName, branchName, prName, currentVersion, next
   });
 }
 
-function createOrUpdatePullRequest(branchName, title, body) {
-  return ghGot.post(`repos/${repoName}/pulls`, {
-    token: token,
-    body: {
-      title: title,
-      head: branchName,
-      base: 'master',
-      body: 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;
-        }
-        const existingPrNo = res.body[0].number;
-        return ghGot.patch(`repos/${repoName}/pulls/${existingPrNo}`, {
-          token: token,
-          body: {
-            title: title,
-            body: body,
-          }
-        }).then(res => {
-          console.log('Updated Pull Request: ' + title);
-        });
+function createOrUpdatePullRequest(branchName, prTitle, prBody) {
+  return github.getPrNo(branchName).then(prNo => {
+    if (prNo) {
+      // PR already exists - update it
+      // Note: PR might be unchanged, so no log message
+      return github.updatePr(prNo, prTitle, prBody)
+      .catch(err => {
+        console.error('Error: Failed to update Pull Request: ' + prTitle);
+        console.log(err);
       });
-    } else {
-      console.log('Error creating Pull Request:');
-      console.log(error.response.body);
-      Promise.reject();
     }
+    return github.createPr(branchName, prTitle, prBody).then(res => {
+      console.log('Created Pull Request: ' + prTitle);
+    }).catch(err => {
+      console.error('Error: Failed to create Pull Request: ' + prTitle);
+      console.log(err);
+    });
   });
 }
diff --git a/src/npm.js b/src/npm.js
new file mode 100644
index 0000000000..d14aaf4571
--- /dev/null
+++ b/src/npm.js
@@ -0,0 +1,37 @@
+const got = require('got');
+const semver = require('semver');
+const stable = require('semver-stable');
+
+var config = {};
+
+module.exports = {
+  init: function(verbose = false) {
+    config.verbose = verbose;
+  },
+  getDependency(depName) {
+    if (config.verbose) {
+      console.log(`Looking up npm for ${depName}`);
+    }
+    // supports scoped packages, e.g. @user/package
+    return got(`https://registry.npmjs.org/${depName.replace('/', '%2F')}`, { json: true });
+  },
+  getDependencyUpgrades(depName, currentVersion) {
+    return this.getDependency(depName).then(res => {
+      let allUpgrades = {};
+      Object.keys(res.body['versions']).forEach(function(version) {
+        if (stable.is(currentVersion) && !stable.is(version)) {
+          // Ignore unstable versions, unless the current version is unstable
+          return;
+        }
+        if (semver.gt(version, currentVersion)) {
+          // Group by major versions
+          var thisMajor = semver.major(version);
+          if (!allUpgrades[thisMajor] || semver.gt(version, allUpgrades[thisMajor])) {
+            allUpgrades[thisMajor] = version;
+          }
+        }
+      });
+      return allUpgrades;
+    });
+  },
+};
-- 
GitLab