hologit
Version:
Hologit automates the projection of layered composite file trees based on flat, declarative plans
218 lines (188 loc) • 6.94 kB
JavaScript
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;
};