UNPKG

hologit

Version:

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

398 lines (314 loc) 12.6 kB
const path = require('path'); const fs = require('mz/fs'); const BlobObject = require('./BlobObject.js'); const TreeObject = require('./TreeObject.js'); const CommitObject = require('./CommitObject.js'); const Git = require('./Git.js'); const Workspace = require('./Workspace.js'); const instanceCache = new Map(); const gitCache = new WeakMap(); const workspaces = new WeakMap(); class Repo { /** * Get holo repo instance from environment */ static async getFromEnvironment ({ ref = 'HEAD', working = false } = {}) { const git = await Git.get(); // get git repository const gitDir = await git.constructor.getGitDirFromEnvironment(); if (!gitDir) { throw new Error('current directory is not a git repository and none was otherwise specified'); } // get git working tree const workTree = working && await git.constructor.getWorkTreeFromEnvironment(); if (working && !workTree) { throw new Error('current directory is not a work tree and none was otherwise specified'); } // try to return existing instance first let cache = instanceCache.get(gitDir); const cacheKey = working ? `working:${workTree}` : `ref:${ref}`; const cachedRepo = cache && cache.get(cacheKey); if (cachedRepo) { return cachedRepo; } // instantiate repo const repo = new this({ gitDir, ref, workTree }); // ensure .git/holo/ directory exists if (!await fs.exists(`${gitDir}/holo`)) { await fs.mkdir(`${gitDir}/holo`); } // save instance to cache if (!cache) { cache = new Map(); instanceCache.set(gitDir, cache); } cache.set(cacheKey, repo); // return instance return repo; } constructor ({ gitDir, ref = 'HEAD', workTree = null }) { this.gitDir = gitDir; this.ref = ref; this.workTree = workTree; Object.freeze(this); } async getWorkspace () { let workspace = workspaces.get(this); if (!workspace) { workspace = this.workTree ? await this.createWorkspaceFromTreeHash(await this.hashWorkTree()) : await this.createWorkspaceFromRef(this.ref); workspaces.set(this, workspace); } return workspace; } async createWorkspaceFromRef (ref) { return new Workspace({ root: await this.createTreeFromRef(ref) }); } async createWorkspaceFromTreeHash (hash) { return new Workspace({ root: await this.createTree({ hash }) }); } async getGit () { const cachedGit = gitCache.get(this); if (cachedGit) { return cachedGit; } const envGit = await Git.get(); const git = new envGit.Git({ gitDir: this.gitDir, workTree: this.workTree }); gitCache.set(this, git); return git; } async resolveRef (ref = null) { const git = await this.getGit(); return await git.revParse({ verify: true }, ref || this.ref, { $nullOnError: true }); } createBlob () { return new BlobObject(this, ...arguments); } async writeBlob () { return BlobObject.write(this, ...arguments); } async writeBlobFromFile () { return BlobObject.writeFromFile(this, ...arguments); } createTree () { return new TreeObject(this, ...arguments); } async createTreeFromRef () { return TreeObject.createFromRef(this, ...arguments); } createCommit () { return new CommitObject(this, ...arguments); } /** * Test whether a given commit is available in the repository * @param {string} commit- hash or ref for commit to check */ async hasCommit (commit) { const git = await this.getGit(); return 'commit' == await git.catFile({ t: true }, commit, { $nullOnError: true }); } async hashWorkTree () { const logger = require('../lib/logger.js'); if (!this.workTree) { throw new Error('cannot call hashWorkTree from non-working repo instance'); } const git = await this.getGit(); // get holoindex path, cloning index if needed const indexPath = await git.getIndexPath(); const holoIndexPath = `${indexPath}.holo`; if (!await fs.exists(holoIndexPath) && await fs.exists(indexPath)) { await fs.copyFile(indexPath, holoIndexPath); } // build a tree via the cloned index logger.info('indexing working tree (this can take a while under Docker)...'); const holoIndexEnv = { $indexFile: holoIndexPath }; try { await git.reset(holoIndexEnv, 'HEAD'); } catch (err) { // re-clone index and retry once await fs.copyFile(indexPath, holoIndexPath); await git.reset(holoIndexEnv, 'HEAD'); } await git.add(holoIndexEnv, { all: true }); const treeHash = await git.writeTree(holoIndexEnv); logger.info(`using working tree: ${treeHash}`); return treeHash; } async watch ({ callback }) { const debounce = require('debounce'); const logger = require('../lib/logger.js'); // either use watchman to monitor working tree or use fs.watch on .git/HEAD if (this.workTree) { // load watchman client const watchman = require('fb-watchman'); const client = new watchman.Client(); // get watchman capabilities const capabilities = await new Promise((resolve, reject) => { client.capabilityCheck( { required: ['relative_root', 'term-dirname', 'term-not', 'term-allof', 'term-type'], optional: ['defer_vcs'] }, (err, response) => { if (err) { client.end(); reject(err); } else { resolve(response); } } ); }); logger.debug('watchman capabilities: %o', capabilities); // initiate watch const watchResponse = await new Promise((resolve, reject) => { client.command(['watch-project', this.workTree], (err, response) => { if (err) { client.end(); reject(err); } else { if ('warning' in response) { logger.warn(response.warning); } resolve(response); } }); }); logger.debug('watch established: %o', watchResponse); // get current clock const clockResponse = await new Promise((resolve, reject) => { client.command(['clock', watchResponse.watch], (err, response) => { if (err) { reject(err); } else { resolve(response); } }); }); // subscribe to changes const subscription = await new Promise((resolve, reject) => { const expression = [ 'allof', ['type', 'f'], ['not', ['suffix', 'swp']], ['not', ['suffix', 'swo']], ['not', ['match', '*~', 'basename', { includedotfiles: true }]], ['not', ['match', '.watchman-*', 'basename', { includedotfiles: true }]] ]; const gitDirRelative = path.relative(this.workTree, this.gitDir); if (!gitDirRelative.startsWith('../')) { expression.push(['not', ['dirname', gitDirRelative]]); } client.command( [ 'subscribe', watchResponse.watch, 'hologitsubscription', { expression: expression, since: clockResponse.clock, relative_root: watchResponse.relative_path, defer_vcs: true } ], (err, response) => { if (err) { reject(err); } else { resolve(response); } } ); }); // relay changes to tree // TODO: why is this extra dupe suppression necessary? watchman should handle this client.on('subscription', debounce(async () => { callback(await this.hashWorkTree()); }, 50)); // return cancel method and watching promise let cancel = () => {}; return { watching: new Promise((resolve, reject) => { cancel = resolve; client.on('end', resolve); }), cancel }; } else { const chokidar = require('chokidar'); const watcher = chokidar.watch([], { persistent: true, cwd: this.gitDir, ignoreInitial: true }); // watcher.on('all', logger.debug); // watcher.on('error', logger.error); const symbolicRefs = new Map(); const addRef = async ref => { // add watch for ref watcher.add(ref); // debug output watcher.add result // setTimeout(() => { // const watched = watcher.getWatched(); // logger.debug('watcher.add(%s)', ref, [].concat(...Object.keys(watched).map(watchedDir => watched[watchedDir].map(watchedFile => path.join(watchedDir, watchedFile))))); // }, 100); // resolve any symbolic target let targetRef; const refContents = await fs.readFile(path.join(this.gitDir, ref), 'ascii'); if (refContents.startsWith('ref:')) { targetRef = refContents.substr(4).trim(); } // remove existing watch on any symbolic target const oldTargetRef = symbolicRefs.get(ref); if (oldTargetRef && oldTargetRef != targetRef) { watcher.unwatch(oldTargetRef); symbolicRefs.delete(ref); } // recurse to watch target ref if (targetRef && targetRef != oldTargetRef) { symbolicRefs.set(ref, targetRef); return addRef(targetRef); } }; // make initial call to addRef await addRef(this.ref); // monitor for tree hash changes let lastRefHash, lastTreeHash; watcher.on('change', async ref => { await addRef(ref); // add watch if contents is symbolic ref const git = await this.getGit(); const refHash = await git.revParse({ verify: true }, ref); if (refHash != lastRefHash) { lastRefHash = refHash; const treeHash = await git.getTreeHash(refHash); if (treeHash != lastTreeHash) { lastTreeHash = treeHash; callback(treeHash, refHash); } } }); // return cancel method and watching promise let cancel = () => {}; return { watching: new Promise((resolve, reject) => { watcher.on('error', reject); cancel = () => { logger.debug('cancelling watcher'); watcher.close(); resolve(); }; }), cancel }; } } } module.exports = Repo;