renovate/lib/api/github.js
Rhys Arkins a08902fefc Refactor branch creation & update (#74)
This commit refactors the way we create commits and branches to use the low-level GitHub "git data" API endpoints. in particular, it commits the file first and creates the branch second, rather than vice versa. This prevents unnecessary builds in CI due to the (empty) branch creation we had in previous versions. Closes #82.
2017-01-29 21:25:12 +01:00

321 lines
8.3 KiB
JavaScript

const logger = require('winston');
const ghGot = require('gh-got');
const config = {};
module.exports = {
initRepo,
getRateLimit,
// Search
findFilePaths,
// Branch
branchExists,
createBranch,
deleteBranch,
getBranchPr,
updateBranch,
// issue
addAssignees,
addLabels,
// PR
checkForClosedPr,
createPr,
getPr,
updatePr,
// file
commitFile,
getFile,
getFileContent,
getFileJson,
writeFile,
};
// Initialize GitHub by getting base branch and SHA
function initRepo(repoName) {
logger.debug(`initRepo(${repoName})`);
config.repoName = repoName;
return ghGot(`repos/${repoName}`)
.then((res) => {
config.owner = res.body.owner.login;
logger.debug(`${repoName} owner = ${config.owner}`);
config.defaultBranch = res.body.default_branch;
logger.debug(`${repoName} default branch = ${config.defaultBranch}`);
return config.defaultBranch;
})
.then(getBranchCommit)
.then((commit) => {
config.baseCommitSHA = commit;
return commit;
})
.then(getCommitTree)
.then((tree) => {
config.baseTreeSHA = tree;
return Promise.resolve();
})
.catch((err) => {
logger.error(`GitHub init error: ${JSON.stringify(err)}`);
throw err;
});
}
// Get rate limit
function getRateLimit() {
return ghGot('rate_limit')
.then(res => res.body);
}
// Search
// Returns an array of file paths in current repo matching the fileName
function findFilePaths(fileName) {
return ghGot(`search/code?q=repo:${config.repoName}+filename:${fileName}`)
.then((res) => {
const exactMatches = res.body.items.filter(item => item.name === fileName);
const filePaths = exactMatches.map(item => item.path);
return filePaths;
});
}
// Branch
// Returns true if branch exists, otherwise false
function branchExists(branchName) {
logger.debug(`Checking if branch exists: ${branchName}`);
return ghGot(`repos/${config.repoName}/git/refs/heads/${branchName}`)
.then((res) => {
if (res.statusCode === 200) {
logger.debug('Branch exists');
return true;
}
// This probably shouldn't happen
logger.debug('Branch doesn\'t exist');
return false;
})
.catch((error) => {
if (error.statusCode === 404) {
// If file not found, then return false
logger.debug('Branch doesn\'t exist');
return false;
}
// Propagate if it's any other error
throw error;
});
}
// Creates a new branch with provided commit
// If commit not present then defaults to branch off master
function createBranch(branchName, commit = config.baseCommitSHA) {
return ghGot.post(`repos/${config.repoName}/git/refs`, {
body: {
ref: `refs/heads/${branchName}`,
sha: commit,
},
});
}
function deleteBranch(branchName) {
return ghGot.delete(`repos/${config.repoName}/git/refs/heads/${branchName}`);
}
// Updates an existing branch to new commit sha
function updateBranch(branchName, commit) {
logger.debug(`Updating branch ${branchName} with commit ${commit}`);
return ghGot.patch(`repos/${config.repoName}/git/refs/heads/${branchName}`, {
body: {
sha: commit,
force: true,
},
});
}
// Returns the Pull Request number for a branch. Null if not exists.
function getBranchPr(branchName) {
const gotString = `repos/${config.repoName}/pulls?` +
`state=open&base=${config.defaultBranch}&head=${config.owner}:${branchName}`;
return ghGot(gotString).then((res) => {
if (res.body.length) {
return res.body[0];
}
return null;
});
}
// Issue
function addAssignees(issueNo, assignees) {
logger.debug(`Adding assignees ${assignees} to #${issueNo}`);
return ghGot.post(`repos/${config.repoName}/issues/${issueNo}/assignees`, {
body: {
assignees,
},
})
.catch((error) => {
logger.error(JSON.stringify(error));
throw error;
});
}
function addLabels(issueNo, labels) {
logger.debug(`Adding labels ${labels} to #${issueNo}`);
return ghGot.post(`repos/${config.repoName}/issues/${issueNo}/labels`, {
body: JSON.stringify(labels),
})
.catch((error) => {
logger.error(JSON.stringify(error));
throw error;
});
}
// Pull Request
function checkForClosedPr(branchName, prTitle) {
return ghGot(
`repos/${config.repoName}/pulls?state=closed&head=${config.owner}:${branchName}`)
.then(res =>
res.body.some(pr => pr.title === prTitle && pr.head.label === `${config.owner}:${branchName}`))
.catch((error) => {
logger.error(`Error checking if PR already existed: ${error}`);
});
}
function createPr(branchName, title, body) {
return ghGot
.post(`repos/${config.repoName}/pulls`, {
body: { title, head: branchName, base: config.defaultBranch, body },
})
.then(res => res.body);
}
function getPr(prNo) {
return ghGot(`repos/${config.repoName}/pulls/${prNo}`)
.then(res => res.body);
}
function updatePr(prNo, title, body) {
return ghGot.patch(`repos/${config.repoName}/pulls/${prNo}`, {
body: { title, body },
});
}
// Generic File operations
function getFile(filePath, branchName = config.defaultBranch) {
return ghGot(`repos/${config.repoName}/contents/${filePath}?ref=${branchName}`);
}
function getFileContent(filePath, branchName = config.baseBranch) {
return getFile(filePath, branchName)
.then(res => new Buffer(res.body.content, 'base64').toString())
.catch((error) => {
if (error.statusCode === 404) {
// If file not found, then return null JSON
return null;
}
// Propagate if it's any other error
throw error;
});
}
function getFileJson(filePath, branchName = config.baseBranch) {
return getFile(filePath, branchName)
.then(res => JSON.parse(new Buffer(res.body.content, 'base64').toString()))
.catch((error) => {
if (error.statusCode === 404) {
// If file not found, then return null JSON
return null;
}
// Propagate if it's any other error
throw error;
});
}
function writeFile(branchName, oldFileSHA, filePath, fileContents, message) {
return ghGot.put(`repos/${config.repoName}/contents/${filePath}`, {
body: {
branch: branchName,
sha: oldFileSHA,
message,
content: new Buffer(fileContents).toString('base64'),
},
});
}
// Add a new commit, return SHA
function commitFile(fileName, fileContents, message, parentBranch = config.defaultBranch) {
logger.debug(`commitFile(${fileName}, fileContents, message, ${parentBranch})`);
let parentCommit = null;
let parentTree = null;
return getBranchCommit(parentBranch)
.then((commit) => {
parentCommit = commit;
return Promise.resolve();
})
.then(() => getCommitTree(parentCommit))
.then((tree) => {
parentTree = tree;
return Promise.resolve();
})
.then(() => createBlob(fileContents))
.then(blob => createTree(parentTree, fileName, blob))
.then(tree => createCommit(parentCommit, tree, message));
}
// Low-level commit operations
// Create a blob with fileContents and return sha
function createBlob(fileContents) {
logger.debug('Creating blob');
return ghGot.post(`repos/${config.repoName}/git/blobs`, {
body: {
encoding: 'base64',
content: new Buffer(fileContents).toString('base64'),
},
}).then(res => res.body.sha);
}
// Return the commit SHA for a branch
function getBranchCommit(branchName) {
return ghGot(`repos/${config.repoName}/git/refs/heads/${branchName}`)
.then((res) => {
const commit = res.body.object.sha;
logger.debug(`${branchName} commit = ${commit}`);
return commit;
});
}
// Return the tree SHA for a commit
function getCommitTree(commit) {
logger.debug(`getCommitTree(${commit})`);
return ghGot(`repos/${config.repoName}/git/commits/${commit}`)
.then(res => res.body.tree.sha);
}
// Create a tree and return SHA
function createTree(baseTree, filePath, fileBlob) {
logger.debug(`createTree(${baseTree}, ${filePath}, ${fileBlob})`);
return ghGot.post(`repos/${config.repoName}/git/trees`, {
body: {
base_tree: baseTree,
tree: [
{
path: filePath,
mode: '100644',
type: 'blob',
sha: fileBlob,
},
],
},
}).then(res => res.body.sha);
}
// Create a commit and return commit SHA
function createCommit(parent, tree, message) {
logger.debug(`createCommit(${parent}, ${tree}, ${message})`);
return ghGot.post(`repos/${config.repoName}/git/commits`, {
body: {
message,
parents: [parent],
tree,
},
}).then(res => res.body.sha);
}