UNPKG

5.37 kBJavaScriptView Raw
1var fs = require('fs'),
2 path = require('path'),
3 handlebars = require('handlebars'),
4 escapeBackslashes = require('./escapeBackslashes.handlebars');
5
6handlebars.registerHelper('escapeBackslashes', escapeBackslashes);
7
8/**
9 * Class to help manage a hook
10 * @class Hook
11 * @module githooks
12 * @constructor
13 */
14function Hook(hookName, taskNames, options) {
15
16 /**
17 * The name of the hook
18 * @property hookName
19 * @type {String}
20 */
21 this.hookName = hookName;
22
23 /**
24 * The name of the tasks that should be run by the hook, space separated
25 * @property taskNames
26 * @type {String}
27 */
28 this.taskNames = taskNames;
29
30 /**
31 * Options for the creation of the hook
32 * @property options
33 * @type {Object}
34 */
35 this.options = options || {};
36
37 this.markerRegExp = new RegExp(this.options.startMarker.replace(/\//g, '\\/') +
38 '[\\s\\S]*' + // Not .* because it needs to match \n
39 this.options.endMarker.replace(/\//g, '\\/'), 'm');
40}
41
42Hook.prototype = {
43
44 /**
45 * Creates the hook
46 * @method create
47 */
48 create: function() {
49
50 var hookPath = path.resolve(this.options.dest, this.hookName);
51
52 var existingCode = this.getHookContent(hookPath);
53
54 if (existingCode) {
55 this.validateScriptLanguage(existingCode);
56 }
57
58 var hookContent;
59 if(this.hasMarkers(existingCode)) {
60 hookContent = this.insertBindingCode(existingCode);
61 } else {
62 hookContent = this.appendBindingCode(existingCode);
63 }
64
65 fs.writeFileSync(hookPath, hookContent);
66
67 fs.chmodSync(hookPath, '755');
68 },
69
70 /**
71 * Returns the content of the hook at given path
72 * @method getHookContent
73 * @param path {String} The path to the hook
74 * @return {String}
75 */
76 getHookContent: function (path) {
77
78 if (fs.existsSync(path)) {
79 return fs.readFileSync(path, {encoding: 'utf-8'});
80 }
81 },
82
83 /**
84 * Validates that the language of given script matches the one
85 * the binding code will be generated in
86 * @method validateScriptLanguage
87 * @param script {String}
88 * @throws {Error}
89 */
90 validateScriptLanguage: function (script) {
91
92 if (!this.isValidScriptLanguage(script, this.options.hashbang)) {
93 throw new Error('ERR_INVALID_SCRIPT_LANGUAGE');
94 }
95 },
96
97 /**
98 * Checks that content of given hook is written in the appropriate scripting
99 * language, checking if it starts with the appropriate hashbang
100 * @method isValidScriptLanguage
101 * @param hookContent {String}
102 * @param hashbang {String}
103 * @return {Boolean}
104 */
105 isValidScriptLanguage: function(hookContent, hashbang) {
106
107 var firstLine = hookContent.split('\n')[0];
108 return firstLine.indexOf(hashbang) !== -1;
109 },
110
111 /**
112 * Checks if given code has marker for inserting the binding code
113 * @method hasMarkers
114 * @param existingCode {String}
115 * @return {Boolean}
116 */
117 hasMarkers: function(existingCode) {
118
119 return this.markerRegExp.test(existingCode);
120 },
121
122 /**
123 * Creates the code that will run the grunt task from the hook
124 * @method createBindingCode
125 * @param task {String}
126 * @param templatePath {Object}
127 */
128 createBindingCode: function () {
129
130 var template = this.loadTemplate(this.options.template);
131 var bindingCode = template({
132 command: this.options.command,
133 task: this.taskNames,
134 preventExit: this.options.preventExit,
135 args: this.options.args,
136 gruntfileDirectory: process.cwd(),
137 options: this.options
138 });
139
140 return this.options.startMarker + '\n' + bindingCode + '\n' + this.options.endMarker;
141 },
142
143 /**
144 * Loads template at given path
145 * @method loadTemplate
146 * @param templatePath {String}
147 * @return {Function}
148 */
149 loadTemplate: function () {
150
151 var template = fs.readFileSync(this.options.template, {
152 encoding: 'utf-8'
153 });
154
155 return handlebars.compile(template);
156 },
157
158 /**
159 * Appends code binding the given task to given existing code
160 * @method appendBindingCode
161 * @param existingCode {String}
162 * @return {String}
163 */
164 appendBindingCode: function (existingCode) {
165
166 var bindingCode = this.createBindingCode();
167 return (existingCode || this.options.hashbang) +
168 '\n\n' +
169 bindingCode;
170 },
171
172 /**
173 * Inserts binding code at the position shown by markers in the given code
174 * @method insertBindingCode
175 * @param existingCode {String}
176 * @return {String}
177 */
178 insertBindingCode: function (existingCode) {
179
180 var bindingCode = this.createBindingCode();
181 return existingCode.replace(this.markerRegExp, bindingCode);
182 },
183};
184
185/**
186 * The name of the hooks offered by Git
187 * @property HOOK_NAMES
188 * @type {Array}
189 * @static
190 */
191Hook.HOOK_NAMES = [
192 // CLIENT HOOKS
193 'applypatch-msg',
194 'pre-applypatch',
195 'post-applypatch',
196 'pre-commit',
197 'prepare-commit-msg',
198 'commit-msg',
199 'post-commit',
200 'pre-rebase',
201 'pre-push',
202 'post-checkout',
203 'post-merge',
204 'post-rewrite',
205
206 // SERVER HOOKS
207 'pre-receive',
208 'update',
209 'post-receive',
210 'post-update',
211 'pre-auto-gc'
212];
213/**
214 * Checks that given name is the name of a Git hook (see HOOK_NAMES for the list of Git hook names)
215 * @method isNameOfAGitHook
216 * @param name {String}
217 * @return {Boolean}
218 * @static
219 */
220Hook.isNameOfAGitHook = function(name) {
221 return Hook.HOOK_NAMES.indexOf(name) !== -1;
222};
223
224module.exports.Hook = Hook;