@interaktiv/mibuilder-core
Version:
Core libraries to interact with MiBuilder projects.
346 lines (292 loc) • 11.5 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.isGitRepository = isGitRepository;
exports.clearRepository = clearRepository;
exports.syncRemotes = syncRemotes;
exports.cloneOrUpdateRepository = cloneOrUpdateRepository;
exports.updateRepository = updateRepository;
exports.getTags = getTags;
exports.getLastCommitHash = getLastCommitHash;
var _functional = require("@interaktiv/functional");
var _types = require("@interaktiv/types");
var _execa = _interopRequireDefault(require("execa"));
var _nodegit = _interopRequireDefault(require("nodegit"));
var _mibuilderError = require("./mibuilder-error");
var _ux = require("./ux");
const REFERENCE_NOT_FOUND_ERR_NO = -3;
let cliUxInternal;
/**
* lift2 (apply) for Functions.
*
* @see http://www.tomharding.me/2017/04/15/functions-as-functors/
* @see https://ramdajs.com/docs/#converge
*
* Type signature:
* (a -> b -> c) -> (x -> a) -> (x -> b) -> (x -> c)
*
* @param {Function} f A function that takes two parameters of type a and b, and returns a value of type c.
* @param {Function} g A function that takes one paramter of type x.
* @param {Function} h A function that takes one parameter of type x.
* @return {*} Returns a function that computes f(g(x), h(x))
*/
const lift2 = (0, _functional.curry)(function (f, g, h, x) {
// eslint-disable-next-line no-warning-comments
// FIXME: Cause how we use lift2 in this module the implementation here is wrong ! We need to made the provided functions to use `curry()` by @interaktiv/functional, after that we can use the correct implementation that is commented out below this line
// return f(g(x), h(x));
return f(g(x))(h(x));
});
const getCliUx = async function () {
if (cliUxInternal) return Promise.resolve(cliUxInternal);
cliUxInternal = await _ux.UX.create('git-utils');
return cliUxInternal;
};
const getRemoteActionOpts = (opts = {}) => ({
fetchOpts: {
callbacks: {
certificateCheck: () => 1,
credentials: (_url, userName) => _nodegit.default.Cred.sshKeyNew(userName, opts.sshPublicKeyPath, opts.sshPrivateKeyPath, opts.sshPrivateKeyPassword)
}
}
});
/**
* Separate repo url / path / branch from url of the form
* https://server/org/repo/path#branch or https://server/org/repo#branch or
* https://server/org/repo
*
* @param {String} fullUrl Full repo url
* @return {Object} map with repo / path /branch {repo:https://server/org/repo, branch:branch, path:path}
*/
const separateRepoUrlPathCommitish = fullUrl => {
const parts = fullUrl.split('#');
const [repoWithPath, commitish = 'master'] = parts;
const repo = repoWithPath.split('/').splice(0, 5).join('/');
const path = repoWithPath.split('/').splice(5).join('/');
return {
repo,
commitish,
path
};
};
const getObjectType = obj => obj.type();
const getObjectId = obj => obj.id();
const getRepoCommitForObjectWithId = repo => type => async id => {
if (type === _nodegit.default.Object.TYPE.COMMIT) {
return _nodegit.default.Commit.lookup(repo, id);
}
if (type === _nodegit.default.Object.TYPE.TAG) {
const tag = await _nodegit.default.Tag.lookup(repo, id);
return _nodegit.default.Commit.lookup(repo, tag.targetId());
}
if (type === _nodegit.default.Object.TYPE.TREE) {
const branch = await _nodegit.default.Branch.lookup(repo, id);
return repo.getBranchCommit(branch);
}
return undefined;
};
const getCommitForCommitish = repo => async commitish => {
(0, _types.ensureObject)(repo, 'Repo is not a valid repository reference');
(0, _types.ensureString)(commitish, 'Provide a valid commitish string');
const cliUx = await getCliUx();
cliUx.debug(`Fetching reference for ${commitish}`);
let doBranchCheck = false;
let gitObject, commit;
try {
gitObject = await _nodegit.default.Revparse.single(repo, commitish);
} catch (err) {
if (err.errno !== REFERENCE_NOT_FOUND_ERR_NO) throw err;
doBranchCheck = true;
}
const getCommitForObjectWithId = getRepoCommitForObjectWithId(repo);
const getCommitForObject = lift2(getCommitForObjectWithId, getObjectType, getObjectId);
if (doBranchCheck) {
const doBranchPull = await createBranchInRepoIfNeeded(repo)(commitish);
if (doBranchPull) {
const branchOrigin = `origin/${commitish}`;
cliUx.debug(`Checking out branch: ${commitish}`);
await repo.checkoutBranch(commitish, {
checkoutStrategy: _nodegit.default.Checkout.STRATEGY.FORCE
});
cliUx.debug(`Merging branches: ${branchOrigin} and ${commitish}`);
await repo.mergeBranches(commitish, branchOrigin);
}
gitObject = await _nodegit.default.Revparse.single(repo, commitish);
}
if (gitObject) commit = await getCommitForObject(gitObject);
(0, _types.ensureObject)(commit, 'Expected commit to be an object');
cliUx.debug(`Commit ${commit} for ${commitish}`);
return commit;
};
const createBranchInRepoIfNeeded = repo => async branch => {
(0, _types.ensureObject)(repo, 'Repo is not a valid repository reference');
if (branch == null) return false;
const cliUx = await getCliUx();
let createBranch = false;
try {
cliUx.debug(`Checking if branch ${branch} is locally available`);
await repo.getBranch(branch);
cliUx.debug(`Branch locally available`);
} catch (err) {
if (err.errno !== REFERENCE_NOT_FOUND_ERR_NO) throw err;
cliUx.debug(`Branch ${branch} not found`);
createBranch = true;
}
if (createBranch) {
cliUx.debug(`Creating ${branch}`);
const origin = `origin/${branch}`;
cliUx.debug(`Get branch commit for "${origin}"`);
const branchCommit = await repo.getBranchCommit(origin);
cliUx.debug(`Create branch ${branch} at commit ${branchCommit}`);
await repo.createBranch(branch, branchCommit);
}
return createBranch;
};
const checkoutCommitWithStrategyInRepo = strategy => repo => {
(0, _types.ensureObject)(repo);
return async commit => {
(0, _types.ensureObject)(commit);
await _nodegit.default.Checkout.tree(repo, commit, {
checkoutStrategy: strategy
});
await repo.setHeadDetached(commit, repo.defaultSignature, `Checkout: HEAD ${commit.id()}`);
};
};
const forceCheckoutCommitInRepo = checkoutCommitWithStrategyInRepo(_nodegit.default.Checkout.STRATEGY.FORCE);
const getRepo = async repository => {
if ((0, _types.isString)(repository) === false) return repository;
if ((await isGitRepository(repository)) === false) {
throw new _mibuilderError.MiBuilderError(`Der Pfad '${repository}' ist kein git Repository`, 'InvalidGitRespository');
}
return _nodegit.default.Repository.open(repository);
};
async function isGitRepository(cwd) {
let result;
try {
result = await (0, _execa.default)('git', ['rev-parse', '--is-inside-work-tree'], {
stdio: 'ignore',
cwd
});
} catch (err) {
return false;
}
return result.exitCode === 0;
}
/**
* Reverts local changes, my delete files that cannot be recovered.
* Files and directories in .gitignore will be preserved.
*
* @param {String} cwd The directory containg the .git dir
* @return {Promise<void>} A promise that resolves if the process is done
*/
async function clearRepository(cwd) {
(await getCliUx()).debug(`Soft-Clear repository at ${cwd}`); // Use the `-n` option to show what changes would be done without actually
// trigger the action
// Add the `-x` to include .gitignore files and dirs too
// Files in .gitignore will be preserved
await (0, _execa.default)('git', ['clean', '-dfq'], {
stdio: 'ignore',
cwd
});
return true;
}
async function syncRemotes(repository, credentials) {
const repo = await getRepo(repository);
return repo.fetchAll(getRemoteActionOpts(credentials).fetchOpts);
}
/**
* Synchronizes the local repository, clones it from remote if needed or just
* updates it.
*
* @param {Object} opts={} The options for the repo checkout
* @param {String} opts.commitishOverride An override for the commitish part of the remoteUrl
* @param {String} opts.cwd The working dir
* @param {String} opts.remoteUrl The git ssh url
* @param {Object} opts.credentials={} The credentials for the ssh connection
* @param {Object} opts.credentials.sshPublicKeyPath The Path to the public ssh key
* @param {Object} opts.credentials.sshPrivateKeyPath The path to the private ssh key
* @param {Object} opts.credentials.sshPrivateKeyPassword The password for the private ssh key
* @return {Promise<void>} A promise that resolves if the process is done
*/
async function cloneOrUpdateRepository({
commitishOverride,
cwd,
remoteUrl,
credentials = {}
} = {}) {
(0, _types.ensureString)(remoteUrl, 'Provide a valide remote url string for template repository');
let isInGitRepository, repo;
try {
isInGitRepository = await isGitRepository(cwd);
} catch (err) {
isInGitRepository = false;
}
const cliUx = await getCliUx();
cliUx.debug(`Is git repository? ${isInGitRepository ? 'YES' : 'NO'}`);
cliUx.debug(`Private ssh key path: ${credentials.sshPrivateKeyPath}`);
cliUx.debug(`Public ssh key path: ${credentials.sshPublicKeyPath}`);
const parseRepoUri = separateRepoUrlPathCommitish(remoteUrl);
const {
repo: repoUrl
} = parseRepoUri;
const commitish = commitishOverride || parseRepoUri.commitish;
if (isInGitRepository === false) {
cliUx.debug(`Cloning repository from ${repoUrl}`);
repo = await _nodegit.default.Clone.clone(repoUrl, cwd, getRemoteActionOpts(credentials) || {});
}
cliUx.debug(`Updating repository in ${cwd}`);
return updateRepository({
cwd,
commitish,
credentials,
repo
});
}
/**
* Updates the local repository
*
* @param {Object} opts={} The options for the repo checkout
* @param {String} opts.cwd The working dir
* @param {Object} [opts.repo] The repository object from NodeGit
* @param {String} [opts.commitish='master'] The commitish to checkout
* @param {Object} opts.credentials={} The credentials for the ssh connection
* @param {Object} opts.credentials.sshPublicKeyPath The Path to the public ssh key
* @param {Object} opts.credentials.sshPrivateKeyPath The path to the private ssh key
* @param {Object} opts.credentials.sshPrivateKeyPassword The password for the private ssh key
* @return {Promise<void>} A promise that resolves if the process is done
*/
async function updateRepository({
cwd,
commitish = 'master',
credentials = {},
repo
} = {}) {
const cliUx = await getCliUx();
const repository = await getRepo(repo || cwd);
cliUx.debug('Fetching from all remotes');
await syncRemotes(repository, credentials);
cliUx.debug(`Checkout commitish ${commitish}`);
const checkoutCommit = await getCommitForCommitish(repository)(commitish);
await forceCheckoutCommitInRepo(repository)(checkoutCommit);
return repo;
}
async function getTags({
cwd,
credentials
} = {}) {
const repo = await getRepo(cwd);
await syncRemotes(repo, credentials);
return _nodegit.default.Tag.list(repo);
}
async function getLastCommitHash(cwd = process.cwd(), ref = 'HEAD', short = true) {
(0, _types.ensureString)(ref, 'Reference to fetch commit sha is not given');
try {
const result = await (0, _execa.default)('git', ['rev-parse', short ? '--short' : null, ref].filter(Boolean), {
cwd
});
return result.exitCode === 0 ? result.stdout : null;
} catch (err) {
return null;
}
}