const {GitExecutor} = require('./lib/runners/git-executor'); const {Scheduler} = require('./lib/runners/scheduler'); const {GitLogger} = require('./lib/git-logger'); const {adhocExecTask, configurationErrorTask} = require('./lib/tasks/task'); const {NOOP, appendTaskOptions, asArray, filterArray, filterPrimitives, filterString, filterStringOrStringArray, filterType, folderExists, getTrailingOptions, trailingFunctionArgument, trailingOptionsArgument} = require('./lib/utils'); const {applyPatchTask} = require('./lib/tasks/apply-patch') const {branchTask, branchLocalTask, deleteBranchesTask, deleteBranchTask} = require('./lib/tasks/branch'); const {taskCallback} = require('./lib/task-callback'); const {checkIsRepoTask} = require('./lib/tasks/check-is-repo'); const {cloneTask, cloneMirrorTask} = require('./lib/tasks/clone'); const {addConfigTask, listConfigTask} = require('./lib/tasks/config'); const {cleanWithOptionsTask, isCleanOptionsArray} = require('./lib/tasks/clean'); const {commitTask} = require('./lib/tasks/commit'); const {diffSummaryTask} = require('./lib/tasks/diff'); const {fetchTask} = require('./lib/tasks/fetch'); const {hashObjectTask} = require('./lib/tasks/hash-object'); const {initTask} = require('./lib/tasks/init'); const {logTask, parseLogOptions} = require('./lib/tasks/log'); const {mergeTask} = require('./lib/tasks/merge'); const {moveTask} = require("./lib/tasks/move"); const {pullTask} = require('./lib/tasks/pull'); const {pushTagsTask, pushTask} = require('./lib/tasks/push'); const {addRemoteTask, getRemotesTask, listRemotesTask, remoteTask, removeRemoteTask} = require('./lib/tasks/remote'); const {getResetMode, resetTask} = require('./lib/tasks/reset'); const {stashListTask} = require('./lib/tasks/stash-list'); const {statusTask} = require('./lib/tasks/status'); const {addSubModuleTask, initSubModuleTask, subModuleTask, updateSubModuleTask} = require('./lib/tasks/sub-module'); const {addAnnotatedTagTask, addTagTask, tagListTask} = require('./lib/tasks/tag'); const {straightThroughStringTask} = require('./lib/tasks/task'); const {parseCheckIgnore} = require('./lib/responses/CheckIgnore'); const ChainedExecutor = Symbol('ChainedExecutor'); /** * Git handling for node. All public functions can be chained and all `then` handlers are optional. * * @param {SimpleGitOptions} options Configuration settings for this instance * * @constructor */ function Git (options) { this._executor = new GitExecutor( options.binary, options.baseDir, new Scheduler(options.maxConcurrentProcesses) ); this._logger = new GitLogger(); } /** * The executor that runs each of the added commands * @type {GitExecutor} * @private */ Git.prototype._executor = null; /** * Logging utility for printing out info or error messages to the user * @type {GitLogger} * @private */ Git.prototype._logger = null; /** * 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._executor.binary = 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._executor.env = name; } else { (this._executor.env = this._executor.env || {})[name] = value; } return this; }; /** * Sets the working directory of the subsequent commands. */ Git.prototype.cwd = function (workingDirectory) { const task = (typeof workingDirectory !== 'string') ? configurationErrorTask('Git.cwd: workingDirectory must be supplied as a string') : adhocExecTask(() => { if (!folderExists(workingDirectory)) { throw new Error(`Git.cwd: cannot change to non-directory "${ workingDirectory }"`); } return (this._executor.cwd = workingDirectory); }); return this._runTask(task, trailingFunctionArgument(arguments) || NOOP); }; /** * 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._executor.outputHandler = outputHandler; return this; }; /** * Initialize a git repo * * @param {Boolean} [bare=false] * @param {Function} [then] */ Git.prototype.init = function (bare, then) { return this._runTask( initTask(bare === true, this._executor.cwd, getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * Check the status of the local repo */ Git.prototype.status = function () { return this._runTask( statusTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * List the stash(s) of the local repo */ Git.prototype.stashList = function (options) { return this._runTask( stashListTask( trailingOptionsArgument(arguments) || {}, filterArray(options) && options || [] ), trailingFunctionArgument(arguments), ); }; /** * Stash the local repo * * @param {Object|Array} [options] * @param {Function} [then] */ Git.prototype.stash = function (options, then) { return this._runTask( straightThroughStringTask(['stash', ...getTrailingOptions(arguments)]), trailingFunctionArgument(arguments), ); }; function createCloneTask (api, task, repoPath, localPath) { if (typeof repoPath !== 'string') { return configurationErrorTask(`git.${ api }() requires a string 'repoPath'`); } return task(repoPath, filterType(localPath, filterString), getTrailingOptions(arguments)); } /** * Clone a git repo */ Git.prototype.clone = function () { return this._runTask( createCloneTask('clone', cloneTask, ...arguments), trailingFunctionArgument(arguments), ); }; /** * Mirror a git repo */ Git.prototype.mirror = function () { return this._runTask( createCloneTask('mirror', cloneMirrorTask, ...arguments), trailingFunctionArgument(arguments), ); }; /** * Moves one or more files to a new destination. * * @see https://git-scm.com/docs/git-mv * * @param {string|string[]} from * @param {string} to */ Git.prototype.mv = function (from, to) { return this._runTask(moveTask(from, to), trailingFunctionArgument(arguments)); }; /** * 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 */ Git.prototype.add = function (files) { return this._run( ['add'].concat(files), trailingFunctionArgument(arguments), ); }; /** * 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) { const next = trailingFunctionArgument(arguments); const messages = []; if (filterStringOrStringArray(message)) { messages.push(...asArray(message)); } else { console.warn('simple-git deprecation notice: git.commit: requires the commit message to be supplied as a string/string[], this will be an error in version 3'); } return this._runTask( commitTask( messages, asArray(filterType(files, filterStringOrStringArray, [])), [...filterType(options, filterArray, []), ...getTrailingOptions(arguments, 0, true)] ), next ); }; /** * Pull the updated contents of the current repo */ Git.prototype.pull = function (remote, branch, options, then) { return this._runTask( pullTask(filterType(remote, filterString), filterType(branch, filterString), getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * 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] */ Git.prototype.fetch = function (remote, branch) { return this._runTask( fetchTask(filterType(remote, filterString), filterType(branch, filterString), getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * 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._logger.silent(!!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) { return this._runTask( tagListTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * 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. */ Git.prototype.rebase = function () { return this._run( ['rebase'].concat(getTrailingOptions(arguments)), 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) { return this._runTask( resetTask(getResetMode(mode), getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * Revert one or more commits in the local working copy */ Git.prototype.revert = function (commit) { const next = trailingFunctionArgument(arguments); if (typeof commit !== 'string') { return this._runTask( configurationErrorTask('Commit must be a string'), next, ); } return this._run([ 'revert', ...getTrailingOptions(arguments, 0, true), commit ], next); }; /** * Add a lightweight tag to the head of the current branch * * @param {string} name * @param {Function} [then] */ Git.prototype.addTag = function (name, then) { const task = (typeof name === 'string') ? addTagTask(name) : configurationErrorTask('Git.addTag requires a tag name'); return this._runTask(task, trailingFunctionArgument(arguments)); }; /** * 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._runTask( addAnnotatedTagTask(tagName, tagMessage), trailingFunctionArgument(arguments), ); }; /** * 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) { const commands = ['checkout', ...getTrailingOptions(arguments, true)]; return this._runTask( straightThroughStringTask(commands), trailingFunctionArgument(arguments), ); }; /** * 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], trailingFunctionArgument(arguments)); }; /** * Check out a local branch */ Git.prototype.checkoutLocalBranch = function (branchName, then) { return this.checkout(['-b', branchName], trailingFunctionArgument(arguments)); }; /** * Delete a local branch */ Git.prototype.deleteLocalBranch = function (branchName, forceDelete, then) { return this._runTask( deleteBranchTask(branchName, typeof forceDelete === "boolean" ? forceDelete : false), trailingFunctionArgument(arguments), ); }; /** * Delete one or more local branches */ Git.prototype.deleteLocalBranches = function (branchNames, forceDelete, then) { return this._runTask( deleteBranchesTask(branchNames, typeof forceDelete === "boolean" ? forceDelete : false), trailingFunctionArgument(arguments), ); }; /** * List all branches * * @param {Object | string[]} [options] * @param {Function} [then] */ Git.prototype.branch = function (options, then) { return this._runTask( branchTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * Return list of local branches * * @param {Function} [then] */ Git.prototype.branchLocal = function (then) { return this._runTask( branchLocalTask(), trailingFunctionArgument(arguments), ); }; /** * 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 {boolean} [append=false] optionally append the key/value pair (equivalent of passing `--add` option). * @param {Function} [then] */ Git.prototype.addConfig = function (key, value, append, then) { return this._runTask( addConfigTask(key, value, typeof append === "boolean" ? append : false), trailingFunctionArgument(arguments), ); }; Git.prototype.listConfig = function () { return this._runTask(listConfigTask(), trailingFunctionArgument(arguments)); }; /** * Executes any command against the git binary. */ Git.prototype.raw = function (commands) { const createRestCommands = !Array.isArray(commands); const command = [].slice.call(createRestCommands ? arguments : commands, 0); for (let i = 0; i < command.length && createRestCommands; i++) { if (!filterPrimitives(command[i])) { command.splice(i, command.length - i); break; } } command.push( ...getTrailingOptions(arguments, 0, true), ); var next = trailingFunctionArgument(arguments); if (!command.length) { return this._runTask( configurationErrorTask('Raw: must supply one or more command to execute'), next, ); } return this._run(command, next); }; Git.prototype.submoduleAdd = function (repo, path, then) { return this._runTask( addSubModuleTask(repo, path), trailingFunctionArgument(arguments), ); }; Git.prototype.submoduleUpdate = function (args, then) { return this._runTask( updateSubModuleTask(getTrailingOptions(arguments, true)), trailingFunctionArgument(arguments), ); }; Git.prototype.submoduleInit = function (args, then) { return this._runTask( initSubModuleTask(getTrailingOptions(arguments, true)), trailingFunctionArgument(arguments), ); }; Git.prototype.subModule = function (options, then) { return this._runTask( subModuleTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; Git.prototype.listRemote = function () { return this._runTask( listRemotesTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * Adds a remote to the list of remotes. */ Git.prototype.addRemote = function (remoteName, remoteRepo, then) { return this._runTask( addRemoteTask(remoteName, remoteRepo, getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * Removes an entry by name from the list of remotes. */ Git.prototype.removeRemote = function (remoteName, then) { return this._runTask( removeRemoteTask(remoteName), trailingFunctionArgument(arguments), ); }; /** * Gets the currently available remotes, setting the optional verbose argument to true includes additional * detail on the remotes themselves. */ Git.prototype.getRemotes = function (verbose, then) { return this._runTask( getRemotesTask(verbose === true), trailingFunctionArgument(arguments), ); }; /** * Compute object ID from a file */ Git.prototype.hashObject = function (path, write) { return this._runTask( hashObjectTask(path, write === true), trailingFunctionArgument(arguments), ); }; /** * 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) { return this._runTask( remoteTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * 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 */ Git.prototype.mergeFromTo = function (from, to) { if (!(filterString(from) && filterString(to))) { return this._runTask(configurationErrorTask( `Git.mergeFromTo requires that the 'from' and 'to' arguments are supplied as strings` )); } return this._runTask( mergeTask([from, to, ...getTrailingOptions(arguments)]), trailingFunctionArgument(arguments, false), ); }; /** * 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 () { return this._runTask( mergeTask(getTrailingOptions(arguments)), trailingFunctionArgument(arguments), ); }; /** * 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) { const command = getTrailingOptions(arguments); if (command[0] !== 'tag') { command.unshift('tag'); } return this._run(command, trailingFunctionArgument(arguments)); }; /** * Updates repository server info * * @param {Function} [then] */ Git.prototype.updateServerInfo = function (then) { return this._run(["update-server-info"], trailingFunctionArgument(arguments)); }; /** * 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. */ Git.prototype.push = function (remote, branch, then) { const task = pushTask( {remote: filterType(remote, filterString), branch: filterType(branch, filterString)}, getTrailingOptions(arguments), ); return this._runTask(task, trailingFunctionArgument(arguments)); }; /** * 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) { const task = pushTagsTask({remote: filterType(remote, filterString)}, getTrailingOptions(arguments)); return this._runTask(task, trailingFunctionArgument(arguments)); }; /** * 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 = trailingFunctionArgument(args); var command = ['cat-file']; var options = args[0]; if (typeof options === 'string') { return this._runTask( configurationErrorTask('Git#catFile: options must be supplied as an array of strings'), handler, ); } if (Array.isArray(options)) { command.push.apply(command, options); } return this._run(command, handler, { format: format }); }; Git.prototype.diff = function (options, then) { const command = ['diff', ...getTrailingOptions(arguments)]; if (typeof options === 'string') { command.splice(1, 0, options); this._logger.warn('Git#diff: supplying options as a single string is now deprecated, switch to an array of strings'); } return this._runTask( straightThroughStringTask(command), trailingFunctionArgument(arguments), ); }; Git.prototype.diffSummary = function () { return this._runTask( diffSummaryTask(getTrailingOptions(arguments, 1)), trailingFunctionArgument(arguments), ); }; Git.prototype.applyPatch = function(patches) { const task = !filterStringOrStringArray(patches) ? configurationErrorTask(`git.applyPatch requires one or more string patches as the first argument`) : applyPatchTask(asArray(patches), getTrailingOptions([].slice.call(arguments, 1))); return this._runTask( task, trailingFunctionArgument(arguments), ); } Git.prototype.revparse = function () { const commands = ['rev-parse', ...getTrailingOptions(arguments, true)]; return this._runTask( straightThroughStringTask(commands, true), trailingFunctionArgument(arguments), ); }; /** * 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) { return this._runTask( straightThroughStringTask(['show', ...getTrailingOptions(arguments, 1)]), trailingFunctionArgument(arguments) ); }; /** */ Git.prototype.clean = function (mode, options, then) { const usingCleanOptionsArray = isCleanOptionsArray(mode); const cleanMode = usingCleanOptionsArray && mode.join('') || filterType(mode, filterString) || ''; const customArgs = getTrailingOptions([].slice.call(arguments, usingCleanOptionsArray ? 1 : 0)); return this._runTask( cleanWithOptionsTask(cleanMode, customArgs), trailingFunctionArgument(arguments), ); }; /** * Call a simple function at the next step in the chain. * @param {Function} [then] */ Git.prototype.exec = function (then) { const task = { commands: [], format: 'utf-8', parser () { if (typeof then === 'function') { then(); } } }; return this._runTask(task); }; /** * 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. */ Git.prototype.log = function (options) { const next = trailingFunctionArgument(arguments); if (filterString(arguments[0]) && filterString(arguments[1])) { return this._runTask( configurationErrorTask(`git.log(string, string) should be replaced with git.log({ from: string, to: string })`), next ); } const parsedOptions = parseLogOptions( trailingOptionsArgument(arguments) || {}, filterArray(options) && options || [] ); return this._runTask( logTask(parsedOptions.splitter, parsedOptions.fields, parsedOptions.commands), next, ) }; /** * Clears the queue of pending commands and returns the wrapper instance for chaining. * * @returns {Git} */ Git.prototype.clearQueue = function () { // TODO: // this._executor.clear(); 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) { return this._run( ["check-ignore", ...asArray((filterType(pathnames, filterStringOrStringArray, [])))], trailingFunctionArgument(arguments), { parser: parseCheckIgnore } ) }; Git.prototype.checkIsRepo = function (checkType, then) { return this._runTask( checkIsRepoTask(filterType(checkType, filterString)), trailingFunctionArgument(arguments), ); }; Git.prototype._rm = function (_files, options, then) { var files = [].concat(_files); var args = ['rm', options]; args.push.apply(args, files); return this._run(args, trailingFunctionArgument(arguments)); }; /** * 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 {Function} [opt.parser] Optional parser function * @param {number} [opt.onError.exitCode] * @param {string} [opt.onError.stdErr] * * @returns {Git} */ Git.prototype._run = function (command, then, opt) { const task = Object.assign({ concatStdErr: false, onError: undefined, format: 'utf-8', parser (data) { return data; } }, opt || {}, { commands: command, }); return this._runTask(task, then); }; Git.prototype._runTask = function (task, then) { const executor = this[ChainedExecutor] || this._executor.chain(); const promise = executor.push(task); taskCallback( task, promise, then); return Object.create(this, { then: {value: promise.then.bind(promise)}, catch: {value: promise.catch.bind(promise)}, [ChainedExecutor]: {value: executor}, }); }; module.exports = Git;