mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-24 21:46:25 +00:00
a08902fefc
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.
321 lines
8.3 KiB
JavaScript
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);
|
|
}
|