UNPKG

@interaktiv/mibuilder-core

Version:

Core libraries to interact with MiBuilder projects.

346 lines (292 loc) 11.5 kB
"use strict"; 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; } }