diff --git a/lib/platform/github/index.js b/lib/platform/github/index.js
index c0be286527d617cf99ab251618d5a11c4470c70e..afcbac876201ef17b7e22dbe3d9191371695349b 100644
--- a/lib/platform/github/index.js
+++ b/lib/platform/github/index.js
@@ -1,13 +1,12 @@
 const is = require('@sindresorhus/is');
 const addrs = require('email-addresses');
-const moment = require('moment');
-const openpgp = require('openpgp');
 const delay = require('delay');
-const path = require('path');
 const showdown = require('showdown');
 
 const get = require('./gh-got-wrapper');
+const { expandError } = require('./util');
 const endpoints = require('../../util/endpoints');
+const Storage = require('./storage');
 
 const converter = new showdown.Converter();
 converter.setFlavor('github');
@@ -80,19 +79,7 @@ async function getRepos(token, endpoint) {
 function cleanRepo() {
   // In theory most of this isn't necessary. In practice..
   get.reset();
-  config = null;
   config = {};
-  delete config.repository;
-  delete config.repositoryOwner;
-  delete config.repositoryName;
-  delete config.owner;
-  delete config.defaultBranch;
-  delete config.baseBranch;
-  delete config.issueList;
-  delete config.prList;
-  delete config.fileList;
-  delete config.branchList;
-  delete config.forkToken;
 }
 
 // Initialize GitHub by getting base branch and SHA
@@ -151,16 +138,12 @@ async function initRepo({
     }
     platformConfig.privateRepo = res.body.private === true;
     platformConfig.isFork = res.body.fork === true;
-    config.owner = res.body.owner.login;
-    logger.debug(`${repository} owner = ${config.owner}`);
+    const owner = res.body.owner.login;
+    logger.debug(`${repository} owner = ${owner}`);
     // Use default branch as PR target unless later overridden.
     config.defaultBranch = res.body.default_branch;
     // Base branch may be configured but defaultBranch is always fixed
     config.baseBranch = config.defaultBranch;
-    // istanbul ignore if
-    if (process.env.NODE_ENV !== 'test') {
-      getBranchCommit(config.baseBranch); // warm the cache
-    }
     logger.debug(`${repository} default branch = ${config.baseBranch}`);
     // GitHub allows administrators to block certain types of merge, so we need to check it
     if (res.body.allow_rebase_merge) {
@@ -196,15 +179,14 @@ async function initRepo({
   // This shouldn't be necessary, but occasional strange errors happened until it was added
   config.issueList = null;
   config.prList = null;
-  config.fileList = null;
-  config.branchList = null;
+  config.storage = new Storage(config);
   logger.debug('Prefetching prList and fileList');
   await Promise.all([getPrList(), getFileList()]);
   if (forkMode) {
     logger.info('Renovate is in forkMode');
     config.forkToken = forkToken;
     // Save parent SHA then delete
-    config.parentSha = await getBaseCommitSHA();
+    const parentSha = await getBaseCommitSHA();
     config.baseCommitSHA = null;
     // save parent name then delete
     config.parentRepo = config.repository;
@@ -224,7 +206,7 @@ async function initRepo({
       );
       // Need to update base branch
       logger.debug(
-        { baseBranch: config.baseBranch, parentSha: config.parentSha },
+        { baseBranch: config.baseBranch, parentSha },
         'Setting baseBranch ref in fork'
       );
       // This is a lovely "hack" by GitHub that lets us force update our fork's master
@@ -233,7 +215,7 @@ async function initRepo({
         `repos/${config.repository}/git/refs/heads/${config.baseBranch}`,
         {
           body: {
-            sha: config.parentSha,
+            sha: parentSha,
           },
           token: forkToken || opts.token,
         }
@@ -243,6 +225,7 @@ async function initRepo({
       // Wait an arbitrary 30s to hopefully give GitHub enough time for forking to complete
       await delay(30000);
     }
+    config.storage = new Storage(config);
   }
   // istanbul ignore if
   if (mirrorMode) {
@@ -272,7 +255,10 @@ async function initRepo({
       );
     }
     if (!(await branchExists('renovate-config'))) {
-      await createBranch('renovate-config', config.baseCommitSHA);
+      await config.storage.createBranch(
+        'renovate-config',
+        config.baseCommitSHA
+      );
     }
   }
   return platformConfig;
@@ -325,7 +311,9 @@ async function getRepoForceRebase() {
 
 async function getBaseCommitSHA() {
   if (!config.baseCommitSHA) {
-    config.baseCommitSHA = await getBranchCommit(config.baseBranch);
+    config.baseCommitSHA = await config.storage.getBranchCommit(
+      config.baseBranch
+    );
   }
   return config.baseCommitSHA;
 }
@@ -346,7 +334,7 @@ async function setBaseBranch(branchName) {
     logger.debug(`Setting baseBranch to ${branchName}`);
     config.baseBranch = branchName;
     config.baseCommitSHA = null;
-    config.fileList = null;
+    config.storage = new Storage(config);
     await getFileList(branchName);
   }
 }
@@ -354,104 +342,64 @@ async function setBaseBranch(branchName) {
 // Search
 
 // Get full file list
-async function getFileList(branchName = config.baseBranch) {
-  if (config.fileList) {
-    return config.fileList;
-  }
-  try {
-    const res = await get(
-      `repos/${config.repository}/git/trees/${branchName}?recursive=true`
-    );
-    if (res.body.truncated) {
-      logger.warn(
-        { repository: config.repository },
-        'repository tree is truncated'
-      );
-    }
-    config.fileList = res.body.tree
-      .filter(item => item.type === 'blob' && item.mode !== '120000')
-      .map(item => item.path)
-      .sort();
-    logger.debug(`Retrieved fileList with length ${config.fileList.length}`);
-  } catch (err) /* istanbul ignore next */ {
-    if (err.statusCode === 409) {
-      logger.debug('Repository is not initiated');
-      throw new Error('uninitiated');
-    }
-    logger.info(
-      { repository: config.repository },
-      'Error retrieving git tree - no files detected'
-    );
-    config.fileList = [];
-  }
-
-  return config.fileList;
+function getFileList(branchName = config.baseBranch) {
+  return config.storage.getFileList(branchName);
 }
 
 // Branch
 
 // Returns true if branch exists, otherwise false
-async function branchExists(branchName) {
-  if (!config.branchList) {
-    logger.debug('Retrieving branchList');
-    config.branchList = (await get(
-      `repos/${config.repository}/branches?per_page=100`,
-      {
-        paginate: true,
-      }
-    )).body.map(branch => branch.name);
-    logger.debug({ branchList: config.branchList }, 'Retrieved branchList');
-  }
-  const res = config.branchList.includes(branchName);
-  logger.debug(`branchExists(${branchName})=${res}`);
-  return res;
+function branchExists(branchName) {
+  return config.storage.branchExists(branchName);
 }
 
-async function getAllRenovateBranches(branchPrefix) {
-  logger.trace('getAllRenovateBranches');
-  try {
-    const allBranches = (await get(
-      `repos/${config.repository}/git/refs/heads/${branchPrefix}`,
-      {
-        paginate: true,
-      }
-    )).body;
-    return allBranches.reduce((arr, branch) => {
-      if (branch.ref.startsWith(`refs/heads/${branchPrefix}`)) {
-        arr.push(branch.ref.substring('refs/heads/'.length));
-      }
-      if (
-        branchPrefix.endsWith('/') &&
-        branch.ref === `refs/heads/${branchPrefix.slice(0, -1)}`
-      ) {
-        logger.warn(
-          `Pruning branch "${branchPrefix.slice(
-            0,
-            -1
-          )}" so that it does not block PRs`
-        );
-        arr.push(branch.ref.substring('refs/heads/'.length));
-      }
-      return arr;
-    }, []);
-  } catch (err) /* istanbul ignore next */ {
-    return [];
+function getAllRenovateBranches(branchPrefix) {
+  return config.storage.getAllRenovateBranches(branchPrefix);
+}
+
+function isBranchStale(branchName) {
+  return config.storage.isBranchStale(branchName);
+}
+
+function getFile(filePath, branchName) {
+  return config.storage.getFile(filePath, branchName);
+}
+
+function deleteBranch(branchName) {
+  return config.storage.deleteBranch(branchName);
+}
+
+function getBranchLastCommitTime(branchName) {
+  return config.storage.getBranchLastCommitTime(branchName);
+}
+
+function mergeBranch(branchName) {
+  // istanbul ignore if
+  if (config.pushProtection) {
+    logger.info(
+      { branchName },
+      'Branch protection: Attempting to merge branch when push protection is enabled'
+    );
   }
+  return config.storage.mergeBranch(branchName);
 }
 
-async function isBranchStale(branchName) {
-  // Check if branch's parent SHA = master SHA
-  logger.debug(`isBranchStale(${branchName})`);
-  const branchCommit = await getBranchCommit(branchName);
-  logger.debug(`branchCommit=${branchCommit}`);
-  const commitDetails = await getCommitDetails(branchCommit);
-  logger.trace({ commitDetails }, `commitDetails`);
-  const parentSha = commitDetails.parents[0].sha;
-  logger.debug(`parentSha=${parentSha}`);
-  const baseCommitSHA = await getBaseCommitSHA();
-  logger.debug(`baseCommitSHA=${baseCommitSHA}`);
-  // Return true if the SHAs don't match
-  return parentSha !== baseCommitSHA;
+function commitFilesToBranch(
+  branchName,
+  files,
+  message,
+  parentBranch = config.baseBranch
+) {
+  return config.storage.commitFilesToBranch(
+    branchName,
+    files,
+    message,
+    parentBranch
+  );
+}
+
+function getCommitMessages() {
+  return config.storage.getCommitMessages();
 }
 
 // Returns the Pull Request for a branch. Null if not exists.
@@ -484,7 +432,7 @@ async function getBranchStatus(branchName, requiredStatusChecks) {
 }
 
 async function getBranchStatusCheck(branchName, context) {
-  const branchCommit = await getBranchCommit(branchName);
+  const branchCommit = await config.storage.getBranchCommit(branchName);
   const url = `repos/${config.repository}/commits/${branchCommit}/statuses`;
   const res = await get(url);
   for (const check of res.body) {
@@ -512,7 +460,7 @@ async function setBranchStatus(
     return;
   }
   logger.info({ branchName, context, state }, 'Setting branch status');
-  const branchCommit = await getBranchCommit(branchName);
+  const branchCommit = await config.storage.getBranchCommit(branchName);
   const url = `repos/${config.repository}/statuses/${branchCommit}`;
   const options = {
     state,
@@ -525,67 +473,6 @@ async function setBranchStatus(
   await get.post(url, { body: options });
 }
 
-async function deleteBranch(branchName) {
-  const options = config.forkToken ? { token: config.forkToken } : undefined;
-  try {
-    await get.delete(
-      `repos/${config.repository}/git/refs/heads/${branchName}`,
-      options
-    );
-  } catch (err) /* istanbul ignore next */ {
-    if (err.message.startsWith('Reference does not exist')) {
-      logger.info({ branchName }, 'Branch to delete does not exist');
-    } else {
-      logger.warn(
-        { err, body: err.response.body, branchName },
-        'Error deleting branch'
-      );
-    }
-  }
-}
-
-async function mergeBranch(branchName) {
-  logger.debug(`mergeBranch(${branchName})`);
-  // istanbul ignore if
-  if (config.pushProtection) {
-    logger.info(
-      { branchName },
-      'Branch protection: Attempting to merge branch when push protection is enabled'
-    );
-  }
-  const url = `repos/${config.repository}/git/refs/heads/${config.baseBranch}`;
-  const options = {
-    body: {
-      sha: await getBranchCommit(branchName),
-    },
-  };
-  try {
-    await get.patch(url, options);
-  } catch (err) {
-    logger.info(
-      expandError(err),
-      `Error pushing branch merge for ${branchName}`
-    );
-    throw new Error('Branch automerge failed');
-  }
-  // Update base commit
-  config.baseCommitSHA = null;
-  // Delete branch
-  await deleteBranch(branchName);
-}
-
-async function getBranchLastCommitTime(branchName) {
-  try {
-    const res = await get(
-      `repos/${config.repository}/commits?sha=${branchName}`
-    );
-    return new Date(res.body[0].commit.committer.date);
-  } catch (err) {
-    logger.error(expandError(err), `getBranchLastCommitTime error`);
-    return new Date();
-  }
-}
-
 // Issue
 
 async function getIssueList() {
@@ -1136,318 +1023,6 @@ function getPrBody(input) {
   );
 }
 
-// Generic File operations
-
-async function getFile(filePath, branchName) {
-  logger.trace(`getFile(filePath=${filePath}, branchName=${branchName})`);
-  if (!branchName || branchName === config.baseBranch) {
-    if (!config.fileList.includes(filePath)) {
-      return null;
-    }
-  }
-  let res;
-  try {
-    res = await get(
-      `repos/${config.repository}/contents/${encodeURI(
-        filePath
-      )}?ref=${branchName || config.baseBranch}`
-    );
-  } catch (error) {
-    if (error.statusCode === 404) {
-      // If file not found, then return null JSON
-      logger.info({ filePath, branchName }, 'getFile 404');
-      return null;
-    }
-    if (
-      error.statusCode === 403 &&
-      error.message &&
-      error.message.startsWith('This API returns blobs up to 1 MB in size')
-    ) {
-      logger.info('Large file');
-      // istanbul ignore if
-      if (branchName && branchName !== config.baseBranch) {
-        logger.info('Cannot retrieve large files from non-master branch');
-        return null;
-      }
-      // istanbul ignore if
-      if (path.dirname(filePath) !== '.') {
-        logger.info('Cannot retrieve large files from non-root directories');
-        return null;
-      }
-      const treeUrl = `repos/${config.repository}/git/trees/${
-        config.baseBranch
-      }`;
-      const baseName = path.basename(filePath);
-      let fileSha;
-      (await get(treeUrl)).body.tree.forEach(file => {
-        if (file.path === baseName) {
-          fileSha = file.sha;
-        }
-      });
-      if (!fileSha) {
-        logger.warn('Could not locate file blob');
-        throw error;
-      }
-      res = await get(`repos/${config.repository}/git/blobs/${fileSha}`);
-    } else {
-      // Propagate if it's any other error
-      throw error;
-    }
-  }
-  if (res.body.content) {
-    return Buffer.from(res.body.content, 'base64').toString();
-  }
-  return null;
-}
-
-// Add a new commit, create branch if not existing
-async function commitFilesToBranch(
-  branchName,
-  files,
-  message,
-  parentBranch = config.baseBranch
-) {
-  logger.debug(
-    `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
-  );
-  const parentCommit = await getBranchCommit(parentBranch);
-  const parentTree = await getCommitTree(parentCommit);
-  const fileBlobs = [];
-  // Create blobs
-  for (const file of files) {
-    const blob = await createBlob(file.contents);
-    fileBlobs.push({
-      name: file.name,
-      blob,
-    });
-  }
-  // Create tree
-  const tree = await createTree(parentTree, fileBlobs);
-  const commit = await createCommit(parentCommit, tree, message);
-  const isBranchExisting = await branchExists(branchName);
-  try {
-    if (isBranchExisting) {
-      await updateBranch(branchName, commit);
-    } else {
-      await createBranch(branchName, commit);
-    }
-  } catch (err) /* istanbul ignore next */ {
-    logger.debug({
-      files: files.filter(
-        file =>
-          !file.name.endsWith('package-lock.json') &&
-          !file.name.endsWith('npm-shrinkwrap.json') &&
-          !file.name.endsWith('yarn.lock')
-      ),
-    });
-    throw err;
-  }
-}
-
-// Internal branch operations
-
-// Creates a new branch with provided commit
-async function createBranch(branchName, sha) {
-  logger.debug(`createBranch(${branchName})`);
-  const options = {
-    body: {
-      ref: `refs/heads/${branchName}`,
-      sha,
-    },
-  };
-  // istanbul ignore if
-  if (config.forkToken) {
-    options.token = config.forkToken;
-  }
-  try {
-    // istanbul ignore if
-    if (branchName.includes('/')) {
-      const [blockingBranch] = branchName.split('/');
-      if (await branchExists(blockingBranch)) {
-        logger.warn({ blockingBranch }, 'Deleting blocking branch');
-        await deleteBranch(blockingBranch);
-      }
-    }
-    logger.debug({ options, branchName }, 'Creating branch');
-    await get.post(`repos/${config.repository}/git/refs`, options);
-    config.branchList.push(branchName);
-    logger.debug('Created branch');
-  } catch (err) /* istanbul ignore next */ {
-    const headers = err.response.req.getHeaders();
-    delete headers.token;
-    logger.warn(
-      {
-        err,
-        message: err.message,
-        responseBody: err.response.body,
-        headers,
-        options,
-      },
-      'Error creating branch'
-    );
-    if (err.statusCode === 422) {
-      throw new Error('repository-changed');
-    }
-    throw err;
-  }
-}
-
-// Internal: Updates an existing branch to new commit sha
-async function updateBranch(branchName, commit) {
-  logger.debug(`Updating branch ${branchName} with commit ${commit}`);
-  const options = {
-    body: {
-      sha: commit,
-      force: true,
-    },
-  };
-  // istanbul ignore if
-  if (config.forkToken) {
-    options.token = config.forkToken;
-  }
-  try {
-    await get.patch(
-      `repos/${config.repository}/git/refs/heads/${branchName}`,
-      options
-    );
-  } catch (err) /* istanbul ignore next */ {
-    if (err.statusCode === 422) {
-      logger.info(expandError(err), 'Branch no longer exists - exiting');
-      throw new Error('repository-changed');
-    }
-    throw err;
-  }
-}
-
-// Low-level commit operations
-
-// Create a blob with fileContents and return sha
-async function createBlob(fileContents) {
-  logger.debug('Creating blob');
-  const options = {
-    body: {
-      encoding: 'base64',
-      content: Buffer.from(fileContents).toString('base64'),
-    },
-  };
-  // istanbul ignore if
-  if (config.forkToken) {
-    options.token = config.forkToken;
-  }
-  return (await get.post(`repos/${config.repository}/git/blobs`, options)).body
-    .sha;
-}
-
-// Return the commit SHA for a branch
-async function getBranchCommit(branchName) {
-  const res = await get(
-    `repos/${config.repository}/git/refs/heads/${branchName}`
-  );
-  return res.body.object.sha;
-}
-
-async function getCommitDetails(commit) {
-  logger.debug(`getCommitDetails(${commit})`);
-  const results = await get(`repos/${config.repository}/git/commits/${commit}`);
-  return results.body;
-}
-
-// Return the tree SHA for a commit
-async function getCommitTree(commit) {
-  logger.debug(`getCommitTree(${commit})`);
-  return (await get(`repos/${config.repository}/git/commits/${commit}`)).body
-    .tree.sha;
-}
-
-// Create a tree and return SHA
-async function createTree(baseTree, files) {
-  logger.debug(`createTree(${baseTree}, files)`);
-  const body = {
-    base_tree: baseTree,
-    tree: [],
-  };
-  files.forEach(file => {
-    body.tree.push({
-      path: file.name,
-      mode: '100644',
-      type: 'blob',
-      sha: file.blob,
-    });
-  });
-  logger.trace({ body }, 'createTree body');
-  const options = { body };
-  // istanbul ignore if
-  if (config.forkToken) {
-    options.token = config.forkToken;
-  }
-  return (await get.post(`repos/${config.repository}/git/trees`, options)).body
-    .sha;
-}
-
-// Create a commit and return commit SHA
-async function createCommit(parent, tree, message) {
-  logger.debug(`createCommit(${parent}, ${tree}, ${message})`);
-  const { gitAuthor, gitPrivateKey } = config;
-  const now = moment();
-  let author;
-  if (gitAuthor) {
-    logger.trace('Setting gitAuthor');
-    author = {
-      name: gitAuthor.name,
-      email: gitAuthor.address,
-      date: now.format(),
-    };
-  }
-  const body = {
-    message,
-    parents: [parent],
-    tree,
-  };
-  if (author) {
-    body.author = author;
-    // istanbul ignore if
-    if (gitPrivateKey) {
-      logger.debug('Found gitPrivateKey');
-      const privKeyObj = openpgp.key.readArmored(gitPrivateKey).keys[0];
-      const commit = `tree ${tree}\nparent ${parent}\nauthor ${author.name} <${
-        author.email
-      }> ${now.format('X ZZ')}\ncommitter ${author.name} <${
-        author.email
-      }> ${now.format('X ZZ')}\n\n${message}`;
-      const { signature } = await openpgp.sign({
-        data: openpgp.util.str2Uint8Array(commit),
-        privateKeys: privKeyObj,
-        detached: true,
-        armor: true,
-      });
-      body.signature = signature;
-    }
-  }
-  const options = {
-    body,
-  };
-  // istanbul ignore if
-  if (config.forkToken) {
-    options.token = config.forkToken;
-  }
-  return (await get.post(`repos/${config.repository}/git/commits`, options))
-    .body.sha;
-}
-
-async function getCommitMessages() {
-  logger.debug('getCommitMessages');
-  const res = await get(`repos/${config.repository}/commits`);
-  return res.body.map(commit => commit.commit.message);
-}
-
-function expandError(err) {
-  return {
-    err,
-    message: err.message,
-    body: err.response ? err.response.body : undefined,
-  };
-}
-
 async function getVulnerabilityAlerts() {
   const headers = {
     accept: 'application/vnd.github.vixen-preview+json',
diff --git a/lib/platform/github/storage.js b/lib/platform/github/storage.js
new file mode 100644
index 0000000000000000000000000000000000000000..ad9bf0baf3810d5a6a6f59af4c2265f6201217ba
--- /dev/null
+++ b/lib/platform/github/storage.js
@@ -0,0 +1,476 @@
+const moment = require('moment');
+const openpgp = require('openpgp');
+const path = require('path');
+const get = require('./gh-got-wrapper');
+const { expandError } = require('./util');
+
+class Storage {
+  constructor(config) {
+    // config
+    this.config = config;
+    this.gitAuthor = config.gitAuthor;
+    this.gitPrivateKey = config.gitPrivateKey;
+    this.forkToken = config.forkToken;
+    this.repository = config.repository;
+    this.baseBranch = config.baseBranch;
+    // cache
+    this.branchFiles = {};
+    this.branchList = null;
+  }
+
+  // Returns true if branch exists, otherwise false
+  async branchExists(branchName) {
+    if (!this.branchList) {
+      logger.debug('Retrieving branchList');
+      this.branchList = (await get(
+        `repos/${this.repository}/branches?per_page=100`,
+        {
+          paginate: true,
+        }
+      )).body.map(branch => branch.name);
+      logger.debug({ branchList: this.branchList }, 'Retrieved branchList');
+    }
+    const res = this.branchList.includes(branchName);
+    logger.debug(`branchExists(${branchName})=${res}`);
+    return res;
+  }
+
+  // Get full file list
+  async getFileList(branchName) {
+    const branch = branchName || this.baseBranch;
+    if (this.branchFiles[branch]) {
+      return this.branchFiles[branch];
+    }
+    try {
+      const res = await get(
+        `repos/${this.repository}/git/trees/${branch}?recursive=true`
+      );
+      if (res.body.truncated) {
+        logger.warn(
+          { repository: this.repository },
+          'repository tree is truncated'
+        );
+      }
+      const fileList = res.body.tree
+        .filter(item => item.type === 'blob' && item.mode !== '120000')
+        .map(item => item.path)
+        .sort();
+      logger.debug(`Retrieved fileList with length ${fileList.length}`);
+      this.branchFiles[branch] = fileList;
+      return fileList;
+    } catch (err) /* istanbul ignore next */ {
+      if (err.statusCode === 409) {
+        logger.debug('Repository is not initiated');
+        throw new Error('uninitiated');
+      }
+      logger.info(
+        { repository: this.repository },
+        'Error retrieving git tree - no files detected'
+      );
+      return [];
+    }
+  }
+
+  async getAllRenovateBranches(branchPrefix) {
+    logger.trace('getAllRenovateBranches');
+    try {
+      const allBranches = (await get(
+        `repos/${this.repository}/git/refs/heads/${branchPrefix}`,
+        {
+          paginate: true,
+        }
+      )).body;
+      return allBranches.reduce((arr, branch) => {
+        if (branch.ref.startsWith(`refs/heads/${branchPrefix}`)) {
+          arr.push(branch.ref.substring('refs/heads/'.length));
+        }
+        if (
+          branchPrefix.endsWith('/') &&
+          branch.ref === `refs/heads/${branchPrefix.slice(0, -1)}`
+        ) {
+          logger.warn(
+            `Pruning branch "${branchPrefix.slice(
+              0,
+              -1
+            )}" so that it does not block PRs`
+          );
+          arr.push(branch.ref.substring('refs/heads/'.length));
+        }
+        return arr;
+      }, []);
+    } catch (err) /* istanbul ignore next */ {
+      return [];
+    }
+  }
+
+  async isBranchStale(branchName) {
+    // Check if branch's parent SHA = master SHA
+    logger.debug(`isBranchStale(${branchName})`);
+    const branchCommit = await this.getBranchCommit(branchName);
+    logger.debug(`branchCommit=${branchCommit}`);
+    const commitDetails = await getCommitDetails(this, branchCommit);
+    logger.trace({ commitDetails }, `commitDetails`);
+    const parentSha = commitDetails.parents[0].sha;
+    logger.debug(`parentSha=${parentSha}`);
+    const baseCommitSHA = await this.getBranchCommit(this.baseBranch);
+    logger.debug(`baseCommitSHA=${baseCommitSHA}`);
+    // Return true if the SHAs don't match
+    return parentSha !== baseCommitSHA;
+  }
+
+  async deleteBranch(branchName) {
+    delete this.branchFiles[branchName];
+    const options = this.forkToken ? { token: this.forkToken } : undefined;
+    try {
+      await get.delete(
+        `repos/${this.repository}/git/refs/heads/${branchName}`,
+        options
+      );
+    } catch (err) /* istanbul ignore next */ {
+      if (err.message.startsWith('Reference does not exist')) {
+        logger.info({ branchName }, 'Branch to delete does not exist');
+      } else {
+        logger.warn(
+          { err, body: err.response.body, branchName },
+          'Error deleting branch'
+        );
+      }
+    }
+  }
+
+  async mergeBranch(branchName) {
+    logger.debug(`mergeBranch(${branchName})`);
+    const url = `repos/${this.repository}/git/refs/heads/${this.baseBranch}`;
+    const options = {
+      body: {
+        sha: await this.getBranchCommit(branchName),
+      },
+    };
+    try {
+      await get.patch(url, options);
+    } catch (err) {
+      logger.info(
+        expandError(err),
+        `Error pushing branch merge for ${branchName}`
+      );
+      throw new Error('Branch automerge failed');
+    }
+    // Delete branch
+    await this.deleteBranch(branchName);
+  }
+
+  async getBranchLastCommitTime(branchName) {
+    try {
+      const res = await get(
+        `repos/${this.repository}/commits?sha=${branchName}`
+      );
+      return new Date(res.body[0].commit.committer.date);
+    } catch (err) {
+      logger.error(expandError(err), `getBranchLastCommitTime error`);
+      return new Date();
+    }
+  }
+
+  // Generic File operations
+
+  async getFile(filePath, branchName) {
+    logger.trace(`getFile(filePath=${filePath}, branchName=${branchName})`);
+    const branchFiles = await this.getFileList(branchName);
+    if (!branchFiles.includes(filePath)) {
+      return null;
+    }
+    let res;
+    try {
+      res = await get(
+        `repos/${this.repository}/contents/${encodeURI(
+          filePath
+        )}?ref=${branchName || this.baseBranch}`
+      );
+    } catch (error) {
+      if (error.statusCode === 404) {
+        // If file not found, then return null JSON
+        logger.info({ filePath, branchName }, 'getFile 404');
+        return null;
+      }
+      if (
+        error.statusCode === 403 &&
+        error.message &&
+        error.message.startsWith('This API returns blobs up to 1 MB in size')
+      ) {
+        logger.info('Large file');
+        // istanbul ignore if
+        if (branchName && branchName !== this.baseBranch) {
+          logger.info('Cannot retrieve large files from non-master branch');
+          return null;
+        }
+        // istanbul ignore if
+        if (path.dirname(filePath) !== '.') {
+          logger.info('Cannot retrieve large files from non-root directories');
+          return null;
+        }
+        const treeUrl = `repos/${this.repository}/git/trees/${this.baseBranch}`;
+        const baseName = path.basename(filePath);
+        let fileSha;
+        (await get(treeUrl)).body.tree.forEach(file => {
+          if (file.path === baseName) {
+            fileSha = file.sha;
+          }
+        });
+        if (!fileSha) {
+          logger.warn('Could not locate file blob');
+          throw error;
+        }
+        res = await get(`repos/${this.repository}/git/blobs/${fileSha}`);
+      } else {
+        // Propagate if it's any other error
+        throw error;
+      }
+    }
+    if (res && res.body.content) {
+      return Buffer.from(res.body.content, 'base64').toString();
+    }
+    return null;
+  }
+
+  // Add a new commit, create branch if not existing
+  async commitFilesToBranch(
+    branchName,
+    files,
+    message,
+    parentBranch = this.baseBranch
+  ) {
+    logger.debug(
+      `commitFilesToBranch('${branchName}', files, message, '${parentBranch})'`
+    );
+    delete this.branchFiles[branchName];
+    const parentCommit = await this.getBranchCommit(parentBranch);
+    const parentTree = await getCommitTree(this, parentCommit);
+    const fileBlobs = [];
+    // Create blobs
+    for (const file of files) {
+      const blob = await createBlob(this, file.contents);
+      fileBlobs.push({
+        name: file.name,
+        blob,
+      });
+    }
+    // Create tree
+    const tree = await createTree(this, parentTree, fileBlobs);
+    const commit = await createCommit(this, parentCommit, tree, message);
+    const isBranchExisting = await this.branchExists(branchName);
+    try {
+      if (isBranchExisting) {
+        await updateBranch(this, branchName, commit);
+      } else {
+        await this.createBranch(branchName, commit);
+      }
+    } catch (err) /* istanbul ignore next */ {
+      logger.debug({
+        files: files.filter(
+          file =>
+            !file.name.endsWith('package-lock.json') &&
+            !file.name.endsWith('npm-shrinkwrap.json') &&
+            !file.name.endsWith('yarn.lock')
+        ),
+      });
+      throw err;
+    }
+  }
+
+  // Internal branch operations
+
+  // Creates a new branch with provided commit
+  async createBranch(branchName, sha) {
+    logger.debug(`createBranch(${branchName})`);
+    const options = {
+      body: {
+        ref: `refs/heads/${branchName}`,
+        sha,
+      },
+    };
+    // istanbul ignore if
+    if (this.forkToken) {
+      options.token = this.forkToken;
+    }
+    try {
+      // istanbul ignore if
+      if (branchName.includes('/')) {
+        const [blockingBranch] = branchName.split('/');
+        if (await this.branchExists(blockingBranch)) {
+          logger.warn({ blockingBranch }, 'Deleting blocking branch');
+          await this.deleteBranch(blockingBranch);
+        }
+      }
+      logger.debug({ options, branchName }, 'Creating branch');
+      await get.post(`repos/${this.repository}/git/refs`, options);
+      this.branchList.push(branchName);
+      logger.debug('Created branch');
+    } catch (err) /* istanbul ignore next */ {
+      const headers = err.response.req.getHeaders();
+      delete headers.token;
+      logger.warn(
+        {
+          err,
+          message: err.message,
+          responseBody: err.response.body,
+          headers,
+          options,
+        },
+        'Error creating branch'
+      );
+      if (err.statusCode === 422) {
+        throw new Error('repository-changed');
+      }
+      throw err;
+    }
+  }
+
+  // Return the commit SHA for a branch
+  async getBranchCommit(branchName) {
+    const res = await get(
+      `repos/${this.repository}/git/refs/heads/${branchName}`
+    );
+    return res.body.object.sha;
+  }
+
+  async getCommitMessages() {
+    logger.debug('getCommitMessages');
+    const res = await get(`repos/${this.repository}/commits`);
+    return res.body.map(commit => commit.commit.message);
+  }
+}
+
+// Internal: Updates an existing branch to new commit sha
+async function updateBranch(self, branchName, commit) {
+  logger.debug(`Updating branch ${branchName} with commit ${commit}`);
+  const options = {
+    body: {
+      sha: commit,
+      force: true,
+    },
+  };
+  // istanbul ignore if
+  if (self.forkToken) {
+    options.token = self.forkToken;
+  }
+  try {
+    await get.patch(
+      `repos/${self.repository}/git/refs/heads/${branchName}`,
+      options
+    );
+  } catch (err) /* istanbul ignore next */ {
+    if (err.statusCode === 422) {
+      logger.info(expandError(err), 'Branch no longer exists - exiting');
+      throw new Error('repository-changed');
+    }
+    throw err;
+  }
+}
+// Low-level commit operations
+
+// Create a blob with fileContents and return sha
+async function createBlob(self, fileContents) {
+  logger.debug('Creating blob');
+  const options = {
+    body: {
+      encoding: 'base64',
+      content: Buffer.from(fileContents).toString('base64'),
+    },
+  };
+  // istanbul ignore if
+  if (self.forkToken) {
+    options.token = self.forkToken;
+  }
+  return (await get.post(`repos/${self.repository}/git/blobs`, options)).body
+    .sha;
+}
+
+// Return the tree SHA for a commit
+async function getCommitTree(self, commit) {
+  logger.debug(`getCommitTree(${commit})`);
+  return (await get(`repos/${self.repository}/git/commits/${commit}`)).body.tree
+    .sha;
+}
+
+// Create a tree and return SHA
+async function createTree(self, baseTree, files) {
+  logger.debug(`createTree(${baseTree}, files)`);
+  const body = {
+    base_tree: baseTree,
+    tree: [],
+  };
+  files.forEach(file => {
+    body.tree.push({
+      path: file.name,
+      mode: '100644',
+      type: 'blob',
+      sha: file.blob,
+    });
+  });
+  logger.trace({ body }, 'createTree body');
+  const options = { body };
+  // istanbul ignore if
+  if (self.forkToken) {
+    options.token = self.forkToken;
+  }
+  return (await get.post(`repos/${self.repository}/git/trees`, options)).body
+    .sha;
+}
+
+// Create a commit and return commit SHA
+async function createCommit(self, parent, tree, message) {
+  logger.debug(`createCommit(${parent}, ${tree}, ${message})`);
+  const { gitAuthor, gitPrivateKey } = self;
+  const now = moment();
+  let author;
+  if (gitAuthor) {
+    logger.trace('Setting gitAuthor');
+    author = {
+      name: gitAuthor.name,
+      email: gitAuthor.address,
+      date: now.format(),
+    };
+  }
+  const body = {
+    message,
+    parents: [parent],
+    tree,
+  };
+  if (author) {
+    body.author = author;
+    // istanbul ignore if
+    if (gitPrivateKey) {
+      logger.debug('Found gitPrivateKey');
+      const privKeyObj = openpgp.key.readArmored(gitPrivateKey).keys[0];
+      const commit = `tree ${tree}\nparent ${parent}\nauthor ${author.name} <${
+        author.email
+      }> ${now.format('X ZZ')}\ncommitter ${author.name} <${
+        author.email
+      }> ${now.format('X ZZ')}\n\n${message}`;
+      const { signature } = await openpgp.sign({
+        data: openpgp.util.str2Uint8Array(commit),
+        privateKeys: privKeyObj,
+        detached: true,
+        armor: true,
+      });
+      body.signature = signature;
+    }
+  }
+  const options = {
+    body,
+  };
+  // istanbul ignore if
+  if (self.forkToken) {
+    options.token = self.forkToken;
+  }
+  return (await get.post(`repos/${self.repository}/git/commits`, options)).body
+    .sha;
+}
+
+async function getCommitDetails(self, commit) {
+  logger.debug(`getCommitDetails(${commit})`);
+  const results = await get(`repos/${self.repository}/git/commits/${commit}`);
+  return results.body;
+}
+
+module.exports = Storage;
diff --git a/lib/platform/github/util.js b/lib/platform/github/util.js
new file mode 100644
index 0000000000000000000000000000000000000000..71aa77f5078295b8c9f129ca8a7bd77b6dfe4612
--- /dev/null
+++ b/lib/platform/github/util.js
@@ -0,0 +1,11 @@
+module.exports = {
+  expandError,
+};
+
+function expandError(err) {
+  return {
+    err,
+    message: err.message,
+    body: err.response ? err.response.body : undefined,
+  };
+}
diff --git a/test/platform/github/index.spec.js b/test/platform/github/index.spec.js
index 5cbe68faaa52e697b286b7f58c1d60caa34f6c9c..34893f2556197a564423f65064dbbb9deda2dba2 100644
--- a/test/platform/github/index.spec.js
+++ b/test/platform/github/index.spec.js
@@ -444,11 +444,17 @@ describe('platform/github', () => {
     });
   });
   describe('getFileList', () => {
+    beforeEach(async () => {
+      await initRepo({
+        repository: 'some/repo',
+        token: 'token',
+      });
+    });
     it('returns empty array if error', async () => {
       get.mockImplementationOnce(() => {
         throw new Error('some error');
       });
-      const files = await github.getFileList();
+      const files = await github.getFileList('error-branch');
       expect(files).toEqual([]);
     });
     it('warns if truncated result', async () => {
@@ -458,7 +464,7 @@ describe('platform/github', () => {
           tree: [],
         },
       }));
-      const files = await github.getFileList();
+      const files = await github.getFileList('truncated-branch');
       expect(files.length).toBe(0);
     });
     it('caches the result', async () => {
@@ -468,9 +474,9 @@ describe('platform/github', () => {
           tree: [],
         },
       }));
-      let files = await github.getFileList();
+      let files = await github.getFileList('cached-branch');
       expect(files.length).toBe(0);
-      files = await github.getFileList();
+      files = await github.getFileList('cached-branch');
       expect(files.length).toBe(0);
     });
     it('should return the files matching the fileName', async () => {
@@ -488,7 +494,7 @@ describe('platform/github', () => {
           ],
         },
       }));
-      const files = await github.getFileList();
+      const files = await github.getFileList('npm-branch');
       expect(files).toMatchSnapshot();
     });
   });
@@ -1835,6 +1841,11 @@ describe('platform/github', () => {
   });
   describe('getCommitMessages()', () => {
     it('returns commits messages', async () => {
+      await initRepo({
+        repository: 'some/repo',
+        token: 'token',
+        gitAuthor: 'Renovate Bot <bot@renovatebot.com>',
+      });
       get.mockReturnValueOnce({
         body: [
           {