(function () { 'use strict'; var debug = require('debug')('simple-git'); var deferred = require('./util/deferred'); var exists = require('./util/exists'); var NOOP = function () {}; var responses = require('./responses'); /** * Git handling for node. All public functions can be chained and all `then` handlers are optional. * * @param {string} baseDir base directory for all processes to run * * @param {Object} ChildProcess The ChildProcess module * @param {Function} Buffer The Buffer implementation to use * * @constructor */ function Git (baseDir, ChildProcess, Buffer) { this._baseDir = baseDir; this._runCache = []; this.ChildProcess = ChildProcess; this.Buffer = Buffer; } /** * @type {string} The command to use to reference the git binary */ Git.prototype._command = 'git'; /** * @type {[key: string]: string} An object of key=value pairs to be passed as environment variables to the * spawned child process. */ Git.prototype._env = null; /** * @type {Function} An optional handler to use when a child process is created */ Git.prototype._outputHandler = null; /** * @type {boolean} Property showing whether logging will be silenced - defaults to true in a production environment */ Git.prototype._silentLogging = /prod/.test(process.env.NODE_ENV); /** * Sets the path to a custom git binary, should either be `git` when there is an installation of git available on * the system path, or a fully qualified path to the executable. * * @param {string} command * @returns {Git} */ Git.prototype.customBinary = function (command) { this._command = command; return this; }; /** * Sets an environment variable for the spawned child process, either supply both a name and value as strings or * a single object to entirely replace the current environment variables. * * @param {string|Object} name * @param {string} [value] * @returns {Git} */ Git.prototype.env = function (name, value) { if (arguments.length === 1 && typeof name === 'object') { this._env = name; } else { (this._env = this._env || {})[name] = value; } return this; }; /** * Sets the working directory of the subsequent commands. * * @param {string} workingDirectory * @param {Function} [then] * @returns {Git} */ Git.prototype.cwd = function (workingDirectory, then) { var git = this; var next = Git.trailingFunctionArgument(arguments); return this.exec(function () { git._baseDir = workingDirectory; if (!exists(workingDirectory, exists.FOLDER)) { Git.exception(git, 'Git.cwd: cannot change to non-directory "' + workingDirectory + '"', next); } else { next && next(null, workingDirectory); } }); }; /** * Sets a handler function to be called whenever a new child process is created, the handler function will be called * with the name of the command being run and the stdout & stderr streams used by the ChildProcess. * * @example * require('simple-git') * .outputHandler(function (command, stdout, stderr) { * stdout.pipe(process.stdout); * }) * .checkout('https://github.com/user/repo.git'); * * @see https://nodejs.org/api/child_process.html#child_process_class_childprocess * @see https://nodejs.org/api/stream.html#stream_class_stream_readable * @param {Function} outputHandler * @returns {Git} */ Git.prototype.outputHandler = function (outputHandler) { this._outputHandler = outputHandler; return this; }; /** * Initialize a git repo * * @param {Boolean} [bare=false] * @param {Function} [then] */ Git.prototype.init = function (bare, then) { var commands = ['init']; var next = Git.trailingFunctionArgument(arguments); if (bare === true) { commands.push('--bare'); } return this._run(commands, function (err) { next && next(err); }); }; /** * Check the status of the local repo * * @param {Function} [then] */ Git.prototype.status = function (then) { return this._run( ['status', '--porcelain', '-b', '-u'], Git._responseHandler(then, 'StatusSummary') ); }; /** * List the stash(s) of the local repo * * @param {Object|Array} [options] * @param {Function} [then] */ Git.prototype.stashList = function (options, then) { var handler = Git.trailingFunctionArgument(arguments); var opt = (handler === then ? options : null) || {}; var splitter = opt.splitter || requireResponseHandler('ListLogSummary').SPLITTER; var command = ["stash", "list", "--pretty=format:" + requireResponseHandler('ListLogSummary').START_BOUNDARY + "%H %ai %s%d %aN %ae".replace(/\s+/g, splitter) + requireResponseHandler('ListLogSummary').COMMIT_BOUNDARY ]; if (Array.isArray(opt)) { command = command.concat(opt); } return this._run(command, Git._responseHandler(handler, 'ListLogSummary', splitter) ); }; /** * Stash the local repo * * @param {Object|Array} [options] * @param {Function} [then] */ Git.prototype.stash = function (options, then) { var command = ['stash']; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); command.push.apply(command, Git.trailingArrayArgument(arguments)); return this._run(command, Git._responseHandler(Git.trailingFunctionArgument(arguments))); }; /** * Clone a git repo * * @param {string} repoPath * @param {string} localPath * @param {String[]} [options] Optional array of options to pass through to the clone command * @param {Function} [then] */ Git.prototype.clone = function (repoPath, localPath, options, then) { var next = Git.trailingFunctionArgument(arguments); var command = ['clone'].concat(Git.trailingArrayArgument(arguments)); for (var i = 0, iMax = arguments.length; i < iMax; i++) { if (typeof arguments[i] === 'string') { command.push(arguments[i]); } } return this._run(command, function (err, data) { next && next(err, data); }); }; /** * Mirror a git repo * * @param {string} repoPath * @param {string} localPath * @param {Function} [then] */ Git.prototype.mirror = function (repoPath, localPath, then) { return this.clone(repoPath, localPath, ['--mirror'], then); }; /** * Moves one or more files to a new destination. * * @see https://git-scm.com/docs/git-mv * * @param {string|string[]} from * @param {string} to * @param {Function} [then] */ Git.prototype.mv = function (from, to, then) { var handler = Git.trailingFunctionArgument(arguments); var command = [].concat(from); command.unshift('mv', '-v'); command.push(to); this._run(command, Git._responseHandler(handler, 'MoveSummary')) }; /** * Internally uses pull and tags to get the list of tags then checks out the latest tag. * * @param {Function} [then] */ Git.prototype.checkoutLatestTag = function (then) { var git = this; return this.pull(function () { git.tags(function (err, tags) { git.checkout(tags.latest, then); }); }); }; /** * Adds one or more files to source control * * @param {string|string[]} files * @param {Function} [then] */ Git.prototype.add = function (files, then) { return this._run(['add'].concat(files), function (err, data) { then && then(err); }); }; /** * Commits changes in the current working directory - when specific file paths are supplied, only changes on those * files will be committed. * * @param {string|string[]} message * @param {string|string[]} [files] * @param {Object} [options] * @param {Function} [then] */ Git.prototype.commit = function (message, files, options, then) { var handler = Git.trailingFunctionArgument(arguments); var command = ['commit']; [].concat(message).forEach(function (message) { command.push('-m', message); }); [].push.apply(command, [].concat(typeof files === "string" || Array.isArray(files) ? files : [])); Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); return this._run( command, Git._responseHandler(handler, 'CommitSummary') ); }; /** * Gets a function to be used for logging. * * @param {string} level * @param {string} [message] * * @returns {Function} * @private */ Git.prototype._getLog = function (level, message) { var log = this._silentLogging ? NOOP : console[level].bind(console); if (arguments.length > 1) { log(message); } return log; }; /** * Pull the updated contents of the current repo * * @param {string} [remote] When supplied must also include the branch * @param {string} [branch] When supplied must also include the remote * @param {Object} [options] Optionally include set of options to merge into the command * @param {Function} [then] */ Git.prototype.pull = function (remote, branch, options, then) { var command = ["pull"]; var handler = Git.trailingFunctionArgument(arguments); if (typeof remote === 'string' && typeof branch === 'string') { command.push(remote, branch); } Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); return this._run(command, Git._responseHandler(handler, 'PullSummary')); }; /** * Fetch the updated contents of the current repo. * * @example * .fetch('upstream', 'master') // fetches from master on remote named upstream * .fetch(function () {}) // runs fetch against default remote and branch and calls function * * @param {string} [remote] * @param {string} [branch] * @param {Function} [then] */ Git.prototype.fetch = function (remote, branch, then) { var command = ["fetch"]; var next = Git.trailingFunctionArgument(arguments); Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); if (typeof remote === 'string' && typeof branch === 'string') { command.push(remote, branch); } if (Array.isArray(remote)) { command = command.concat(remote); } return this._run( command, Git._responseHandler(next, 'FetchSummary'), { concatStdErr: true } ); }; /** * Disables/enables the use of the console for printing warnings and errors, by default messages are not shown in * a production environment. * * @param {boolean} silence * @returns {Git} */ Git.prototype.silent = function (silence) { this._silentLogging = !!silence; return this; }; /** * List all tags. When using git 2.7.0 or above, include an options object with `"--sort": "property-name"` to * sort the tags by that property instead of using the default semantic versioning sort. * * Note, supplying this option when it is not supported by your Git version will cause the operation to fail. * * @param {Object} [options] * @param {Function} [then] */ Git.prototype.tags = function (options, then) { var next = Git.trailingFunctionArgument(arguments); var command = ['-l']; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); var hasCustomSort = command.some(function (option) { return /^--sort=/.test(option); }); return this.tag( command, Git._responseHandler(next, 'TagList', [hasCustomSort]) ); }; /** * Rebases the current working copy. Options can be supplied either as an array of string parameters * to be sent to the `git rebase` command, or a standard options object. * * @param {Object|String[]} [options] * @param {Function} [then] * @returns {Git} */ Git.prototype.rebase = function (options, then) { var command = ['rebase']; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); command.push.apply(command, Git.trailingArrayArgument(arguments)); return this._run(command, Git._responseHandler(Git.trailingFunctionArgument(arguments))); }; /** * Reset a repo * * @param {string|string[]} [mode=soft] Either an array of arguments supported by the 'git reset' command, or the * string value 'soft' or 'hard' to set the reset mode. * @param {Function} [then] */ Git.prototype.reset = function (mode, then) { var command = ['reset']; var next = Git.trailingFunctionArgument(arguments); if (next === mode || typeof mode === 'string' || !mode) { var modeStr = ['mixed', 'soft', 'hard'].includes(mode) ? mode : 'soft'; command.push('--' + modeStr); } else if (Array.isArray(mode)) { command.push.apply(command, mode); } return this._run(command, function (err) { next && next(err || null); }); }; /** * Revert one or more commits in the local working copy * * @param {string} commit The commit to revert. Can be any hash, offset (eg: `HEAD~2`) or range (eg: `master~5..master~2`) * @param {Object} [options] Optional options object * @param {Function} [then] */ Git.prototype.revert = function (commit, options, then) { var next = Git.trailingFunctionArgument(arguments); var command = ['revert']; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); if (typeof commit !== 'string') { return this.exec(function () { next && next(new TypeError("Commit must be a string")); }); } command.push(commit); return this._run(command, function (err) { next && next(err || null); }); }; /** * Add a lightweight tag to the head of the current branch * * @param {string} name * @param {Function} [then] */ Git.prototype.addTag = function (name, then) { if (typeof name !== "string") { return this.exec(function () { then && then(new TypeError("Git.addTag requires a tag name")); }); } var command = [name]; return then ? this.tag(command, then) : this.tag(command); }; /** * Add an annotated tag to the head of the current branch * * @param {string} tagName * @param {string} tagMessage * @param {Function} [then] */ Git.prototype.addAnnotatedTag = function (tagName, tagMessage, then) { return this.tag(['-a', '-m', tagMessage, tagName], function (err) { then && then(err); }); }; /** * Check out a tag or revision, any number of additional arguments can be passed to the `git checkout` command * by supplying either a string or array of strings as the `what` parameter. * * @param {string|string[]} what One or more commands to pass to `git checkout` * @param {Function} [then] */ Git.prototype.checkout = function (what, then) { var command = ['checkout']; command = command.concat(what); return this._run(command, function (err, data) { then && then(err, !err && this._parseCheckout(data)); }); }; /** * Check out a remote branch * * @param {string} branchName name of branch * @param {string} startPoint (e.g origin/development) * @param {Function} [then] */ Git.prototype.checkoutBranch = function (branchName, startPoint, then) { return this.checkout(['-b', branchName, startPoint], then); }; /** * Check out a local branch * * @param {string} branchName of branch * @param {Function} [then] */ Git.prototype.checkoutLocalBranch = function (branchName, then) { return this.checkout(['-b', branchName], then); }; /** * Delete a local branch * * @param {string} branchName name of branch * @param {Function} [then] */ Git.prototype.deleteLocalBranch = function (branchName, then) { return this.branch(['-d', branchName], then); }; /** * List all branches * * @param {Object | string[]} [options] * @param {Function} [then] */ Git.prototype.branch = function (options, then) { var isDelete, responseHandler; var next = Git.trailingFunctionArgument(arguments); var command = ['branch']; command.push.apply(command, Git.trailingArrayArgument(arguments)); Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); if (!arguments.length || next === options) { command.push('-a'); } isDelete = ['-d', '-D', '--delete'].reduce(function (isDelete, flag) { return isDelete || command.indexOf(flag) > 0; }, false); if (command.indexOf('-v') < 0) { command.splice(1, 0, '-v'); } responseHandler = isDelete ? Git._responseHandler(next, 'BranchDeleteSummary', false) : Git._responseHandler(next, 'BranchSummary'); return this._run(command, responseHandler); }; /** * Return list of local branches * * @param {Function} [then] */ Git.prototype.branchLocal = function (then) { return this.branch(['-v'], then); }; /** * Add config to local git instance * * @param {string} key configuration key (e.g user.name) * @param {string} value for the given key (e.g your name) * @param {Function} [then] */ Git.prototype.addConfig = function (key, value, then) { return this._run(['config', '--local', key, value], function (err, data) { then && then(err, !err && data); }); }; /** * Executes any command against the git binary. * * @param {string[]|Object} commands * @param {Function} [then] * * @returns {Git} */ Git.prototype.raw = function (commands, then) { var command = []; if (Array.isArray(commands)) { command = commands.slice(0); } else { Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); } var next = Git.trailingFunctionArgument(arguments); if (!command.length) { return this.exec(function () { next && next(new Error('Raw: must supply one or more command to execute'), null); }); } return this._run(command, function (err, data) { next && next(err, !err && data || null); }); }; /** * Add a submodule * * @param {string} repo * @param {string} path * @param {Function} [then] */ Git.prototype.submoduleAdd = function (repo, path, then) { return this._run(['submodule', 'add', repo, path], function (err) { then && then(err); }); }; /** * Update submodules * * @param {string[]} [args] * @param {Function} [then] */ Git.prototype.submoduleUpdate = function (args, then) { if (typeof args === 'string') { this._getLog('warn', 'Git#submoduleUpdate: args should be supplied as an array of individual arguments'); } var next = Git.trailingFunctionArgument(arguments); var command = (args !== next) ? args : []; return this.subModule(['update'].concat(command), function (err, args) { next && next(err, args); }); }; /** * Initialize submodules * * @param {string[]} [args] * @param {Function} [then] */ Git.prototype.submoduleInit = function (args, then) { if (typeof args === 'string') { this._getLog('warn', 'Git#submoduleInit: args should be supplied as an array of individual arguments'); } var next = Git.trailingFunctionArgument(arguments); var command = (args !== next) ? args : []; return this.subModule(['init'].concat(command), function (err, args) { next && next(err, args); }); }; /** * Call any `git submodule` function with arguments passed as an array of strings. * * @param {string[]} options * @param {Function} [then] */ Git.prototype.subModule = function (options, then) { if (!Array.isArray(options)) { return this.exec(function () { then && then(new TypeError("Git.subModule requires an array of arguments")); }); } if (options[0] !== 'submodule') { options.unshift('submodule'); } return this._run(options, function (err, data) { then && then(err || null, err ? null : data); }); }; /** * List remote * * @param {string[]} [args] * @param {Function} [then] */ Git.prototype.listRemote = function (args, then) { var next = Git.trailingFunctionArgument(arguments); var data = next === args || args === undefined ? [] : args; if (typeof data === 'string') { this._getLog('warn', 'Git#listRemote: args should be supplied as an array of individual arguments'); } return this._run(['ls-remote'].concat(data), function (err, data) { next && next(err, data); }); }; /** * Adds a remote to the list of remotes. * * @param {string} remoteName Name of the repository - eg "upstream" * @param {string} remoteRepo Fully qualified SSH or HTTP(S) path to the remote repo * @param {Function} [then] * @returns {*} */ Git.prototype.addRemote = function (remoteName, remoteRepo, then) { return this._run(['remote', 'add', remoteName, remoteRepo], function (err) { then && then(err); }); }; /** * Removes an entry from the list of remotes. * * @param {string} remoteName Name of the repository - eg "upstream" * @param {Function} [then] * @returns {*} */ Git.prototype.removeRemote = function (remoteName, then) { return this._run(['remote', 'remove', remoteName], function (err) { then && then(err); }); }; /** * Gets the currently available remotes, setting the optional verbose argument to true includes additional * detail on the remotes themselves. * * @param {boolean} [verbose=false] * @param {Function} [then] */ Git.prototype.getRemotes = function (verbose, then) { var next = Git.trailingFunctionArgument(arguments); var args = verbose === true ? ['-v'] : []; return this.remote(args, function (err, data) { next && next(err, !err && function () { return data.trim().split('\n').filter(Boolean).reduce(function (remotes, remote) { var detail = remote.trim().split(/\s+/); var name = detail.shift(); if (!remotes[name]) { remotes[name] = remotes[remotes.length] = { name: name, refs: {} }; } if (detail.length) { remotes[name].refs[detail.pop().replace(/[^a-z]/g, '')] = detail.pop(); } return remotes; }, []).slice(0); }()); }); }; /** * Call any `git remote` function with arguments passed as an array of strings. * * @param {string[]} options * @param {Function} [then] */ Git.prototype.remote = function (options, then) { if (!Array.isArray(options)) { return this.exec(function () { then && then(new TypeError("Git.remote requires an array of arguments")); }); } if (options[0] !== 'remote') { options.unshift('remote'); } return this._run(options, function (err, data) { then && then(err || null, err ? null : data); }); }; /** * Merges from one branch to another, equivalent to running `git merge ${from} $[to}`, the `options` argument can * either be an array of additional parameters to pass to the command or null / omitted to be ignored. * * @param {string} from * @param {string} to * @param {string[]} [options] * @param {Function} [then] */ Git.prototype.mergeFromTo = function (from, to, options, then) { var commands = [ from, to ]; var callback = Git.trailingFunctionArgument(arguments); if (Array.isArray(options)) { commands = commands.concat(options); } return this.merge(commands, callback); }; /** * Runs a merge, `options` can be either an array of arguments * supported by the [`git merge`](https://git-scm.com/docs/git-merge) * or an options object. * * Conflicts during the merge result in an error response, * the response type whether it was an error or success will be a MergeSummary instance. * When successful, the MergeSummary has all detail from a the PullSummary * * @param {Object | string[]} [options] * @param {Function} [then] * @returns {*} * * @see ./responses/MergeSummary.js * @see ./responses/PullSummary.js */ Git.prototype.merge = function (options, then) { var self = this; var userHandler = Git.trailingFunctionArgument(arguments) || NOOP; var mergeHandler = function (err, mergeSummary) { if (!err && mergeSummary.failed) { return Git.fail(self, mergeSummary, userHandler); } userHandler(err, mergeSummary); }; var command = []; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); command.push.apply(command, Git.trailingArrayArgument(arguments)); if (command[0] !== 'merge') { command.unshift('merge'); } if (command.length === 1) { return this.exec(function () { then && then(new TypeError("Git.merge requires at least one option")); }); } return this._run(command, Git._responseHandler(mergeHandler, 'MergeSummary'), { concatStdErr: true }); }; /** * Call any `git tag` function with arguments passed as an array of strings. * * @param {string[]} options * @param {Function} [then] */ Git.prototype.tag = function (options, then) { var command = []; Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); command.push.apply(command, Git.trailingArrayArgument(arguments)); if (command[0] !== 'tag') { command.unshift('tag'); } return this._run(command, Git._responseHandler(Git.trailingFunctionArgument(arguments))); }; /** * Updates repository server info * * @param {Function} [then] */ Git.prototype.updateServerInfo = function (then) { return this._run(["update-server-info"], function (err, data) { then && then(err, !err && data); }); }; /** * Pushes the current committed changes to a remote, optionally specify the names of the remote and branch to use * when pushing. Supply multiple options as an array of strings in the first argument - see examples below. * * @param {string|string[]} [remote] * @param {string} [branch] * @param {Function} [then] */ Git.prototype.push = function (remote, branch, then) { var command = []; var handler = Git.trailingFunctionArgument(arguments); if (typeof remote === 'string' && typeof branch === 'string') { command.push(remote, branch); } if (Array.isArray(remote)) { command = command.concat(remote); } Git._appendOptions(command, Git.trailingOptionsArgument(arguments)); if (command[0] !== 'push') { command.unshift('push'); } return this._run(command, function (err, data) { handler && handler(err, !err && data); }); }; /** * Pushes the current tag changes to a remote which can be either a URL or named remote. When not specified uses the * default configured remote spec. * * @param {string} [remote] * @param {Function} [then] */ Git.prototype.pushTags = function (remote, then) { var command = ['push']; if (typeof remote === "string") { command.push(remote); } command.push('--tags'); then = typeof arguments[arguments.length - 1] === "function" ? arguments[arguments.length - 1] : null; return this._run(command, function (err, data) { then && then(err, !err && data); }); }; /** * Removes the named files from source control. * * @param {string|string[]} files * @param {Function} [then] */ Git.prototype.rm = function (files, then) { return this._rm(files, '-f', then); }; /** * Removes the named files from source control but keeps them on disk rather than deleting them entirely. To * completely remove the files, use `rm`. * * @param {string|string[]} files * @param {Function} [then] */ Git.prototype.rmKeepLocal = function (files, then) { return this._rm(files, '--cached', then); }; /** * Returns a list of objects in a tree based on commit hash. Passing in an object hash returns the object's content, * size, and type. * * Passing "-p" will instruct cat-file to determine the object type, and display its formatted contents. * * @param {string[]} [options] * @param {Function} [then] */ Git.prototype.catFile = function (options, then) { return this._catFile('utf-8', arguments); }; /** * Equivalent to `catFile` but will return the native `Buffer` of content from the git command's stdout. * * @param {string[]} options * @param then */ Git.prototype.binaryCatFile = function (options, then) { return this._catFile('buffer', arguments); }; Git.prototype._catFile = function (format, args) { var handler = Git.trailingFunctionArgument(args); var command = ['cat-file']; var options = args[0]; if (typeof options === 'string') { throw new TypeError('Git#catFile: options must be supplied as an array of strings'); } else if (Array.isArray(options)) { command.push.apply(command, options); } return this._run(command, function (err, data) { handler && handler(err, data); }, { format: format }); }; /** * Return repository changes. * * @param {string[]} [options] * @param {Function} [then] */ Git.prototype.diff = function (options, then) { var command = ['diff']; if (typeof options === 'string') { command[0] += ' ' + options; this._getLog('warn', 'Git#diff: supplying options as a single string is now deprecated, switch to an array of strings'); } else if (Array.isArray(options)) { command.push.apply(command, options); } if (typeof arguments[arguments.length - 1] === 'function') { then = arguments[arguments.length - 1]; } return this._run(command, function (err, data) { then && then(err, data); }); }; Git.prototype.diffSummary = function (options, then) { var next = Git.trailingFunctionArgument(arguments); var command = ['--stat=4096']; if (options && options !== next) { command.push.apply(command, [].concat(options)); } return this.diff(command, Git._responseHandler(next, 'DiffSummary')); }; /** * Wraps `git rev-parse`. Primarily used to convert friendly commit references (ie branch names) to SHA1 hashes. * * Options should be an array of string options compatible with the `git rev-parse` * * @param {string|string[]} [options] * @param {Function} [then] * * @see https://git-scm.com/docs/git-rev-parse */ Git.prototype.revparse = function (options, then) { var command = ['rev-parse']; if (typeof options === 'string') { command = command + ' ' + options; this._getLog('warn', 'Git#revparse: supplying options as a single string is now deprecated, switch to an array of strings'); } else if (Array.isArray(options)) { command.push.apply(command, options); } if (typeof arguments[arguments.length - 1] === 'function') { then = arguments[arguments.length - 1]; } return this._run(command, function (err, data) { then && then(err, err ? null : String(data).trim()); }); }; /** * Show various types of objects, for example the file at a certain commit * * @param {string[]} [options] * @param {Function} [then] */ Git.prototype.show = function (options, then) { var args = [].slice.call(arguments, 0); var handler = typeof args[args.length - 1] === "function" ? args.pop() : null; var command = ['show']; if (typeof options === 'string') { command = command + ' ' + options; this._getLog('warn', 'Git#show: supplying options as a single string is now deprecated, switch to an array of strings'); } else if (Array.isArray(options)) { command.push.apply(command, options); } return this._run(command, function (err, data) { handler && handler(err, !err && data); }); }; /** * @param {string} mode Required parameter "n" or "f" * @param {string[]} options * @param {Function} [then] */ Git.prototype.clean = function (mode, options, then) { var handler = Git.trailingFunctionArgument(arguments); if (typeof mode !== 'string' || !/[nf]/.test(mode)) { return this.exec(function () { handler && handler(new TypeError('Git clean mode parameter ("n" or "f") is required')); }); } if (/[^dfinqxX]/.test(mode)) { return this.exec(function () { handler && handler(new TypeError('Git clean unknown option found in ' + JSON.stringify(mode))); }); } var command = ['clean', '-' + mode]; if (Array.isArray(options)) { command = command.concat(options); } if (command.some(interactiveMode)) { return this.exec(function () { handler && handler(new TypeError('Git clean interactive mode is not supported')); }); } return this._run(command, function (err, data) { handler && handler(err, !err && data); }); function interactiveMode (option) { if (/^-[^\-]/.test(option)) { return option.indexOf('i') > 0; } return option === '--interactive'; } }; /** * Call a simple function at the next step in the chain. * @param {Function} [then] */ Git.prototype.exec = function (then) { this._run([], function () { typeof then === 'function' && then(); }); return this; }; /** * Deprecated means of adding a regular function call at the next step in the chain. Use the replacement * Git#exec, the Git#then method will be removed in version 2.x * * @see exec * @deprecated */ Git.prototype.then = function (then) { this._getLog( 'error', ` Git#then is deprecated after version 1.72 and will be removed in version 2.x To use promises switch to importing 'simple-git/promise'.`); return this.exec(then); }; /** * Show commit logs from `HEAD` to the first commit. * If provided between `options.from` and `options.to` tags or branch. * * Additionally you can provide options.file, which is the path to a file in your repository. Then only this file will be considered. * * To use a custom splitter in the log format, set `options.splitter` to be the string the log should be split on. * * Options can also be supplied as a standard options object for adding custom properties supported by the git log command. * For any other set of options, supply options as an array of strings to be appended to the git log command. * * @param {Object|string[]} [options] * @param {string} [options.from] The first commit to include * @param {string} [options.to] The most recent commit to include * @param {string} [options.file] A single file to include in the result * @param {boolean} [options.multiLine] Optionally include multi-line commit messages * * @param {Function} [then] */ Git.prototype.log = function (options, then) { var handler = Git.trailingFunctionArgument(arguments); var opt = (handler === then ? options : null) || {}; var splitter = opt.splitter || requireResponseHandler('ListLogSummary').SPLITTER; var format = opt.format || { hash: '%H', date: '%ai', message: '%s', refs: '%D', body: opt.multiLine ? '%B' : '%b', author_name: '%aN', author_email: '%ae' }; var rangeOperator = (opt.symmetric !== false) ? '...' : '..'; var fields = Object.keys(format); var formatstr = fields.map(function (k) { return format[k]; }).join(splitter); var suffix = []; var command = ["log", "--pretty=format:" + requireResponseHandler('ListLogSummary').START_BOUNDARY + formatstr + requireResponseHandler('ListLogSummary').COMMIT_BOUNDARY ]; if (Array.isArray(opt)) { command = command.concat(opt); opt = {}; } else if (typeof arguments[0] === "string" || typeof arguments[1] === "string") { this._getLog('warn', 'Git#log: supplying to or from as strings is now deprecated, switch to an options configuration object'); opt = { from: arguments[0], to: arguments[1] }; } if (opt.n || opt['max-count']) { command.push("--max-count=" + (opt.n || opt['max-count'])); } if (opt.from && opt.to) { command.push(opt.from + rangeOperator + opt.to); } if (opt.file) { suffix.push("--follow", options.file); } 'splitter n max-count file from to --pretty format symmetric multiLine'.split(' ').forEach(function (key) { delete opt[key]; }); Git._appendOptions(command, opt); return this._run( command.concat(suffix), Git._responseHandler(handler, 'ListLogSummary', [splitter, fields]) ); }; /** * Clears the queue of pending commands and returns the wrapper instance for chaining. * * @returns {Git} */ Git.prototype.clearQueue = function () { this._runCache.length = 0; return this; }; /** * Check if a pathname or pathnames are excluded by .gitignore * * @param {string|string[]} pathnames * @param {Function} [then] */ Git.prototype.checkIgnore = function (pathnames, then) { var handler = Git.trailingFunctionArgument(arguments); var command = ["check-ignore"]; if (handler !== pathnames) { command = command.concat(pathnames); } return this._run(command, function (err, data) { handler && handler(err, !err && this._parseCheckIgnore(data)); }); }; /** * Validates that the current repo is a Git repo. * * @param {Function} [then] */ Git.prototype.checkIsRepo = function (then) { function onError (exitCode, stdErr, done, fail) { if (exitCode === 128 && /(Not a git repository|Kein Git-Repository)/i.test(stdErr)) { return done(false); } fail(stdErr); } function handler (err, isRepo) { then && then(err, String(isRepo).trim() === 'true'); } return this._run(['rev-parse', '--is-inside-work-tree'], handler, {onError: onError}); }; Git.prototype._rm = function (_files, options, then) { var files = [].concat(_files); var args = ['rm', options]; args.push.apply(args, files); return this._run(args, function (err) { then && then(err); }); }; Git.prototype._parseCheckout = function (checkout) { // TODO }; /** * Parser for the `check-ignore` command - returns each * @param {string} [files] * @returns {string[]} */ Git.prototype._parseCheckIgnore = function (files) { return files.split(/\n/g).filter(Boolean).map(function (file) { return file.trim() }); }; /** * Schedules the supplied command to be run, the command should not include the name of the git binary and should * be an array of strings passed as the arguments to the git binary. * * @param {string[]} command * @param {Function} then * @param {Object} [opt] * @param {boolean} [opt.concatStdErr=false] Optionally concatenate stderr output into the stdout * @param {boolean} [opt.format="utf-8"] The format to use when reading the content of stdout * @param {Function} [opt.onError] Optional error handler for this command - can be used to allow non-clean exits * without killing the remaining stack of commands * @param {number} [opt.onError.exitCode] * @param {string} [opt.onError.stdErr] * * @returns {Git} */ Git.prototype._run = function (command, then, opt) { if (typeof command === "string") { command = command.split(" "); } this._runCache.push([command, then, opt || {}]); this._schedule(); return this; }; Git.prototype._schedule = function () { if (!this._childProcess && this._runCache.length) { var git = this; var Buffer = git.Buffer; var task = git._runCache.shift(); var command = task[0]; var then = task[1]; var options = task[2]; debug(command); var result = deferred(); var attempted = false; var attemptClose = function attemptClose (e) { // closing when there is content, terminate immediately if (attempted || stdErr.length || stdOut.length) { result.resolve(e); attempted = true; } // first attempt at closing but no content yet, wait briefly for the close/exit that may follow if (!attempted) { attempted = true; setTimeout(attemptClose.bind(this, e), 50); } }; var stdOut = []; var stdErr = []; var spawned = git.ChildProcess.spawn(git._command, command.slice(0), { cwd: git._baseDir, env: git._env, windowsHide: true }); spawned.stdout.on('data', function (buffer) { stdOut.push(buffer); }); spawned.stderr.on('data', function (buffer) { stdErr.push(buffer); }); spawned.on('error', function (err) { stdErr.push(Buffer.from(err.stack, 'ascii')); }); spawned.on('close', attemptClose); spawned.on('exit', attemptClose); result.promise.then(function (exitCode) { function done (output) { then.call(git, null, output); } function fail (error) { Git.fail(git, error, then); } delete git._childProcess; if (exitCode && stdErr.length && options.onError) { options.onError(exitCode, Buffer.concat(stdErr).toString('utf-8'), done, fail); } else if (exitCode && stdErr.length) { fail(Buffer.concat(stdErr).toString('utf-8')); } else { if (options.concatStdErr) { [].push.apply(stdOut, stdErr); } var stdOutput = Buffer.concat(stdOut); if (options.format !== 'buffer') { stdOutput = stdOutput.toString(options.format || 'utf-8'); } done(stdOutput); } process.nextTick(git._schedule.bind(git)); }); git._childProcess = spawned; if (git._outputHandler) { git._outputHandler(command[0], git._childProcess.stdout, git._childProcess.stderr); } } }; /** * Handles an exception in the processing of a command. */ Git.fail = function (git, error, handler) { git._getLog('error', error); git._runCache.length = 0; if (typeof handler === 'function') { handler.call(git, error, null); } }; /** * Given any number of arguments, returns the last argument if it is a function, otherwise returns null. * @returns {Function|null} */ Git.trailingFunctionArgument = function (args) { var trailing = args[args.length - 1]; return (typeof trailing === "function") ? trailing : null; }; /** * Given any number of arguments, returns the trailing options argument, ignoring a trailing function argument * if there is one. When not found, the return value is null. * @returns {Object|null} */ Git.trailingOptionsArgument = function (args) { var options = args[(args.length - (Git.trailingFunctionArgument(args) ? 2 : 1))]; return Object.prototype.toString.call(options) === '[object Object]' ? options : null; }; /** * Given any number of arguments, returns the trailing options array argument, ignoring a trailing function argument * if there is one. When not found, the return value is an empty array. * @returns {Array} */ Git.trailingArrayArgument = function (args) { var options = args[(args.length - (Git.trailingFunctionArgument(args) ? 2 : 1))]; return Object.prototype.toString.call(options) === '[object Array]' ? options : []; }; /** * Mutates the supplied command array by merging in properties in the options object. When the * value of the item in the options object is a string it will be concatenated to the key as * a single `name=value` item, otherwise just the name will be used. * * @param {string[]} command * @param {Object} options * @private */ Git._appendOptions = function (command, options) { if (options === null) { return; } Object.keys(options).forEach(function (key) { var value = options[key]; if (typeof value === 'string') { command.push(key + '=' + value); } else { command.push(key); } }); }; /** * Given the type of response and the callback to receive the parsed response, * uses the correct parser and calls back the callback. * * @param {Function} callback * @param {string} [type] * @param {Object[]} [args] * * @private */ Git._responseHandler = function (callback, type, args) { return function (error, data) { if (typeof callback !== 'function') { return; } if (error) { return callback(error, null); } if (!type) { return callback(null, data); } var handler = requireResponseHandler(type); var result = handler.parse.apply(handler, [data].concat(args === undefined ? [] : args)); callback(null, result); }; }; /** * Marks the git instance as having had a fatal exception by clearing the pending queue of tasks and * logging to the console. * * @param git * @param error * @param callback */ Git.exception = function (git, error, callback) { git._runCache.length = 0; if (typeof callback === 'function') { callback(error instanceof Error ? error : new Error(error)); } git._getLog('error', error); }; module.exports = Git; /** * Requires and returns a response handler based on its named type * @param {string} type */ function requireResponseHandler (type) { return responses[type]; } }());