UNPKG

hologit

Version:

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

387 lines (288 loc) 11.5 kB
const toposort = require('toposort'); const logger = require('./logger'); const Configurable = require('./Configurable.js'); const Mapping = require('./Mapping.js'); const Lens = require('./Lens.js'); const mappingCache = new WeakMap(); const mappingMapCache = new WeakMap(); const lensCache = new WeakMap(); const lensMapCache = new WeakMap(); class Branch extends Configurable { constructor ({ workspace, name }) { if (!workspace) { throw new Error('workspace required'); } if (!name) { throw new Error('name required'); } super(...arguments); this.name = name; Object.freeze(this); } getKind () { return 'holobranch'; } getConfigPath () { return `.holo/branches/${this.name}.toml`; } async readConfig () { return await super.readConfig() || Branch.DEFAULT_CONFIG; } async isDefined () { return await this.readConfig() !== Branch.DEFAULT_CONFIG || (await this.getMappings()).size; } getMapping (key) { let cache = mappingCache.get(this); const cachedBranch = cache && cache.get(key); if (cachedBranch) { return cachedBranch; } // instantiate mapping const mapping = new Mapping({ branch: this, key }); // save instance to cache if (!cache) { cache = new Map(); mappingCache.set(this, cache); } cache.set(key, mapping); // return instance return mapping; } async getMappings () { // return cached map if available const cachedMap = mappingMapCache.get(this); if (cachedMap) { return cachedMap; } // read tree logger.info(`reading mappings from holobranch: ${this.name}`); const tree = await this.workspace.root.getSubtree(`.holo/branches/${this.name}`); // build unsorted hash and by-layer grouping const nameRe = /^([^\/]+)\.toml$/; const searchQueue = tree ? [{ prefix: '', tree }] : []; const mappings = {}, mappingsByLayer = {}; while (searchQueue.length) { const { prefix, tree } = searchQueue.shift(); const children = await tree.getChildren(); for (const childName in children) { // enqueue child trees const child = children[childName]; if (child.isTree) { searchQueue.push({ prefix: prefix+childName+'/', tree: child }); continue; } // match .toml files const nameMatches = childName.match(nameRe); if (!nameMatches) { continue; } // instantiate mapping const [,name] = nameMatches; const mapping = this.getMapping(prefix+name); mappings[mapping.key] = mapping; // group by layer const { layer } = await mapping.getCachedConfig(); if (layer in mappingsByLayer) { mappingsByLayer[layer].push(mapping); } else { mappingsByLayer[layer] = [mapping]; } } } // compile edges formed by before/after requirements const edges = []; for (const key in mappings) { const mapping = mappings[key]; const { after, before, layer } = await mapping.getCachedConfig(); if (after) { for (const afterLayer of after) { if (afterLayer == '*') { for (const otherLayer in mappingsByLayer) { if (otherLayer != layer && after.indexOf(otherLayer) == -1) { after.push(otherLayer); } } continue; } if (!mappingsByLayer[afterLayer]) { throw new Error(`layer ${afterLayer} not found, configured as 'after' requirement for mapping ${key}`); } for (const afterMapping of mappingsByLayer[afterLayer]) { edges.push([afterMapping, mapping]); } } } if (before) { for (const beforeLayer of before) { if (beforeLayer == '*') { for (const otherLayer in mappingsByLayer) { if (otherLayer != layer && before.indexOf(otherLayer) == -1) { before.push(otherLayer); } } continue; } if (!mappingsByLayer[beforeLayer]) { throw new Error(`layer ${beforeLayer} not found, configured as 'before' requirement for mapping ${key}`); } for (const beforeMapping of mappingsByLayer[beforeLayer]) { edges.push([mapping, beforeMapping]); } } } } // build map of mappings sorted by before/after requirements const map = new Map(); for (const mapping of toposort.array(Object.values(mappings), edges)) { map.set(mapping.key, mapping); } // cache and return map mappingMapCache.set(this, map); return map; } async composite ({ outputTree = this.getRepo().createTree(), fetch = false, cacheFrom = null, cacheTo = null }) { const repo = this.getRepo(); const mappings = await this.getMappings(); logger.info('compositing tree...'); for (const mapping of mappings.values()) { const { layer, root, files, output, holosource } = await mapping.getCachedConfig(); logger.info(`merging ${layer}:${root != '.' ? root+'/' : ''}{${files}} -> /${output != '.' ? output+'/' : ''}`); // load source const source = await this.workspace.getSource(holosource); if ( fetch === true || (Array.isArray(fetch) && fetch.indexOf(source.holosourceName) >= 0) ) { const originalHash = await source.getHead(); await source.fetch(); const hash = await source.getHead(); const { url, ref } = await source.getCachedConfig(); if (hash == originalHash) { logger.info(`${source.name}@${hash.substr(0, 8)} up-to-date`); } else { logger.info(`${source.name}@${originalHash.substr(0, 8)}..${hash.substr(0, 8)} fetched ${url}#${ref}`); } } // load tree const sourceTreeHash = await source.getOutputTree({ fetch, cacheFrom, cacheTo }); const sourceTree = await repo.createTreeFromRef(`${sourceTreeHash}:${root == '.' ? '' : root}`); // merge source into target const targetTree = await outputTree.getSubtree(output, true); // TODO: investigate why this crashes when a submodule commit is present at the target tree path await targetTree.merge(sourceTree, { files: files }); } // return supplied or created tree return outputTree; } getLens (name) { let cache = lensCache.get(this); const cachedLens = cache && cache.get(name); if (cachedLens) { return cachedLens; } // instantiate lens const lens = new Lens({ workspace: this.workspace, name, path: `.holo/branches/${this.name}.lenses/${name}.toml` }); // save instance to cache if (!cache) { cache = new Map(); lensCache.set(this, cache); } cache.set(name, lens); // return instance return lens; } /** * Return an order list of Lens objects */ async getLenses () { // return cached map if available const cachedMap = lensMapCache.get(this); if (cachedMap) { return cachedMap; } // read tree const tree = await this.workspace.root.getSubtree(`.holo/branches/${this.name}.lenses`); const children = tree ? await tree.getChildren() : {}; // build unsorted hash const childNameRe = /^([^\/]+)\.toml$/; const lenses = {}; for (const childName in children) { // skip any child not ending in .toml const filenameMatches = childName.match(childNameRe); if (!filenameMatches) { continue } // skip any child that is deleted or isn't a blob const treeChild = children[childName]; if (!treeChild || !treeChild.isBlob) { continue; } // read lens const [, name] = filenameMatches; lenses[name] = this.getLens(name); } // compile edges formed by before/after requirements const edges = []; for (const name in lenses) { const lens = lenses[name]; const { after, before } = await lens.getCachedConfig(); if (after) { for (const afterLens of after) { if (afterLens == '*') { for (const otherLens in lenses) { if (otherLens != name && after.indexOf(otherLens) == -1) { after.push(otherLens); } } continue; } else if (!(afterLens in lenses)) { throw new Error(`lens "${name}" defines after="${afterLens}", but it was not found in [${Object.keys(lenses)}]`); } edges.push([lenses[afterLens], lens]); } } if (before) { for (const beforeLens of before) { if (beforeLens == '*') { for (const otherLens in lenses) { if (otherLens != name && before.indexOf(otherLens) == -1) { before.push(otherLens); } } continue; } else if (!(beforeLens in lenses)) { throw new Error(`lens "${name}" defines before="${beforeLens}", but it was not found in [${Object.keys(lenses)}]`); } edges.push([lens, lenses[beforeLens]]); } } } // build map of lenses sorted by before/after requirements const map = new Map(); for (const lens of toposort.array(Object.values(lenses), edges)) { map.set(lens.name, lens); } // cache and return map lensMapCache.set(this, map); return map; } } Object.freeze(Branch.DEFAULT_CONFIG = {}); module.exports = Branch;