280 lines
7.1 KiB
JavaScript
280 lines
7.1 KiB
JavaScript
const cp = require('child_process');
|
|
const path = require('path');
|
|
const util = require('util');
|
|
const fs = require('fs-extra');
|
|
|
|
/**
|
|
* @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;
|