UNPKG

hologit

Version:

Hologit automates the projection of layered composite file trees based on flat, declarative plans

218 lines (188 loc) 6.94 kB
exports.command = 'project <holobranch>'; exports.desc = 'Projects holobranch named <holobranch> and outputs resulting tree hash'; exports.builder = { 'commit-to': { describe: 'A target branch/ref to commit the projected tree to', type: 'string' }, 'commit-message': { describe: 'A commit message to use if commit-branch is specified', type: 'string' }, 'commit-source-parent': { describe: 'Include the source commit as a second parent in the projection commit', type: 'boolean', default: true }, 'ref': { describe: 'Commit ref to read holobranch from', default: 'HEAD' }, 'working': { describe: 'Set to use the (possibly uncommited) contents of the working tree', type: 'boolean', default: false }, 'lens': { describe: 'Whether to apply lensing to the composite tree', type: 'boolean', default: null }, 'fetch': { describe: 'List of sources to fetch while projecting or * to fetch all', type: 'string' }, 'watch': { describe: 'Set to continously output updated output', type: 'boolean', default: false }, 'cache-to': { describe: 'Set a remote to push caches to', type: 'string' }, 'cache-from': { describe: 'Set a remote to pull caches from', type: 'string' } }; exports.handler = async function project ({ holobranch, ref = 'HEAD', lens = null, working = false, debug = false, fetch = null, watch = false, commitTo = null, commitMessage = null, commitSourceParent = true, cacheFrom = null, cacheTo = null }) { const logger = require('../lib/logger.js'); const { Repo, Projection, WatchLoop } = require('../lib'); // check inputs if (!holobranch) { throw new Error('holobranch required'); } // parse fetch list from options and env const fetchSplitRe = /[\s,:]+/; if (fetch || fetch === '') { if (fetch == '*' || !fetch) { fetch = true; } else if (!Array.isArray(fetch)) { fetch = fetch.split(fetchSplitRe); } } const fetchEnv = process.env.HOLO_FETCH; if (fetchEnv) { if (fetchEnv == '*' || fetch === true) { fetch = true; } else { (fetch || (fetch = [])).push(...fetchEnv.split(fetchSplitRe)); } } // parse cache from/to from env if (cacheFrom === false) { cacheFrom = null; } else if (!cacheFrom) { const cacheFromEnv = process.env.HOLO_CACHE_FROM; if (cacheFromEnv) { cacheFrom = cacheFromEnv; } else { cacheFrom = 'origin'; } } else if (typeof cacheFrom != 'string') { throw new Error(`cache-from must be a single string value`); } if (!cacheTo) { const cacheToEnv = process.env.HOLO_CACHE_TO; if (cacheToEnv) { cacheTo = cacheToEnv; } } else if (typeof cacheTo != 'string') { throw new Error(`cache-to must be a single string value`); } // load holorepo const repo = await Repo.getFromEnvironment({ ref, working }); const parentCommit = await repo.resolveRef(`${ref}^{commit}`); // load workspace const workspace = await repo.getWorkspace(); // examine holobranch const workspaceBranch = workspace.getBranch(holobranch); if (!await workspaceBranch.isDefined()) { throw new Error(`holobranch not defined: ${holobranch}`); } let outputHash = null; if (!watch) { outputHash = await Projection.projectBranch(workspaceBranch, { debug, lens, commitTo, commitMessage, parentCommit: commitSourceParent ? parentCommit : null, fetch, cacheFrom, cacheTo }); console.log(outputHash); } else { // watch for changes (specs/behaviors/watch.md): cycles are // serialized and latest-wins — rapid changes coalesce, and a cycle // whose input was superseded mid-flight discards its result at the // publication gate, before the output line and any commit const watchLoop = new WatchLoop({ cycle: async ({ treeHash, fetch: cycleFetch = false }) => { const cycleWorkspace = await repo.createWorkspaceFromTreeHash(treeHash); return Projection.projectBranch(cycleWorkspace.getBranch(holobranch), { debug, lens, fetch: cycleFetch, cacheFrom, cacheTo }); }, publish: async (projectedHash, { commitHash }) => { if (commitTo) { projectedHash = await Projection.commitTree(repo, holobranch, projectedHash, commitTo, { parentCommit: commitSourceParent ? commitHash : null, commitMessage }); } outputHash = projectedHash; console.log(projectedHash); }, onError: (err, { treeHash }) => { // a failed cycle publishes nothing and never stops the // watch; the next change triggers a normal cycle logger.error('projection cycle failed for %s: %s', treeHash, err.message); lastQueuedTreeHash = null; // allow an identical re-trigger to retry } }); // suppress re-projection of an input state identical to the last // queued one (duplicate suppression is by hash, not by event) let lastQueuedTreeHash = null; // establish the watcher before the initial projection so a change // racing watch startup supersedes it instead of being lost const { watching } = await repo.watch({ callback: (newTreeHash, newCommitHash = null) => { logger.info('watch new hash: %s (from:%s)', newTreeHash, newCommitHash || 'unknown'); if (newTreeHash == lastQueuedTreeHash) { logger.debug('input tree unchanged, skipping cycle'); return; } lastQueuedTreeHash = newTreeHash; watchLoop.push({ treeHash: newTreeHash, commitHash: newCommitHash }); } }); // initial cycle: a watch session's first publication is the current // state, through the same loop as every later cycle (fetch, when // requested, is a startup-only side effect) lastQueuedTreeHash = await workspace.root.write(); watchLoop.push({ treeHash: lastQueuedTreeHash, commitHash: parentCommit, fetch }); await watching; } // finished return outputHash; };