renovate/lib/platform/git/storage.js
2019-03-08 11:49:47 +01:00

327 lines
9.3 KiB
JavaScript

const fs = require('fs-extra');
const { join } = require('path');
const path = require('path');
const URL = require('url');
const Git = require('simple-git/promise');
const convertHrtime = require('convert-hrtime');
class Storage {
constructor() {
let config = {};
let git = null;
let cwd = null;
Object.assign(this, {
initRepo,
cleanRepo,
getRepoStatus,
setBaseBranch,
branchExists,
commitFilesToBranch,
createBranch,
deleteBranch,
getAllRenovateBranches,
getBranchCommit,
getBranchLastCommitTime,
getCommitMessages,
getFile,
getFileList,
isBranchStale,
mergeBranch,
});
// istanbul ignore next
async function resetToBranch(branchName) {
await git.raw(['reset', '--hard']);
await git.checkout(branchName);
await git.raw(['reset', '--hard', 'origin/' + branchName]);
}
// istanbul ignore next
async function cleanLocalBranches() {
const existingBranches = (await git.raw(['branch']))
.split('\n')
.map(branch => branch.trim())
.filter(branch => branch.length)
.filter(branch => !branch.startsWith('* '));
logger.debug({ existingBranches });
for (const branchName of existingBranches) {
await deleteLocalBranch(branchName);
}
}
async function initRepo(args) {
cleanRepo();
config = { ...args };
cwd = config.localDir;
logger.info('Initialising git repository into ' + cwd);
const gitHead = path.join(cwd, '.git/HEAD');
let clone = true;
async function determineBaseBranch() {
// see https://stackoverflow.com/a/44750379/1438522
try {
config.baseBranch =
config.baseBranch ||
(await git.raw(['symbolic-ref', 'refs/remotes/origin/HEAD']))
.replace('refs/remotes/origin/', '')
.trim();
} catch (err) /* istanbul ignore next */ {
if (
err.message.startsWith(
'fatal: ref refs/remotes/origin/HEAD is not a symbolic ref'
)
) {
throw new Error('empty');
}
throw err;
}
}
// istanbul ignore if
if (process.env.NODE_ENV !== 'test' && (await fs.exists(gitHead))) {
try {
git = Git(cwd).silent(true);
await git.raw(['remote', 'set-url', 'origin', config.url]);
const fetchStart = process.hrtime();
await git.fetch(config.url, ['--depth=2', '--no-single-branch']);
await determineBaseBranch();
await resetToBranch(config.baseBranch);
await cleanLocalBranches();
await git.raw(['remote', 'prune', 'origin']);
const fetchSeconds =
Math.round(
1 + 10 * convertHrtime(process.hrtime(fetchStart)).seconds
) / 10;
logger.info({ fetchSeconds }, 'git fetch completed');
clone = false;
} catch (err) {
logger.error({ err }, 'git fetch error');
}
}
if (clone) {
await fs.emptyDir(cwd);
git = Git(cwd).silent(true);
const cloneStart = process.hrtime();
try {
await git.clone(config.url, '.', ['--depth=2', '--no-single-branch']);
} catch (err) /* istanbul ignore next */ {
logger.debug({ err }, 'git clone error');
throw new Error('platform-failure');
}
const cloneSeconds =
Math.round(
1 + 10 * convertHrtime(process.hrtime(cloneStart)).seconds
) / 10;
logger.info({ cloneSeconds }, 'git clone completed');
}
if (config.gitAuthor) {
await git.raw(['config', 'user.name', config.gitAuthor.name]);
await git.raw(['config', 'user.email', config.gitAuthor.address]);
// not supported yet
await git.raw(['config', 'commit.gpgsign', 'false']);
}
await determineBaseBranch();
}
// istanbul ignore next
function getRepoStatus() {
return git.status();
}
async function createBranch(branchName, sha) {
await git.reset('hard');
await git.checkout(['-B', branchName, sha]);
await git.push(['origin', branchName, '--force']);
}
// Return the commit SHA for a branch
async function getBranchCommit(branchName) {
const res = await git.revparse(['origin/' + branchName]);
return res.trim();
}
async function getCommitMessages() {
logger.debug('getCommitMessages');
const res = await git.log({
n: 10,
format: { message: '%s' },
});
return res.all.map(commit => commit.message);
}
async function setBaseBranch(branchName) {
if (branchName) {
logger.debug(`Setting baseBranch to ${branchName}`);
config.baseBranch = branchName;
if (branchName !== 'master') {
config.baseBranchSha = (await git.raw([
'rev-parse',
'origin/' + branchName,
])).trim();
}
}
}
async function getFileList(branchName) {
const branch = branchName || config.baseBranch;
const exists = await branchExists(branch);
if (!exists) {
return [];
}
const files = await git.raw([
'ls-tree',
'-r',
'--name-only',
'origin/' + branch,
]);
// istanbul ignore if
if (!files) {
return [];
}
return files.split('\n').filter(Boolean);
}
async function branchExists(branchName) {
try {
await git.raw(['show-branch', 'origin/' + branchName]);
return true;
} catch (ex) {
return false;
}
}
async function getAllRenovateBranches(branchPrefix) {
const branches = await git.branch(['--remotes', '--verbose']);
return branches.all
.map(localName)
.filter(branchName => branchName.startsWith(branchPrefix));
}
async function isBranchStale(branchName) {
const branches = await git.branch([
'--remotes',
'--verbose',
'--contains',
config.baseBranchSha || config.baseBranch,
]);
return !branches.all.map(localName).includes(branchName);
}
async function deleteLocalBranch(branchName) {
await git.branch(['-D', branchName]);
}
async function deleteBranch(branchName) {
try {
await git.raw(['push', '--delete', 'origin', branchName]);
logger.debug({ branchName }, 'Deleted remote branch');
} catch (err) /* istanbul ignore next */ {
logger.info({ branchName, err }, 'Error deleting remote branch');
throw new Error('repository-changed');
}
try {
await deleteLocalBranch(branchName);
// istanbul ignore next
logger.debug({ branchName }, 'Deleted local branch');
} catch (err) {
logger.debug({ branchName }, 'No local branch to delete');
}
}
async function mergeBranch(branchName) {
await git.reset('hard');
await git.checkout(['-B', branchName, 'origin/' + branchName]);
await git.checkout(config.baseBranch);
await git.merge([branchName]);
await git.push('origin', config.baseBranch);
}
async function getBranchLastCommitTime(branchName) {
try {
const time = await git.show([
'-s',
'--format=%ai',
'origin/' + branchName,
]);
return new Date(Date.parse(time));
} catch (ex) {
return new Date();
}
}
async function getFile(filePath, branchName) {
if (branchName) {
const exists = await branchExists(branchName);
if (!exists) {
logger.info({ branchName }, 'branch no longer exists - aborting');
throw new Error('repository-changed');
}
}
try {
const content = await git.show([
'origin/' + (branchName || config.baseBranch) + ':' + filePath,
]);
return content;
} catch (ex) {
return null;
}
}
async function commitFilesToBranch(
branchName,
files,
message,
parentBranch = config.baseBranch
) {
try {
await git.reset('hard');
await git.raw(['clean', '-fd']);
await git.checkout(['-B', branchName, 'origin/' + parentBranch]);
for (const file of files) {
await fs.writeFile(join(cwd, file.name), Buffer.from(file.contents));
}
await git.add(files.map(f => f.name));
await git.commit(message);
await git.push([
'origin',
`${branchName}:${branchName}`,
'--force',
'-u',
]);
} catch (err) /* istanbul ignore next */ {
logger.debug({ err }, 'Error commiting files');
if (err.message.includes('[remote rejected]')) {
throw new Error('repository-changed');
}
throw err;
}
}
function cleanRepo() {}
}
}
function localName(branchName) {
return branchName.replace(/^origin\//, '');
}
Storage.getUrl = ({ gitFs, auth, hostname, host, repository }) => {
let protocol = gitFs || 'https';
// istanbul ignore if
if (protocol.toString() === 'true') {
protocol = 'https';
}
if (protocol === 'ssh') {
return `git@${hostname}:${repository}.git`;
}
return URL.format({
protocol,
auth,
hostname,
host,
pathname: repository + '.git',
});
};
module.exports = Storage;