| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- const cp = require('child_process');
- const fs = require('fs-extra');
- const path = require('path');
- const util = require('util');
- /**
- * @function Object() { [native code] }
- * @param {number} code Error code.
- * @param {string} message Error message.
- */
- function ProcessError(code, message) {
- const callee = arguments.callee;
- Error.apply(this, [message]);
- Error.captureStackTrace(this, callee);
- this.code = code;
- this.message = message;
- this.name = callee.name;
- }
- util.inherits(ProcessError, Error);
- /**
- * Util function for handling spawned processes as promises.
- * @param {string} exe Executable.
- * @param {Array<string>} args Arguments.
- * @param {string} cwd Working directory.
- * @return {Promise} A promise.
- */
- function spawn(exe, args, cwd) {
- return new Promise((resolve, reject) => {
- const child = cp.spawn(exe, args, {cwd: cwd || process.cwd()});
- const buffer = [];
- child.stderr.on('data', (chunk) => {
- buffer.push(chunk.toString());
- });
- child.stdout.on('data', (chunk) => {
- buffer.push(chunk.toString());
- });
- child.on('close', (code) => {
- const output = buffer.join('');
- if (code) {
- const msg = output || 'Process failed: ' + code;
- reject(new ProcessError(code, msg));
- } else {
- resolve(output);
- }
- });
- });
- }
- /**
- * Create an object for executing git commands.
- * @param {string} cwd Repository directory.
- * @param {string} cmd Git executable (full path if not already on path).
- * @function Object() { [native code] }
- */
- function Git(cwd, cmd) {
- this.cwd = cwd;
- this.cmd = cmd || 'git';
- this.output = '';
- }
- /**
- * Execute an arbitrary git command.
- * @param {Array<string>} args Arguments (e.g. ['remote', 'update']).
- * @return {Promise} A promise. The promise will be resolved with this instance
- * or rejected with an error.
- */
- Git.prototype.exec = function (...args) {
- return spawn(this.cmd, [...args], this.cwd).then((output) => {
- this.output = output;
- return this;
- });
- };
- /**
- * Initialize repository.
- * @return {Promise} A promise.
- */
- Git.prototype.init = function () {
- return this.exec('init');
- };
- /**
- * Clean up unversioned files.
- * @return {Promise} A promise.
- */
- Git.prototype.clean = function () {
- return this.exec('clean', '-f', '-d');
- };
- /**
- * Hard reset to remote/branch
- * @param {string} remote Remote alias.
- * @param {string} branch Branch name.
- * @return {Promise} A promise.
- */
- Git.prototype.reset = function (remote, branch) {
- return this.exec('reset', '--hard', remote + '/' + branch);
- };
- /**
- * Fetch from a remote.
- * @param {string} remote Remote alias.
- * @return {Promise} A promise.
- */
- Git.prototype.fetch = function (remote) {
- return this.exec('fetch', remote);
- };
- /**
- * Checkout a branch (create an orphan if it doesn't exist on the remote).
- * @param {string} remote Remote alias.
- * @param {string} branch Branch name.
- * @return {Promise} A promise.
- */
- Git.prototype.checkout = function (remote, branch) {
- const treeish = remote + '/' + branch;
- return this.exec('ls-remote', '--exit-code', '.', treeish).then(
- () => {
- // branch exists on remote, hard reset
- return this.exec('checkout', branch)
- .then(() => this.clean())
- .then(() => this.reset(remote, branch));
- },
- (error) => {
- if (error instanceof ProcessError && error.code === 2) {
- // branch doesn't exist, create an orphan
- return this.exec('checkout', '--orphan', branch);
- } else {
- // unhandled error
- throw error;
- }
- }
- );
- };
- /**
- * Remove all unversioned files.
- * @param {string | Array<string>} files Files argument.
- * @return {Promise} A promise.
- */
- Git.prototype.rm = function (files) {
- if (!Array.isArray(files)) {
- files = [files];
- }
- return this.exec('rm', '--ignore-unmatch', '-r', '-f', ...files);
- };
- /**
- * Add files.
- * @param {string | Array<string>} files Files argument.
- * @return {Promise} A promise.
- */
- Git.prototype.add = function (files) {
- if (!Array.isArray(files)) {
- files = [files];
- }
- return this.exec('add', ...files);
- };
- /**
- * Commit (if there are any changes).
- * @param {string} message Commit message.
- * @return {Promise} A promise.
- */
- Git.prototype.commit = function (message) {
- return this.exec('diff-index', '--quiet', 'HEAD').catch(() =>
- this.exec('commit', '-m', message)
- );
- };
- /**
- * Add tag
- * @param {string} name Name of tag.
- * @return {Promise} A promise.
- */
- Git.prototype.tag = function (name) {
- return this.exec('tag', name);
- };
- /**
- * Push a branch.
- * @param {string} remote Remote alias.
- * @param {string} branch Branch name.
- * @param {boolean} force Force push.
- * @return {Promise} A promise.
- */
- Git.prototype.push = function (remote, branch, force) {
- const args = ['push', '--tags', remote, branch];
- if (force) {
- args.push('--force');
- }
- return this.exec.apply(this, args);
- };
- /**
- * Get the URL for a remote.
- * @param {string} remote Remote alias.
- * @return {Promise<string>} A promise for the remote URL.
- */
- Git.prototype.getRemoteUrl = function (remote) {
- return this.exec('config', '--get', 'remote.' + remote + '.url')
- .then((git) => {
- const repo = git.output && git.output.split(/[\n\r]/).shift();
- if (repo) {
- return repo;
- } else {
- throw new Error(
- 'Failed to get repo URL from options or current directory.'
- );
- }
- })
- .catch((err) => {
- throw new Error(
- 'Failed to get remote.' +
- remote +
- '.url (task must either be ' +
- 'run in a git repository with a configured ' +
- remote +
- ' remote ' +
- 'or must be configured with the "repo" option).'
- );
- });
- };
- /**
- * Delete ref to remove branch history
- * @param {string} branch The branch name.
- * @return {Promise} A promise. The promise will be resolved with this instance
- * or rejected with an error.
- */
- Git.prototype.deleteRef = function (branch) {
- return this.exec('update-ref', '-d', 'refs/heads/' + branch);
- };
- /**
- * Clone a repo into the given dir if it doesn't already exist.
- * @param {string} repo Repository URL.
- * @param {string} dir Target directory.
- * @param {string} branch Branch name.
- * @param {options} options All options.
- * @return {Promise<Git>} A promise.
- */
- Git.clone = function clone(repo, dir, branch, options) {
- return fs.exists(dir).then((exists) => {
- if (exists) {
- return Promise.resolve(new Git(dir, options.git));
- } else {
- return fs.mkdirp(path.dirname(path.resolve(dir))).then(() => {
- const args = [
- 'clone',
- repo,
- dir,
- '--branch',
- branch,
- '--single-branch',
- '--origin',
- options.remote,
- '--depth',
- options.depth,
- ];
- return spawn(options.git, args)
- .catch((err) => {
- // try again without branch or depth options
- return spawn(options.git, [
- 'clone',
- repo,
- dir,
- '--origin',
- options.remote,
- ]);
- })
- .then(() => new Git(dir, options.git));
- });
- }
- });
- };
- module.exports = Git;
|