hologit
Version:
Hologit automates the projection of layered composite file trees based on flat, declarative plans
335 lines (241 loc) • 8.01 kB
JavaScript
const toposort = require('toposort');
const TreeObject = require('./TreeObject.js');
const Configurable = require('./Configurable.js');
const Branch = require('./Branch.js');
const Source = require('./Source.js');
const Lens = require('./Lens.js');
const branchCache = new WeakMap();
const branchMapCache = new WeakMap();
const sourceCache = new WeakMap();
const sourceMapCache = new WeakMap();
const lensCache = new WeakMap();
const lensMapCache = new WeakMap();
class Workspace extends Configurable {
constructor ({ root=null }) {
if (!root || !(root instanceof TreeObject)) {
throw new Error('root required, must be instance of TreeObject');
}
super(...arguments);
this.root = root;
Object.freeze(this);
}
getWorkspace () {
return this;
}
getKind () {
return 'holospace';
}
getConfigPath () {
return '.holo/config.toml';
}
async writeWorkingChanges () {
const { root } = this;
if (!root.dirty) {
return;
}
if (!root.repo.workTree) {
throw new Error('cannot write working changes without work tree');
}
const git = await root.repo.getGit();
const originalTreeHash = root.hash;
await root.write();
try {
await git.readTree({ m: true, u: true }, originalTreeHash, root.hash);
} catch (err) {
throw new Error(`failed to apply changes to working tree:\n${err.stderr || err.message}`);
}
}
getBranch (name) {
let cache = branchCache.get(this);
const cachedBranch = cache && cache.get(name);
if (cachedBranch) {
return cachedBranch;
}
// instantiate branch
const branch = new Branch({
workspace: this,
name
});
// save instance to cache
if (!cache) {
cache = new Map();
branchCache.set(this, cache);
}
cache.set(name, branch);
// return instance
return branch;
}
async getBranches () {
// return cached map if available
const cachedMap = branchMapCache.get(this);
if (cachedMap) {
return cachedMap;
}
// read tree
const tree = await this.root.getSubtree(`.holo/branches`);
const children = tree ? await tree.getChildren() : {};
// build unsorted map
const childNameRe = /^([^\/]+)\.toml$/;
const map = new Map();
for (const childName in children) {
let name;
// process trees or files ending in .toml
if (children[childName].isTree) {
name = childName;
} else {
const nameMatches = childName.match(childNameRe);
if (!nameMatches) {
continue;
}
[,name] = nameMatches;
}
map.set(name, this.getBranch(name));
}
// cache and return map
branchMapCache.set(this, map);
return map;
}
getSource (name) {
let cache = sourceCache.get(this);
const cachedSource = cache && cache.get(name);
if (cachedSource) {
return cachedSource;
}
// instantiate source
const source = new Source({
workspace: this,
name
});
// save instance to cache
if (!cache) {
cache = new Map();
sourceCache.set(this, cache);
}
cache.set(name, source);
// return instance
return source;
}
async getSources () {
// return cached map if available
const cachedMap = sourceMapCache.get(this);
if (cachedMap) {
return cachedMap;
}
// read tree
const tree = await this.root.getSubtree(`.holo/sources`);
const children = await tree.getChildren();
// build unsorted map
const childNameRe = /^([^\/]+)\.toml$/;
const map = new Map();
for (const childName in children) {
const nameMatches = childName.match(childNameRe);
// skip any file not ending in .toml
if (!nameMatches) {
continue;
}
const [,name] = nameMatches;
map.set(name, this.getSource(name));
}
// cache and return map
sourceMapCache.set(this, map);
return map;
}
/**
* Return an ordered Map of layers, each being an ordered Map of Mappings
*/
async getLayers () {
}
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,
name
});
// 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.root.getSubtree(`.holo/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 blbo
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
// TODO: de-dupe this behavior with Branch.getLenses
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;
}
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;
}
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;
}
}
module.exports = Workspace;