UNPKG

git-validate

Version:

the extensible core of precommit-hook

299 lines (225 loc) 8.57 kB
var Fs = require('fs'); var Path = require('path'); var internals = {}; /* $lab:coverage:off$ */ // Coverage disabled for this method because we use a child process to test it due to the process.exit() call internals.throwWarn = function (warning) { console.error('WARNING: ' + warning + ', installation aborted.'); process.exit(0); }; /* $lab:coverage:on$ */ // Find the topmost parent of the given module. internals.findParent = function (mod) { return mod.parent ? internals.findParent(mod.parent) : mod; }; // Similar to mkdir -p, recursively creates directories until `path` exists internals.mkdir = function (path) { var mode = ~process.umask() & parseInt('777', 8); if (exports.isDir(path)) { return; } try { Fs.mkdirSync(path, mode); } catch (err) { if (err.code !== 'ENOENT') { throw err; } internals.mkdir(Path.dirname(path)); internals.mkdir(path); } }; // Expands source and target to absolute paths, then calls internals.copy exports.copy = function (source, target, options) { if (typeof target === 'object') { options = target; target = undefined; } options = options || {}; var root = Path.dirname(internals.findParent(module).filename); var projectRoot = exports.findProjectRoot(root); var sourcePath = Path.resolve(root, source); var targetPath = Path.resolve(projectRoot, target || source); if (targetPath.indexOf(projectRoot) !== 0) { throw new Error('Destination must be within project root'); } return internals.copy(sourcePath, targetPath, options); }; // Determine if source is a directory or a file and call the appropriate method internals.copy = function (source, target, options) { if (exports.isDir(source)) { internals.copyDirectory(source, target, options); } else { return internals.copyFile(source, target, options); } }; // Recursively copy a directory internals.copyDirectory = function (source, target, options) { internals.mkdir(target); var sources = Fs.readdirSync(source); for (var i = 0, l = sources.length; i < l; ++i) { var sourcePath = Path.join(source, sources[i]); var targetPath = Path.join(target, sources[i]); internals.copy(sourcePath, targetPath, options); } }; // Copy a single file internals.copyFile = function (source, target, options) { internals.mkdir(Path.dirname(target)); var mode = ~process.umask() & parseInt('666', 8); if (Fs.existsSync(target) && !options.overwrite) { return new Error(target + ' already exists'); } var sourceContent = ''; try { sourceContent = Fs.readFileSync(source); } catch (e) { /* no source to copy */ } Fs.writeFileSync(target, sourceContent, { flag: 'w', mode: mode }); }; // Given a path, determine if the path is a directory exports.isDir = function (path) { try { var stat = Fs.statSync(path); return stat.isDirectory(); } catch (e) { return false; } }; // Given a starting directory, find the root of a git repository. // In this case, the root is defined as the first directory that contains // a directory named ".git" // // Returns a string if found, otherwise undefined exports.findGitRoot = function (start) { start = start || Path.dirname(internals.findParent(module).filename); var root; if (exports.isDir(Path.join(start, '.git'))) { root = start; } /* $lab:coverage:off$ */ // Coverage disabled here due to false positive on else if, since we have to trap the throwWarn method else if (Path.dirname(start) !== start) { root = exports.findGitRoot(Path.dirname(start)); } else { return internals.throwWarn('Unable to find a .git directory for this project'); } /* $lab:coverage:on$ */ return root; }; // Given a starting directory, find the root of the current project. // The root of the project is defined as the topmost directory that is // *not* contained within a directory named "node_modules" that also // contains a file named "package.json" // // Returns a string exports.findProjectRoot = function (start) { start = start || Path.dirname(internals.findParent(module).filename); var position = start.indexOf('node_modules'); var root = start.slice(0, position === -1 ? undefined : position - Path.sep.length); /* $lab:coverage:off$ */ // Coverage disabled here due to having to trap the throwWarn method if (root === Path.resolve(root, '..')) { return internals.throwWarn('Unable to find a package.json for this project'); } /* $lab:coverage:on$ */ while (!Fs.existsSync(Path.join(root, 'package.json'))) { root = exports.findProjectRoot(Path.dirname(root)); } return root; }; // Given a root path, find a list of projects. // A project is defined as any directory within 4 levels of the starting // directory that contains a file named "package.json" // // Returns an array exports.findProjects = function (start, depth) { start = start || exports.findGitRoot(internals.findParent(module).filename); depth = depth || 0; ++depth; if (depth > 4 || !exports.isDir(start) || start.indexOf('node_modules') !== -1) { return []; } var dirs = Fs.readdirSync(start); var projects = []; if (Fs.existsSync(Path.join(start, 'package.json'))) { projects.push(start); } for (var i = 0, il = dirs.length; i < il; ++i) { var dir = dirs[i]; var path = Path.join(start, dir); if (Fs.existsSync(Path.join(path, 'package.json'))) { projects.push(path); } else { projects = projects.concat(exports.findProjects(path, depth)); } } return projects; }; // Install the git hook as specified by `hook`. // For example, Validate.installHook('pre-commit'); exports.installHooks = function (hooks, root) { hooks = Array.isArray(hooks) ? hooks : [hooks]; var gitRoot = exports.findGitRoot(root); var hookRoot = Path.join(gitRoot, '.git', 'hooks'); var source = Path.resolve(__dirname, '..', 'bin', 'validate.sh'); if (!exports.isDir(hookRoot)) { internals.mkdir(hookRoot); } for (var i = 0, il = hooks.length; i < il; ++i) { var hook = hooks[i]; var dest = Path.join(hookRoot, hook); if (Fs.existsSync(dest)) { Fs.renameSync(dest, dest + '.backup'); } else { // There is no harm in removing a non-existant file. // It could also be a broken symlink, which is also fine to be // removed. If it wasn't removed, writing the new hook would fail. try { Fs.unlinkSync(dest) } catch (err) { if (err.code !== 'ENOENT') { throw err; } } } Fs.writeFileSync(dest, Fs.readFileSync(source), { mode: 511 }); } }; // Provide a default configuration for a git hook as specified by `hook`. // For example, Validate.configureHook('pre-commit', ['test', 'lint']); exports.configureHook = function (hook, defaults, overwrite, root) { var packagePath = Path.join(exports.findProjectRoot(root), 'package.json'); var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' })); if (!package.hasOwnProperty(hook) || overwrite) { package[hook] = Array.isArray(defaults) ? defaults : [defaults]; Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' }); } }; // Configure a default script by name and content // For example, Validate.installScript('test', 'lab -a code -L'); // Or Validate.installScript('test', 'lab -a code -L', {overwrite: true}); to force updating exports.installScript = function (name, script, options, root) { if (typeof options === 'string') { root = options; options = null; } options = options || {}; var packagePath = Path.join(exports.findProjectRoot(root), 'package.json'); var package = JSON.parse(Fs.readFileSync(packagePath, { encoding: 'utf8' })); if (!package.hasOwnProperty('scripts') || !package.scripts.hasOwnProperty(name) || options.overwrite) { package.scripts = package.scripts || {}; package.scripts[name] = script; Fs.writeFileSync(packagePath, JSON.stringify(package, null, 2), { encoding: 'utf8' }); } };