wecodemore/grunt-githooks

View on GitHub
lib/githooks.js

Summary

Maintainability
A
0 mins
Test Coverage
var fs = require('fs'),
    path = require('path'),
    handlebars = require('handlebars'),
    escapeBackslashes = require('./escapeBackslashes.handlebars');

handlebars.registerHelper('escapeBackslashes', escapeBackslashes);

/**
 * Class to help manage a hook
 * @class Hook
 * @module githooks
 * @constructor
 */
function Hook(hookName, taskNames, options) {

  /**
   * The name of the hook
   * @property hookName
   * @type {String}
   */
  this.hookName = hookName;

  /**
   * The name of the tasks that should be run by the hook, space separated
   * @property taskNames
   * @type {String}
   */
  this.taskNames = taskNames;

  /**
   * Options for the creation of the hook
   * @property options
   * @type {Object}
   */
  this.options = options || {};

  this.markerRegExp = new RegExp(this.options.startMarker.replace(/\//g, '\\/') +
                      '[\\s\\S]*' + // Not .* because it needs to match \n
                      this.options.endMarker.replace(/\//g, '\\/'), 'm');
}

Hook.prototype = {

  /**
   * Creates the hook
   * @method create
   */
  create: function() {

    var hookPath = path.resolve(this.options.dest, this.hookName);

    var existingCode = this.getHookContent(hookPath);

    if (existingCode) {
      this.validateScriptLanguage(existingCode);
    }

    var hookContent;
    if(this.hasMarkers(existingCode)) {
      hookContent = this.insertBindingCode(existingCode);
    } else {
      hookContent = this.appendBindingCode(existingCode);
    }

    fs.writeFileSync(hookPath, hookContent);

    fs.chmodSync(hookPath, '755');
  },

  /**
   * Returns the content of the hook at given path
   * @method getHookContent
   * @param path {String} The path to the hook
   * @return {String}
   */
  getHookContent: function (path) {

    if (fs.existsSync(path)) {
      return fs.readFileSync(path, {encoding: 'utf-8'});
    }
  },

  /**
   * Validates that the language of given script matches the one
   * the binding code will be generated in
   * @method validateScriptLanguage
   * @param script {String}
   * @throws {Error} 
   */
  validateScriptLanguage: function (script) {

      if (!this.isValidScriptLanguage(script, this.options.hashbang)) {
        throw new Error('ERR_INVALID_SCRIPT_LANGUAGE');
      }
  },

  /**
   * Checks that content of given hook is written in the appropriate scripting
   * language, checking if it starts with the appropriate hashbang
   * @method isValidScriptLanguage
   * @param hookContent {String}
   * @param hashbang {String}
   * @return {Boolean}
   */
  isValidScriptLanguage: function(hookContent, hashbang) {

    var firstLine = hookContent.split('\n')[0];
    return firstLine.indexOf(hashbang) !== -1;
  },

  /**
   * Checks if given code has marker for inserting the binding code
   * @method hasMarkers
   * @param existingCode {String}
   * @return {Boolean}
   */
  hasMarkers: function(existingCode) {

    return this.markerRegExp.test(existingCode);
  },

  /**
   * Creates the code that will run the grunt task from the hook
   * @method createBindingCode
   * @param task {String}
   * @param templatePath {Object}
   */
  createBindingCode: function () {

    var template = this.loadTemplate(this.options.template);
    var bindingCode = template({
      hook: this.hookName,
      command: this.options.command,
      task: this.taskNames,
      preventExit: this.options.preventExit,
      args: this.options.args,
      gruntfileDirectory: process.cwd(),
      options: this.options
    });

    return this.options.startMarker + '\n' + bindingCode + '\n' + this.options.endMarker;
  },

  /**
   * Loads template at given path
   * @method loadTemplate
   * @param templatePath {String}
   * @return {Function}
   */
  loadTemplate: function () {

    var template = fs.readFileSync(this.options.template, {
      encoding: 'utf-8'
    });

    return handlebars.compile(template);
  },

  /**
   * Appends code binding the given task to given existing code
   * @method appendBindingCode
   * @param existingCode {String}
   * @return {String}
   */
  appendBindingCode: function (existingCode) {

    var bindingCode = this.createBindingCode();
    return (existingCode || this.options.hashbang) +
            '\n\n' +
            bindingCode;
  },

  /**
   * Inserts binding code at the position shown by markers in the given code
   * @method insertBindingCode
   * @param existingCode {String}
   * @return {String}
   */
  insertBindingCode: function (existingCode) {

    var bindingCode = this.createBindingCode();
    return existingCode.replace(this.markerRegExp, bindingCode);
  },
};

/**
 * The name of the hooks offered by Git
 * @property HOOK_NAMES
 * @type {Array}
 * @static
 */
Hook.HOOK_NAMES = [
  // CLIENT HOOKS
  'applypatch-msg',
  'pre-applypatch',
  'post-applypatch',
  'pre-commit',
  'prepare-commit-msg',
  'commit-msg',
  'post-commit',
  'pre-rebase',
  'pre-push',
  'post-checkout',
  'post-merge',
  'post-rewrite',

  // SERVER HOOKS
  'pre-receive',
  'update',
  'post-receive',
  'post-update',
  'pre-auto-gc'
];
/**
 * Checks that given name is the name of a Git hook (see HOOK_NAMES for the list of Git hook names)
 * @method isNameOfAGitHook
 * @param name {String}
 * @return {Boolean}
 * @static
 */
Hook.isNameOfAGitHook = function(name) {
  return Hook.HOOK_NAMES.indexOf(name) !== -1;
};

module.exports.Hook = Hook;