UNPKG

47.3 kBJavaScriptView Raw
1(function () {
2
3 'use strict';
4
5 var debug = require('debug')('simple-git');
6 var deferred = require('./util/deferred');
7 var exists = require('./util/exists');
8 var NOOP = function () {};
9
10 /**
11 * Git handling for node. All public functions can be chained and all `then` handlers are optional.
12 *
13 * @param {string} baseDir base directory for all processes to run
14 *
15 * @param {Object} ChildProcess The ChildProcess module
16 * @param {Function} Buffer The Buffer implementation to use
17 *
18 * @constructor
19 */
20 function Git (baseDir, ChildProcess, Buffer) {
21 this._baseDir = baseDir;
22 this._runCache = [];
23
24 this.ChildProcess = ChildProcess;
25 this.Buffer = Buffer;
26 }
27
28 /**
29 * @type {string} The command to use to reference the git binary
30 */
31 Git.prototype._command = 'git';
32
33 /**
34 * @type {[key: string]: string} An object of key=value pairs to be passed as environment variables to the
35 * spawned child process.
36 */
37 Git.prototype._env = null;
38
39 /**
40 * @type {Function} An optional handler to use when a child process is created
41 */
42 Git.prototype._outputHandler = null;
43
44 /**
45 * @type {boolean} Property showing whether logging will be silenced - defaults to true in a production environment
46 */
47 Git.prototype._silentLogging = /prod/.test(process.env.NODE_ENV);
48
49 /**
50 * Sets the path to a custom git binary, should either be `git` when there is an installation of git available on
51 * the system path, or a fully qualified path to the executable.
52 *
53 * @param {string} command
54 * @returns {Git}
55 */
56 Git.prototype.customBinary = function (command) {
57 this._command = command;
58 return this;
59 };
60
61 /**
62 * Sets an environment variable for the spawned child process, either supply both a name and value as strings or
63 * a single object to entirely replace the current environment variables.
64 *
65 * @param {string|Object} name
66 * @param {string} [value]
67 * @returns {Git}
68 */
69 Git.prototype.env = function (name, value) {
70 if (arguments.length === 1 && typeof name === 'object') {
71 this._env = name;
72 }
73 else {
74 (this._env = this._env || {})[name] = value;
75 }
76
77 return this;
78 };
79
80 /**
81 * Sets the working directory of the subsequent commands.
82 *
83 * @param {string} workingDirectory
84 * @param {Function} [then]
85 * @returns {Git}
86 */
87 Git.prototype.cwd = function (workingDirectory, then) {
88 var git = this;
89 var next = Git.trailingFunctionArgument(arguments);
90
91 return this.exec(function () {
92 git._baseDir = workingDirectory;
93 if (!exists(workingDirectory, exists.FOLDER)) {
94 Git.exception(git, 'Git.cwd: cannot change to non-directory "' + workingDirectory + '"', next);
95 }
96 else {
97 next && next(null, workingDirectory);
98 }
99 });
100 };
101
102 /**
103 * Sets a handler function to be called whenever a new child process is created, the handler function will be called
104 * with the name of the command being run and the stdout & stderr streams used by the ChildProcess.
105 *
106 * @example
107 * require('simple-git')
108 * .outputHandler(function (command, stdout, stderr) {
109 * stdout.pipe(process.stdout);
110 * })
111 * .checkout('https://github.com/user/repo.git');
112 *
113 * @see http://nodejs.org/api/child_process.html#child_process_class_childprocess
114 * @see http://nodejs.org/api/stream.html#stream_class_stream_readable
115 * @param {Function} outputHandler
116 * @returns {Git}
117 */
118 Git.prototype.outputHandler = function (outputHandler) {
119 this._outputHandler = outputHandler;
120 return this;
121 };
122
123 /**
124 * Initialize a git repo
125 *
126 * @param {Boolean} [bare=false]
127 * @param {Function} [then]
128 */
129 Git.prototype.init = function (bare, then) {
130 var commands = ['init'];
131 var next = Git.trailingFunctionArgument(arguments);
132
133 if (bare === true) {
134 commands.push('--bare');
135 }
136
137 return this._run(commands, function (err) {
138 next && next(err);
139 });
140 };
141
142 /**
143 * Check the status of the local repo
144 *
145 * @param {Function} [then]
146 */
147 Git.prototype.status = function (then) {
148 return this._run(
149 ['status', '--porcelain', '-b', '-u'],
150 Git._responseHandler(then, 'StatusSummary')
151 );
152 };
153
154 /**
155 * List the stash(s) of the local repo
156 *
157 * @param {Object|Array} [options]
158 * @param {Function} [then]
159 */
160 Git.prototype.stashList = function (options, then) {
161 var handler = Git.trailingFunctionArgument(arguments);
162 var opt = (handler === then ? options : null) || {};
163
164 var splitter = opt.splitter || ';;;;';
165 var command = [
166 "stash",
167 "list",
168 "--pretty=format:%H %ai %s%d %aN %ae".replace(/\s+/g, splitter) + require('./responses/ListLogSummary').COMMIT_BOUNDARY
169 ];
170
171 if (Array.isArray(opt)) {
172 command = command.concat(opt);
173 }
174
175 return this._run(command,
176 Git._responseHandler(handler, 'ListLogSummary', splitter)
177 );
178 };
179
180 /**
181 * Stash the local repo
182 *
183 * @param {Object|Array} [options]
184 * @param {Function} [then]
185 */
186 Git.prototype.stash = function (options, then) {
187 var handler = Git.trailingFunctionArgument(arguments);
188 var command = ["stash"];
189
190 if (Array.isArray(options)) {
191 command = command.concat(options);
192 }
193 else {
194 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
195 }
196
197 return this._run(command, function (err, data) {
198 handler && handler(err, !err && data);
199 });
200 };
201
202 /**
203 * Clone a git repo
204 *
205 * @param {string} repoPath
206 * @param {string} localPath
207 * @param {String[]} [options] Optional array of options to pass through to the clone command
208 * @param {Function} [then]
209 */
210 Git.prototype.clone = function (repoPath, localPath, options, then) {
211 var next = Git.trailingFunctionArgument(arguments);
212 var command = ['clone'].concat(Git.trailingArrayArgument(arguments));
213
214 for (var i = 0, iMax = arguments.length; i < iMax; i++) {
215 if (typeof arguments[i] === 'string') {
216 command.push(arguments[i]);
217 }
218 }
219
220 return this._run(command, function (err, data) {
221 next && next(err, data);
222 });
223 };
224
225 /**
226 * Mirror a git repo
227 *
228 * @param {string} repoPath
229 * @param {string} localPath
230 * @param {Function} [then]
231 */
232 Git.prototype.mirror = function (repoPath, localPath, then) {
233 return this.clone(repoPath, localPath, ['--mirror'], then);
234 };
235
236 /**
237 * Moves one or more files to a new destination.
238 *
239 * @see https://git-scm.com/docs/git-mv
240 *
241 * @param {string|string[]} from
242 * @param {string} to
243 * @param {Function} [then]
244 */
245 Git.prototype.mv = function (from, to, then) {
246 var handler = Git.trailingFunctionArgument(arguments);
247
248 var command = [].concat(from);
249 command.unshift('mv', '-v');
250 command.push(to);
251
252 this._run(command, Git._responseHandler(handler, 'MoveSummary'))
253 };
254
255 /**
256 * Internally uses pull and tags to get the list of tags then checks out the latest tag.
257 *
258 * @param {Function} [then]
259 */
260 Git.prototype.checkoutLatestTag = function (then) {
261 var git = this;
262 return this.pull(function() {
263 git.tags(function(err, tags) {
264 git.checkout(tags.latest, then);
265 });
266 });
267 };
268
269 /**
270 * Adds one or more files to source control
271 *
272 * @param {string|string[]} files
273 * @param {Function} [then]
274 */
275 Git.prototype.add = function (files, then) {
276 return this._run(['add'].concat(files), function (err, data) {
277 then && then(err);
278 });
279 };
280
281 /**
282 * Commits changes in the current working directory - when specific file paths are supplied, only changes on those
283 * files will be committed.
284 *
285 * @param {string|string[]} message
286 * @param {string|string[]} [files]
287 * @param {Object} [options]
288 * @param {Function} [then]
289 */
290 Git.prototype.commit = function (message, files, options, then) {
291 var handler = Git.trailingFunctionArgument(arguments);
292
293 var command = ['commit'];
294
295 [].concat(message).forEach(function (message) {
296 command.push('-m', message);
297 });
298
299 [].push.apply(command, [].concat(typeof files === "string" || Array.isArray(files) ? files : []));
300
301 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
302
303 return this._run(
304 command,
305 Git._responseHandler(handler, 'CommitSummary')
306 );
307 };
308
309 /**
310 * Gets a function to be used for logging.
311 *
312 * @param {string} level
313 * @param {string} [message]
314 *
315 * @returns {Function}
316 * @private
317 */
318 Git.prototype._getLog = function (level, message) {
319 var log = this._silentLogging ? NOOP : console[level].bind(console);
320 if (arguments.length > 1) {
321 log(message);
322 }
323 return log;
324 };
325
326 /**
327 * Pull the updated contents of the current repo
328 *
329 * @param {string} [remote] When supplied must also include the branch
330 * @param {string} [branch] When supplied must also include the remote
331 * @param {Object} [options] Optionally include set of options to merge into the command
332 * @param {Function} [then]
333 */
334 Git.prototype.pull = function (remote, branch, options, then) {
335 var command = ["pull"];
336 var handler = Git.trailingFunctionArgument(arguments);
337
338 if (typeof remote === 'string' && typeof branch === 'string') {
339 command.push(remote, branch);
340 }
341
342 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
343
344 return this._run(command, Git._responseHandler(handler, 'PullSummary'));
345 };
346
347 /**
348 * Fetch the updated contents of the current repo.
349 *
350 * @example
351 * .fetch('upstream', 'master') // fetches from master on remote named upstream
352 * .fetch(function () {}) // runs fetch against default remote and branch and calls function
353 *
354 * @param {string} [remote]
355 * @param {string} [branch]
356 * @param {Function} [then]
357 */
358 Git.prototype.fetch = function (remote, branch, then) {
359 var command = ["fetch"];
360 var next = Git.trailingFunctionArgument(arguments);
361 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
362
363 if (typeof remote === 'string' && typeof branch === 'string') {
364 command.push(remote, branch);
365 }
366
367 if (Array.isArray(remote)) {
368 command = command.concat(remote);
369 }
370
371 return this._run(
372 command,
373 Git._responseHandler(next, 'FetchSummary'),
374 {
375 concatStdErr: true
376 }
377 );
378 };
379
380 /**
381 * Disables/enables the use of the console for printing warnings and errors, by default messages are not shown in
382 * a production environment.
383 *
384 * @param {boolean} silence
385 * @returns {Git}
386 */
387 Git.prototype.silent = function (silence) {
388 this._silentLogging = !!silence;
389 return this;
390 };
391
392 /**
393 * List all tags. When using git 2.7.0 or above, include an options object with `"--sort": "property-name"` to
394 * sort the tags by that property instead of using the default semantic versioning sort.
395 *
396 * Note, supplying this option when it is not supported by your Git version will cause the operation to fail.
397 *
398 * @param {Object} [options]
399 * @param {Function} [then]
400 */
401 Git.prototype.tags = function (options, then) {
402 var next = Git.trailingFunctionArgument(arguments);
403
404 var command = ['-l'];
405 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
406
407 var hasCustomSort = command.some(function (option) {
408 return /^--sort=/.test(option);
409 });
410
411 return this.tag(
412 command,
413 Git._responseHandler(next, 'TagList', [hasCustomSort])
414 );
415 };
416
417 /**
418 * Rebases the current working copy. Options can be supplied either as an array of string parameters
419 * to be sent to the `git rebase` command, or a standard options object.
420 *
421 * @param {Object|String[]} [options]
422 * @param {Function} [then]
423 * @returns {Git}
424 */
425 Git.prototype.rebase = function (options, then) {
426 var handler = Git.trailingFunctionArgument(arguments);
427 var command = ['rebase'];
428 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
429
430 if (Array.isArray(options)) {
431 command.push.apply(command, options);
432 }
433
434 return this._run(command, function (err, data) {
435 handler && handler(err, !err && data);
436 })
437 };
438
439 /**
440 * Reset a repo
441 *
442 * @param {string|string[]} [mode=soft] Either an array of arguments supported by the 'git reset' command, or the
443 * string value 'soft' or 'hard' to set the reset mode.
444 * @param {Function} [then]
445 */
446 Git.prototype.reset = function (mode, then) {
447 var command = ['reset'];
448 var next = Git.trailingFunctionArgument(arguments);
449 if (next === mode || typeof mode === 'string' || !mode) {
450 var modeStr = ['mixed', 'soft', 'hard'].includes(mode) ? mode : 'soft';
451 command.push('--' + modeStr);
452 }
453 else if (Array.isArray(mode)) {
454 command.push.apply(command, mode);
455 }
456
457 return this._run(command, function (err) {
458 next && next(err || null);
459 });
460 };
461
462 /**
463 * Revert one or more commits in the local working copy
464 *
465 * @param {string} commit The commit to revert. Can be any hash, offset (eg: `HEAD~2`) or range (eg: `master~5..master~2`)
466 * @param {Object} [options] Optional options object
467 * @param {Function} [then]
468 */
469 Git.prototype.revert = function (commit, options, then) {
470 var next = Git.trailingFunctionArgument(arguments);
471 var command = ['revert'];
472
473 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
474
475 if (typeof commit !== 'string') {
476 return this.exec(function () {
477 next && next(new TypeError("Commit must be a string"));
478 });
479 }
480
481 command.push(commit);
482 return this._run(command, function (err) {
483 next && next(err || null);
484 });
485 };
486
487 /**
488 * Add a lightweight tag to the head of the current branch
489 *
490 * @param {string} name
491 * @param {Function} [then]
492 */
493 Git.prototype.addTag = function (name, then) {
494 if (typeof name !== "string") {
495 return this.exec(function () {
496 then && then(new TypeError("Git.addTag requires a tag name"));
497 });
498 }
499
500 return this.tag([name], then);
501 };
502
503 /**
504 * Add an annotated tag to the head of the current branch
505 *
506 * @param {string} tagName
507 * @param {string} tagMessage
508 * @param {Function} [then]
509 */
510 Git.prototype.addAnnotatedTag = function (tagName, tagMessage, then) {
511 return this.tag(['-a', '-m', tagMessage, tagName], function (err) {
512 then && then(err);
513 });
514 };
515
516 /**
517 * Check out a tag or revision, any number of additional arguments can be passed to the `git checkout` command
518 * by supplying either a string or array of strings as the `what` parameter.
519 *
520 * @param {string|string[]} what One or more commands to pass to `git checkout`
521 * @param {Function} [then]
522 */
523 Git.prototype.checkout = function (what, then) {
524 var command = ['checkout'];
525 command = command.concat(what);
526
527 return this._run(command, function (err, data) {
528 then && then(err, !err && this._parseCheckout(data));
529 });
530 };
531
532 /**
533 * Check out a remote branch
534 *
535 * @param {string} branchName name of branch
536 * @param {string} startPoint (e.g origin/development)
537 * @param {Function} [then]
538 */
539 Git.prototype.checkoutBranch = function (branchName, startPoint, then) {
540 return this.checkout(['-b', branchName, startPoint], then);
541 };
542
543 /**
544 * Check out a local branch
545 *
546 * @param {string} branchName of branch
547 * @param {Function} [then]
548 */
549 Git.prototype.checkoutLocalBranch = function (branchName, then) {
550 return this.checkout(['-b', branchName], then);
551 };
552
553 /**
554 * Delete a local branch
555 *
556 * @param {string} branchName name of branch
557 * @param {Function} [then]
558 */
559 Git.prototype.deleteLocalBranch = function (branchName, then) {
560 return this.branch(['-d', branchName], then);
561 };
562
563 /**
564 * List all branches
565 *
566 * @param {Object | string[]} [options]
567 * @param {Function} [then]
568 */
569 Git.prototype.branch = function (options, then) {
570 var isDelete, responseHandler;
571 var next = Git.trailingFunctionArgument(arguments);
572 var command = ['branch'];
573 if (Array.isArray(options)) {
574 command.push.apply(command, options);
575 }
576
577 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
578 if (!arguments.length || next === options) {
579 command.push('-a', '-v');
580 }
581
582 isDelete = ['-d', '-D', '--delete'].reduce(function (isDelete, flag) {
583 return isDelete || command.indexOf(flag) > 0;
584 }, false);
585
586 responseHandler = isDelete
587 ? Git._responseHandler(next, 'BranchDeleteSummary', false)
588 : Git._responseHandler(next, 'BranchSummary');
589
590 return this._run(command, responseHandler);
591 };
592
593 /**
594 * Return list of local branches
595 *
596 * @param {Function} [then]
597 */
598 Git.prototype.branchLocal = function (then) {
599 return this.branch(['-v'], then);
600 };
601
602 /**
603 * Add config to local git instance
604 *
605 * @param {string} key configuration key (e.g user.name)
606 * @param {string} value for the given key (e.g your name)
607 * @param {Function} [then]
608 */
609 Git.prototype.addConfig = function (key, value, then) {
610 return this._run(['config', '--local', key, value], function (err, data) {
611 then && then(err, !err && data);
612 });
613 };
614
615 /**
616 * Executes any command against the git binary.
617 *
618 * @param {string[]|Object} commands
619 * @param {Function} [then]
620 *
621 * @returns {Git}
622 */
623 Git.prototype.raw = function (commands, then) {
624 var command = [];
625 if (Array.isArray(commands)) {
626 command = commands.slice(0);
627 }
628 else {
629 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
630 }
631
632 var next = Git.trailingFunctionArgument(arguments);
633
634 if (!command.length) {
635 return this.exec(function () {
636 next && next(new Error('Raw: must supply one or more command to execute'), null);
637 });
638 }
639
640 return this._run(command, function (err, data) {
641 next && next(err, !err && data || null);
642 });
643 };
644
645 /**
646 * Add a submodule
647 *
648 * @param {string} repo
649 * @param {string} path
650 * @param {Function} [then]
651 */
652 Git.prototype.submoduleAdd = function (repo, path, then) {
653 return this._run(['submodule', 'add', repo, path], function (err) {
654 then && then(err);
655 });
656 };
657
658 /**
659 * Update submodules
660 *
661 * @param {string[]} [args]
662 * @param {Function} [then]
663 */
664 Git.prototype.submoduleUpdate = function (args, then) {
665 if (typeof args === 'string') {
666 this._getLog('warn', 'Git#submoduleUpdate: args should be supplied as an array of individual arguments');
667 }
668
669 var next = Git.trailingFunctionArgument(arguments);
670 var command = (args !== next) ? args : [];
671
672 return this.subModule(['update'].concat(command), function (err, args) {
673 next && next(err, args);
674 });
675 };
676
677 /**
678 * Initialize submodules
679 *
680 * @param {string[]} [args]
681 * @param {Function} [then]
682 */
683 Git.prototype.submoduleInit = function (args, then) {
684 if (typeof args === 'string') {
685 this._getLog('warn', 'Git#submoduleInit: args should be supplied as an array of individual arguments');
686 }
687
688 var next = Git.trailingFunctionArgument(arguments);
689 var command = (args !== next) ? args : [];
690
691 return this.subModule(['init'].concat(command), function (err, args) {
692 next && next(err, args);
693 });
694 };
695
696 /**
697 * Call any `git submodule` function with arguments passed as an array of strings.
698 *
699 * @param {string[]} options
700 * @param {Function} [then]
701 */
702 Git.prototype.subModule = function (options, then) {
703 if (!Array.isArray(options)) {
704 return this.exec(function () {
705 then && then(new TypeError("Git.subModule requires an array of arguments"));
706 });
707 }
708
709 if (options[0] !== 'submodule') {
710 options.unshift('submodule');
711 }
712
713 return this._run(options, function (err, data) {
714 then && then(err || null, err ? null : data);
715 });
716 };
717
718 /**
719 * List remote
720 *
721 * @param {string[]} [args]
722 * @param {Function} [then]
723 */
724 Git.prototype.listRemote = function (args, then) {
725 var next = Git.trailingFunctionArgument(arguments);
726 var data = next === args || args === undefined ? [] : args;
727
728 if (typeof data === 'string') {
729 this._getLog('warn', 'Git#listRemote: args should be supplied as an array of individual arguments');
730 }
731
732 return this._run(['ls-remote'].concat(data), function (err, data) {
733 next && next(err, data);
734 });
735 };
736
737 /**
738 * Adds a remote to the list of remotes.
739 *
740 * @param {string} remoteName Name of the repository - eg "upstream"
741 * @param {string} remoteRepo Fully qualified SSH or HTTP(S) path to the remote repo
742 * @param {Function} [then]
743 * @returns {*}
744 */
745 Git.prototype.addRemote = function (remoteName, remoteRepo, then) {
746 return this._run(['remote', 'add', remoteName, remoteRepo], function (err) {
747 then && then(err);
748 });
749 };
750
751 /**
752 * Removes an entry from the list of remotes.
753 *
754 * @param {string} remoteName Name of the repository - eg "upstream"
755 * @param {Function} [then]
756 * @returns {*}
757 */
758 Git.prototype.removeRemote = function (remoteName, then) {
759 return this._run(['remote', 'remove', remoteName], function (err) {
760 then && then(err);
761 });
762 };
763
764 /**
765 * Gets the currently available remotes, setting the optional verbose argument to true includes additional
766 * detail on the remotes themselves.
767 *
768 * @param {boolean} [verbose=false]
769 * @param {Function} [then]
770 */
771 Git.prototype.getRemotes = function (verbose, then) {
772 var next = Git.trailingFunctionArgument(arguments);
773 var args = verbose === true ? ['-v'] : [];
774
775 return this.remote(args, function (err, data) {
776 next && next(err, !err && function () {
777 return data.trim().split('\n').filter(Boolean).reduce(function (remotes, remote) {
778 var detail = remote.trim().split(/\s+/);
779 var name = detail.shift();
780
781 if (!remotes[name]) {
782 remotes[name] = remotes[remotes.length] = {
783 name: name,
784 refs: {}
785 };
786 }
787
788 if (detail.length) {
789 remotes[name].refs[detail.pop().replace(/[^a-z]/g, '')] = detail.pop();
790 }
791
792 return remotes;
793 }, []).slice(0);
794 }());
795 });
796 };
797
798 /**
799 * Call any `git remote` function with arguments passed as an array of strings.
800 *
801 * @param {string[]} options
802 * @param {Function} [then]
803 */
804 Git.prototype.remote = function (options, then) {
805 if (!Array.isArray(options)) {
806 return this.exec(function () {
807 then && then(new TypeError("Git.remote requires an array of arguments"));
808 });
809 }
810
811 if (options[0] !== 'remote') {
812 options.unshift('remote');
813 }
814
815 return this._run(options, function (err, data) {
816 then && then(err || null, err ? null : data);
817 });
818 };
819
820 /**
821 * Merges from one branch to another, equivalent to running `git merge ${from} $[to}`, the `options` argument can
822 * either be an array of additional parameters to pass to the command or null / omitted to be ignored.
823 *
824 * @param {string} from
825 * @param {string} to
826 * @param {string[]} [options]
827 * @param {Function} [then]
828 */
829 Git.prototype.mergeFromTo = function (from, to, options, then) {
830 var commands = [
831 from,
832 to
833 ];
834 var callback = Git.trailingFunctionArgument(arguments);
835
836 if (Array.isArray(options)) {
837 commands = commands.concat(options);
838 }
839
840 return this.merge(commands, callback);
841 };
842
843 /**
844 * Runs a merge, `options` can be either an array of arguments
845 * supported by the [`git merge`](https://git-scm.com/docs/git-merge)
846 * or an options object.
847 *
848 * Conflicts during the merge result in an error response,
849 * the response type whether it was an error or success will be a MergeSummary instance.
850 * When successful, the MergeSummary has all detail from a the PullSummary
851 *
852 * @param {Object | string[]} [options]
853 * @param {Function} [then]
854 * @returns {*}
855 *
856 * @see ./responses/MergeSummary.js
857 * @see ./responses/PullSummary.js
858 */
859 Git.prototype.merge = function (options, then) {
860 var self = this;
861 var userHandler = Git.trailingFunctionArgument(arguments) || NOOP;
862 var mergeHandler = function (err, mergeSummary) {
863 if (!err && mergeSummary.failed) {
864 return Git.fail(self, mergeSummary, userHandler);
865 }
866
867 userHandler(err, mergeSummary);
868 };
869
870 var command = [];
871 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
872 command.push.apply(command, Git.trailingArrayArgument(arguments));
873
874 if (command[0] !== 'merge') {
875 command.unshift('merge');
876 }
877
878 if (command.length === 1) {
879 return this.exec(function () {
880 then && then(new TypeError("Git.merge requires at least one option"));
881 });
882 }
883
884 return this._run(command, Git._responseHandler(mergeHandler, 'MergeSummary'), {
885 concatStdErr: true
886 });
887 };
888
889 /**
890 * Call any `git tag` function with arguments passed as an array of strings.
891 *
892 * @param {string[]} options
893 * @param {Function} [then]
894 */
895 Git.prototype.tag = function (options, then) {
896 if (!Array.isArray(options)) {
897 return this.exec(function () {
898 then && then(new TypeError("Git.tag requires an array of arguments"));
899 });
900 }
901
902 if (options[0] !== 'tag') {
903 options.unshift('tag');
904 }
905
906 return this._run(options, function (err, data) {
907 then && then(err || null, err ? null : data);
908 });
909 };
910
911 /**
912 * Updates repository server info
913 *
914 * @param {Function} [then]
915 */
916 Git.prototype.updateServerInfo = function (then) {
917 return this._run(["update-server-info"], function (err, data) {
918 then && then(err, !err && data);
919 });
920 };
921
922 /**
923 * Pushes the current committed changes to a remote, optionally specify the names of the remote and branch to use
924 * when pushing. Supply multiple options as an array of strings in the first argument - see examples below.
925 *
926 * @param {string|string[]} [remote]
927 * @param {string} [branch]
928 * @param {Function} [then]
929 */
930 Git.prototype.push = function (remote, branch, then) {
931 var command = [];
932 var handler = Git.trailingFunctionArgument(arguments);
933
934 if (typeof remote === 'string' && typeof branch === 'string') {
935 command.push(remote, branch);
936 }
937
938 if (Array.isArray(remote)) {
939 command = command.concat(remote);
940 }
941
942 Git._appendOptions(command, Git.trailingOptionsArgument(arguments));
943
944 if (command[0] !== 'push') {
945 command.unshift('push');
946 }
947
948 return this._run(command, function (err, data) {
949 handler && handler(err, !err && data);
950 });
951 };
952
953 /**
954 * Pushes the current tag changes to a remote which can be either a URL or named remote. When not specified uses the
955 * default configured remote spec.
956 *
957 * @param {string} [remote]
958 * @param {Function} [then]
959 */
960 Git.prototype.pushTags = function (remote, then) {
961 var command = ['push'];
962 if (typeof remote === "string") {
963 command.push(remote);
964 }
965 command.push('--tags');
966
967 then = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : null;
968
969 return this._run(command, function (err, data) {
970 then && then(err, !err && data);
971 });
972 };
973
974 /**
975 * Removes the named files from source control.
976 *
977 * @param {string|string[]} files
978 * @param {Function} [then]
979 */
980 Git.prototype.rm = function (files, then) {
981 return this._rm(files, '-f', then);
982 };
983
984 /**
985 * Removes the named files from source control but keeps them on disk rather than deleting them entirely. To
986 * completely remove the files, use `rm`.
987 *
988 * @param {string|string[]} files
989 * @param {Function} [then]
990 */
991 Git.prototype.rmKeepLocal = function (files, then) {
992 return this._rm(files, '--cached', then);
993 };
994
995 /**
996 * Returns a list of objects in a tree based on commit hash. Passing in an object hash returns the object's content,
997 * size, and type.
998 *
999 * Passing "-p" will instruct cat-file to determine the object type, and display its formatted contents.
1000 *
1001 * @param {string[]} [options]
1002 * @param {Function} [then]
1003 */
1004 Git.prototype.catFile = function (options, then) {
1005 return this._catFile('utf-8', arguments);
1006 };
1007
1008 /**
1009 * Equivalent to `catFile` but will return the native `Buffer` of content from the git command's stdout.
1010 *
1011 * @param {string[]} options
1012 * @param then
1013 */
1014 Git.prototype.binaryCatFile = function (options, then) {
1015 return this._catFile('buffer', arguments);
1016 };
1017
1018 Git.prototype._catFile = function (format, args) {
1019 var handler = Git.trailingFunctionArgument(args);
1020 var command = ['cat-file'];
1021 var options = args[0];
1022
1023 if (typeof options === 'string') {
1024 throw new TypeError('Git#catFile: options must be supplied as an array of strings');
1025 }
1026 else if (Array.isArray(options)) {
1027 command.push.apply(command, options);
1028 }
1029
1030 return this._run(command, function (err, data) {
1031 handler && handler(err, data);
1032 }, {
1033 format: format
1034 });
1035 };
1036
1037 /**
1038 * Return repository changes.
1039 *
1040 * @param {string[]} [options]
1041 * @param {Function} [then]
1042 */
1043 Git.prototype.diff = function (options, then) {
1044 var command = ['diff'];
1045
1046 if (typeof options === 'string') {
1047 command[0] += ' ' + options;
1048 this._getLog('warn',
1049 'Git#diff: supplying options as a single string is now deprecated, switch to an array of strings');
1050 }
1051 else if (Array.isArray(options)) {
1052 command.push.apply(command, options);
1053 }
1054
1055 if (typeof arguments[arguments.length - 1] === 'function') {
1056 then = arguments[arguments.length - 1];
1057 }
1058
1059 return this._run(command, function (err, data) {
1060 then && then(err, data);
1061 });
1062 };
1063
1064 Git.prototype.diffSummary = function (options, then) {
1065 var next = Git.trailingFunctionArgument(arguments);
1066 var command = ['--stat=4096'];
1067
1068 if (options && options !== next) {
1069 command.push.apply(command, [].concat(options));
1070 }
1071
1072 return this.diff(command, Git._responseHandler(next, 'DiffSummary'));
1073 };
1074
1075 /**
1076 * Wraps `git rev-parse`. Primarily used to convert friendly commit references (ie branch names) to SHA1 hashes.
1077 *
1078 * Options should be an array of string options compatible with the `git rev-parse`
1079 *
1080 * @param {string|string[]} [options]
1081 * @param {Function} [then]
1082 *
1083 * @see http://git-scm.com/docs/git-rev-parse
1084 */
1085 Git.prototype.revparse = function (options, then) {
1086 var command = ['rev-parse'];
1087
1088 if (typeof options === 'string') {
1089 command = command + ' ' + options;
1090 this._getLog('warn',
1091 'Git#revparse: supplying options as a single string is now deprecated, switch to an array of strings');
1092 }
1093 else if (Array.isArray(options)) {
1094 command.push.apply(command, options);
1095 }
1096
1097 if (typeof arguments[arguments.length - 1] === 'function') {
1098 then = arguments[arguments.length - 1];
1099 }
1100
1101 return this._run(command, function (err, data) {
1102 then && then(err, data);
1103 });
1104 };
1105
1106 /**
1107 * Show various types of objects, for example the file at a certain commit
1108 *
1109 * @param {string[]} [options]
1110 * @param {Function} [then]
1111 */
1112 Git.prototype.show = function (options, then) {
1113 var args = [].slice.call(arguments, 0);
1114 var handler = typeof args[args.length - 1] === "function" ? args.pop() : null;
1115 var command = ['show'];
1116 if (typeof options === 'string') {
1117 command = command + ' ' + options;
1118 this._getLog('warn',
1119 'Git#show: supplying options as a single string is now deprecated, switch to an array of strings');
1120 }
1121 else if (Array.isArray(options)) {
1122 command.push.apply(command, options);
1123 }
1124
1125 return this._run(command, function (err, data) {
1126 handler && handler(err, !err && data);
1127 });
1128 };
1129
1130 /**
1131 * @param {string} mode Required parameter "n" or "f"
1132 * @param {string[]} options
1133 * @param {Function} [then]
1134 */
1135 Git.prototype.clean = function (mode, options, then) {
1136 var handler = Git.trailingFunctionArgument(arguments);
1137
1138 if (typeof mode !== 'string' || !/[nf]/.test(mode)) {
1139 return this.exec(function () {
1140 handler && handler(new TypeError('Git clean mode parameter ("n" or "f") is required'));
1141 });
1142 }
1143
1144 if (/[^dfinqxX]/.test(mode)) {
1145 return this.exec(function () {
1146 handler && handler(new TypeError('Git clean unknown option found in ' + JSON.stringify(mode)));
1147 });
1148 }
1149
1150 var command = ['clean', '-' + mode];
1151 if (Array.isArray(options)) {
1152 command = command.concat(options);
1153 }
1154
1155 if (command.some(interactiveMode)) {
1156 return this.exec(function () {
1157 handler && handler(new TypeError('Git clean interactive mode is not supported'));
1158 });
1159 }
1160
1161 return this._run(command, function (err, data) {
1162 handler && handler(err, !err && data);
1163 });
1164
1165 function interactiveMode (option) {
1166 if (/^-[^\-]/.test(option)) {
1167 return option.indexOf('i') > 0;
1168 }
1169
1170 return option === '--interactive';
1171 }
1172 };
1173
1174 /**
1175 * Call a simple function at the next step in the chain.
1176 * @param {Function} [then]
1177 */
1178 Git.prototype.exec = function (then) {
1179 this._run([], function () {
1180 typeof then === 'function' && then();
1181 });
1182 return this;
1183 };
1184
1185 /**
1186 * Deprecated means of adding a regular function call at the next step in the chain. Use the replacement
1187 * Git#exec, the Git#then method will be removed in version 2.x
1188 *
1189 * @see exec
1190 * @deprecated
1191 */
1192 Git.prototype.then = function (then) {
1193 this._getLog(
1194 'warn',
1195 "\nGit#then is deprecated after version 1.72 and will be removed in version 2.x"
1196 + "\nPlease switch to using Git#exec to run arbitrary functions as part of the command chain.\n"
1197 );
1198 return this.exec(then);
1199 };
1200
1201 /**
1202 * Show commit logs from `HEAD` to the first commit.
1203 * If provided between `options.from` and `options.to` tags or branch.
1204 *
1205 * Additionally you can provide options.file, which is the path to a file in your repository. Then only this file will be considered.
1206 *
1207 * To use a custom splitter in the log format, set `options.splitter` to be the string the log should be split on.
1208 *
1209 * Options can also be supplied as a standard options object for adding custom properties supported by the git log command.
1210 * For any other set of options, supply options as an array of strings to be appended to the git log command.
1211 *
1212 * @param {Object|string[]} [options]
1213 * @param {string} [options.from] The first commit to include
1214 * @param {string} [options.to] The most recent commit to include
1215 * @param {string} [options.file] A single file to include in the result
1216 *
1217 * @param {Function} [then]
1218 */
1219 Git.prototype.log = function (options, then) {
1220 var handler = Git.trailingFunctionArgument(arguments);
1221 var opt = (handler === then ? options : null) || {};
1222
1223 var splitter = opt.splitter || ';';
1224 var format = opt.format || {
1225 hash: '%H',
1226 date: '%ai',
1227 message: '%s%d',
1228 author_name: '%aN',
1229 author_email: '%ae'
1230 };
1231
1232 var fields = Object.keys(format);
1233 var formatstr = fields.map(function (k) {
1234 return format[k];
1235 }).join(splitter);
1236 var command = ["log", "--pretty=format:" + formatstr + require('./responses/ListLogSummary').COMMIT_BOUNDARY];
1237
1238 if (Array.isArray(opt)) {
1239 command = command.concat(opt);
1240 opt = {};
1241 }
1242 else if (typeof arguments[0] === "string" || typeof arguments[1] === "string") {
1243 this._getLog('warn',
1244 'Git#log: supplying to or from as strings is now deprecated, switch to an options configuration object');
1245 opt = {
1246 from: arguments[0],
1247 to: arguments[1]
1248 };
1249 }
1250
1251 if (opt.n || opt['max-count']) {
1252 command.push("--max-count=" + (opt.n || opt['max-count']));
1253 }
1254
1255 if (opt.from && opt.to) {
1256 command.push(opt.from + "..." + opt.to);
1257 }
1258
1259 if (opt.file) {
1260 command.push("--follow", options.file);
1261 }
1262
1263 'splitter n max-count file from to --pretty format'.split(' ').forEach(function (key) {
1264 delete opt[key];
1265 });
1266
1267 Git._appendOptions(command, opt);
1268
1269 return this._run(command, Git._responseHandler(handler, 'ListLogSummary', [splitter, fields]));
1270 };
1271
1272 /**
1273 * Clears the queue of pending commands and returns the wrapper instance for chaining.
1274 *
1275 * @returns {Git}
1276 */
1277 Git.prototype.clearQueue = function () {
1278 this._runCache.length = 0;
1279 return this;
1280 };
1281
1282 /**
1283 * Check if a pathname or pathnames are excluded by .gitignore
1284 *
1285 * @param {string|string[]} pathnames
1286 * @param {Function} [then]
1287 */
1288 Git.prototype.checkIgnore = function (pathnames, then) {
1289 var handler = Git.trailingFunctionArgument(arguments);
1290 var command = ["check-ignore"];
1291
1292 if (handler !== pathnames) {
1293 command = command.concat(pathnames);
1294 }
1295
1296 return this._run(command, function (err, data) {
1297 handler && handler(err, !err && this._parseCheckIgnore(data));
1298 });
1299 };
1300
1301 /**
1302 * Validates that the current repo is a Git repo.
1303 *
1304 * @param {Function} [then]
1305 */
1306 Git.prototype.checkIsRepo = function (then) {
1307 function onError (exitCode, stdErr, done, fail) {
1308 if (exitCode === 128 && /Not a git repository/i.test(stdErr)) {
1309 return done(false);
1310 }
1311
1312 fail(stdErr);
1313 }
1314
1315 function handler (err, isRepo) {
1316 then && then(err, String(isRepo).trim() === 'true');
1317 }
1318
1319 return this._run(['rev-parse', '--is-inside-work-tree'], handler, {onError: onError});
1320 };
1321
1322 Git.prototype._rm = function (_files, options, then) {
1323 var files = [].concat(_files);
1324 var args = ['rm', options];
1325 args.push.apply(args, files);
1326
1327 return this._run(args, function (err) {
1328 then && then(err);
1329 });
1330 };
1331
1332 Git.prototype._parseCheckout = function (checkout) {
1333 // TODO
1334 };
1335
1336 /**
1337 * Parser for the `check-ignore` command - returns each
1338 * @param {string} [files]
1339 * @returns {string[]}
1340 */
1341 Git.prototype._parseCheckIgnore = function (files) {
1342 return files.split(/\n/g).filter(Boolean).map(function (file) {
1343 return file.trim()
1344 });
1345 };
1346
1347 /**
1348 * Schedules the supplied command to be run, the command should not include the name of the git binary and should
1349 * be an array of strings passed as the arguments to the git binary.
1350 *
1351 * @param {string[]} command
1352 * @param {Function} then
1353 * @param {Object} [opt]
1354 * @param {boolean} [opt.concatStdErr=false] Optionally concatenate stderr output into the stdout
1355 * @param {boolean} [opt.format="utf-8"] The format to use when reading the content of stdout
1356 * @param {Function} [opt.onError] Optional error handler for this command - can be used to allow non-clean exits
1357 * without killing the remaining stack of commands
1358 * @param {number} [opt.onError.exitCode]
1359 * @param {string} [opt.onError.stdErr]
1360 *
1361 * @returns {Git}
1362 */
1363 Git.prototype._run = function (command, then, opt) {
1364 if (typeof command === "string") {
1365 command = command.split(" ");
1366 }
1367 this._runCache.push([command, then, opt || {}]);
1368 this._schedule();
1369
1370 return this;
1371 };
1372
1373 Git.prototype._schedule = function () {
1374 if (!this._childProcess && this._runCache.length) {
1375 var git = this;
1376 var Buffer = git.Buffer;
1377 var task = git._runCache.shift();
1378
1379 var command = task[0];
1380 var then = task[1];
1381 var options = task[2];
1382
1383 debug(command);
1384
1385 var result = deferred();
1386 var stdOut = [];
1387 var stdErr = [];
1388 var spawned = git.ChildProcess.spawn(git._command, command.slice(0), {
1389 cwd: git._baseDir,
1390 env: git._env
1391 });
1392
1393 spawned.stdout.on('data', function (buffer) {
1394 stdOut.push(buffer);
1395 });
1396
1397 spawned.stderr.on('data', function (buffer) {
1398 stdErr.push(buffer);
1399 });
1400
1401 spawned.on('error', function (err) {
1402 stdErr.push(new Buffer(err.stack, 'ascii'));
1403 });
1404
1405 spawned.on('close', result.resolve);
1406
1407 spawned.on('exit', result.resolve);
1408
1409 result.promise.then(function (exitCode) {
1410 function done (output) {
1411 then.call(git, null, output);
1412 }
1413
1414 function fail (error) {
1415 Git.fail(git, error, then);
1416 }
1417
1418 delete git._childProcess;
1419
1420 if (exitCode && stdErr.length && options.onError) {
1421 options.onError(exitCode, Buffer.concat(stdErr).toString('utf-8'), done, fail);
1422 }
1423 else if (exitCode && stdErr.length) {
1424 fail(Buffer.concat(stdErr).toString('utf-8'));
1425 }
1426 else {
1427 if (options.concatStdErr) {
1428 [].push.apply(stdOut, stdErr);
1429 }
1430
1431 var stdOutput = Buffer.concat(stdOut);
1432 if (options.format !== 'buffer') {
1433 stdOutput = stdOutput.toString(options.format || 'utf-8');
1434 }
1435
1436 done(stdOutput);
1437 }
1438
1439 process.nextTick(git._schedule.bind(git));
1440 });
1441
1442 git._childProcess = spawned;
1443
1444 if (git._outputHandler) {
1445 git._outputHandler(command[0], git._childProcess.stdout, git._childProcess.stderr);
1446 }
1447 }
1448 };
1449
1450 /**
1451 * Handles an exception in the processing of a command.
1452 */
1453 Git.fail = function (git, error, handler) {
1454 git._getLog('error', error);
1455 git._runCache.length = 0;
1456 if (typeof handler === 'function') {
1457 handler.call(git, error, null);
1458 }
1459 };
1460
1461 /**
1462 * Given any number of arguments, returns the last argument if it is a function, otherwise returns null.
1463 * @returns {Function|null}
1464 */
1465 Git.trailingFunctionArgument = function (args) {
1466 var trailing = args[args.length - 1];
1467 return (typeof trailing === "function") ? trailing : null;
1468 };
1469
1470 /**
1471 * Given any number of arguments, returns the trailing options argument, ignoring a trailing function argument
1472 * if there is one. When not found, the return value is null.
1473 * @returns {Object|null}
1474 */
1475 Git.trailingOptionsArgument = function (args) {
1476 var options = args[(args.length - (Git.trailingFunctionArgument(args) ? 2 : 1))];
1477 return Object.prototype.toString.call(options) === '[object Object]' ? options : null;
1478 };
1479
1480 /**
1481 * Given any number of arguments, returns the trailing options array argument, ignoring a trailing function argument
1482 * if there is one. When not found, the return value is an empty array.
1483 * @returns {Array}
1484 */
1485 Git.trailingArrayArgument = function (args) {
1486 var options = args[(args.length - (Git.trailingFunctionArgument(args) ? 2 : 1))];
1487 return Object.prototype.toString.call(options) === '[object Array]' ? options : [];
1488 };
1489
1490 /**
1491 * Mutates the supplied command array by merging in properties in the options object. When the
1492 * value of the item in the options object is a string it will be concatenated to the key as
1493 * a single `name=value` item, otherwise just the name will be used.
1494 *
1495 * @param {string[]} command
1496 * @param {Object} options
1497 * @private
1498 */
1499 Git._appendOptions = function (command, options) {
1500 if (options === null) {
1501 return;
1502 }
1503
1504 Object.keys(options).forEach(function (key) {
1505 var value = options[key];
1506 if (typeof value === 'string') {
1507 command.push(key + '=' + value);
1508 }
1509 else {
1510 command.push(key);
1511 }
1512 });
1513 };
1514
1515 /**
1516 * Given the type of response and the callback to receive the parsed response,
1517 * uses the correct parser and calls back the callback.
1518 *
1519 * @param {Function} callback
1520 * @param {string} type
1521 * @param {Object[]} [args]
1522 *
1523 * @private
1524 */
1525 Git._responseHandler = function (callback, type, args) {
1526 return function (error, data) {
1527 if (typeof callback !== 'function') {
1528 return;
1529 }
1530
1531 if (error) {
1532 callback(error, null);
1533 return;
1534 }
1535
1536 var handler = require('./responses/' + type);
1537 var result = handler.parse.apply(handler, [data].concat(args === undefined ? [] : args));
1538
1539 callback(null, result);
1540 };
1541
1542 };
1543
1544 /**
1545 * Marks the git instance as having had a fatal exception by clearing the pending queue of tasks and
1546 * logging to the console.
1547 *
1548 * @param git
1549 * @param error
1550 * @param callback
1551 */
1552 Git.exception = function (git, error, callback) {
1553 git._runCache.length = 0;
1554 if (typeof callback === 'function') {
1555 callback(error instanceof Error ? error : new Error(error));
1556 }
1557
1558 git._getLog('error', error);
1559 };
1560
1561 module.exports = Git;
1562
1563}());