UNPKG

7.28 kBJavaScriptView Raw
1const cp = require('child_process');
2const fs = require('fs-extra');
3const path = require('path');
4const util = require('util');
5
6/**
7 * @function Object() { [native code] }
8 * @param {number} code Error code.
9 * @param {string} message Error message.
10 */
11function ProcessError(code, message) {
12 const callee = arguments.callee;
13 Error.apply(this, [message]);
14 Error.captureStackTrace(this, callee);
15 this.code = code;
16 this.message = message;
17 this.name = callee.name;
18}
19util.inherits(ProcessError, Error);
20
21/**
22 * Util function for handling spawned processes as promises.
23 * @param {string} exe Executable.
24 * @param {Array<string>} args Arguments.
25 * @param {string} cwd Working directory.
26 * @return {Promise} A promise.
27 */
28function spawn(exe, args, cwd) {
29 return new Promise((resolve, reject) => {
30 const child = cp.spawn(exe, args, {cwd: cwd || process.cwd()});
31 const buffer = [];
32 child.stderr.on('data', (chunk) => {
33 buffer.push(chunk.toString());
34 });
35 child.stdout.on('data', (chunk) => {
36 buffer.push(chunk.toString());
37 });
38 child.on('close', (code) => {
39 const output = buffer.join('');
40 if (code) {
41 const msg = output || 'Process failed: ' + code;
42 reject(new ProcessError(code, msg));
43 } else {
44 resolve(output);
45 }
46 });
47 });
48}
49
50/**
51 * Create an object for executing git commands.
52 * @param {string} cwd Repository directory.
53 * @param {string} cmd Git executable (full path if not already on path).
54 * @function Object() { [native code] }
55 */
56function Git(cwd, cmd) {
57 this.cwd = cwd;
58 this.cmd = cmd || 'git';
59 this.output = '';
60}
61
62/**
63 * Execute an arbitrary git command.
64 * @param {Array<string>} args Arguments (e.g. ['remote', 'update']).
65 * @return {Promise} A promise. The promise will be resolved with this instance
66 * or rejected with an error.
67 */
68Git.prototype.exec = function (...args) {
69 return spawn(this.cmd, [...args], this.cwd).then((output) => {
70 this.output = output;
71 return this;
72 });
73};
74
75/**
76 * Initialize repository.
77 * @return {Promise} A promise.
78 */
79Git.prototype.init = function () {
80 return this.exec('init');
81};
82
83/**
84 * Clean up unversioned files.
85 * @return {Promise} A promise.
86 */
87Git.prototype.clean = function () {
88 return this.exec('clean', '-f', '-d');
89};
90
91/**
92 * Hard reset to remote/branch
93 * @param {string} remote Remote alias.
94 * @param {string} branch Branch name.
95 * @return {Promise} A promise.
96 */
97Git.prototype.reset = function (remote, branch) {
98 return this.exec('reset', '--hard', remote + '/' + branch);
99};
100
101/**
102 * Fetch from a remote.
103 * @param {string} remote Remote alias.
104 * @return {Promise} A promise.
105 */
106Git.prototype.fetch = function (remote) {
107 return this.exec('fetch', remote);
108};
109
110/**
111 * Checkout a branch (create an orphan if it doesn't exist on the remote).
112 * @param {string} remote Remote alias.
113 * @param {string} branch Branch name.
114 * @return {Promise} A promise.
115 */
116Git.prototype.checkout = function (remote, branch) {
117 const treeish = remote + '/' + branch;
118 return this.exec('ls-remote', '--exit-code', '.', treeish).then(
119 () => {
120 // branch exists on remote, hard reset
121 return this.exec('checkout', branch)
122 .then(() => this.clean())
123 .then(() => this.reset(remote, branch));
124 },
125 (error) => {
126 if (error instanceof ProcessError && error.code === 2) {
127 // branch doesn't exist, create an orphan
128 return this.exec('checkout', '--orphan', branch);
129 } else {
130 // unhandled error
131 throw error;
132 }
133 }
134 );
135};
136
137/**
138 * Remove all unversioned files.
139 * @param {string | Array<string>} files Files argument.
140 * @return {Promise} A promise.
141 */
142Git.prototype.rm = function (files) {
143 if (!Array.isArray(files)) {
144 files = [files];
145 }
146 return this.exec('rm', '--ignore-unmatch', '-r', '-f', ...files);
147};
148
149/**
150 * Add files.
151 * @param {string | Array<string>} files Files argument.
152 * @return {Promise} A promise.
153 */
154Git.prototype.add = function (files) {
155 if (!Array.isArray(files)) {
156 files = [files];
157 }
158 return this.exec('add', ...files);
159};
160
161/**
162 * Commit (if there are any changes).
163 * @param {string} message Commit message.
164 * @return {Promise} A promise.
165 */
166Git.prototype.commit = function (message) {
167 return this.exec('diff-index', '--quiet', 'HEAD').catch(() =>
168 this.exec('commit', '-m', message)
169 );
170};
171
172/**
173 * Add tag
174 * @param {string} name Name of tag.
175 * @return {Promise} A promise.
176 */
177Git.prototype.tag = function (name) {
178 return this.exec('tag', name);
179};
180
181/**
182 * Push a branch.
183 * @param {string} remote Remote alias.
184 * @param {string} branch Branch name.
185 * @param {boolean} force Force push.
186 * @return {Promise} A promise.
187 */
188Git.prototype.push = function (remote, branch, force) {
189 const args = ['push', '--tags', remote, branch];
190 if (force) {
191 args.push('--force');
192 }
193 return this.exec.apply(this, args);
194};
195
196/**
197 * Get the URL for a remote.
198 * @param {string} remote Remote alias.
199 * @return {Promise<string>} A promise for the remote URL.
200 */
201Git.prototype.getRemoteUrl = function (remote) {
202 return this.exec('config', '--get', 'remote.' + remote + '.url')
203 .then((git) => {
204 const repo = git.output && git.output.split(/[\n\r]/).shift();
205 if (repo) {
206 return repo;
207 } else {
208 throw new Error(
209 'Failed to get repo URL from options or current directory.'
210 );
211 }
212 })
213 .catch((err) => {
214 throw new Error(
215 'Failed to get remote.' +
216 remote +
217 '.url (task must either be ' +
218 'run in a git repository with a configured ' +
219 remote +
220 ' remote ' +
221 'or must be configured with the "repo" option).'
222 );
223 });
224};
225
226/**
227 * Delete ref to remove branch history
228 * @param {string} branch The branch name.
229 * @return {Promise} A promise. The promise will be resolved with this instance
230 * or rejected with an error.
231 */
232Git.prototype.deleteRef = function (branch) {
233 return this.exec('update-ref', '-d', 'refs/heads/' + branch);
234};
235
236/**
237 * Clone a repo into the given dir if it doesn't already exist.
238 * @param {string} repo Repository URL.
239 * @param {string} dir Target directory.
240 * @param {string} branch Branch name.
241 * @param {options} options All options.
242 * @return {Promise<Git>} A promise.
243 */
244Git.clone = function clone(repo, dir, branch, options) {
245 return fs.exists(dir).then((exists) => {
246 if (exists) {
247 return Promise.resolve(new Git(dir, options.git));
248 } else {
249 return fs.mkdirp(path.dirname(path.resolve(dir))).then(() => {
250 const args = [
251 'clone',
252 repo,
253 dir,
254 '--branch',
255 branch,
256 '--single-branch',
257 '--origin',
258 options.remote,
259 '--depth',
260 options.depth,
261 ];
262 return spawn(options.git, args)
263 .catch((err) => {
264 // try again without branch or depth options
265 return spawn(options.git, [
266 'clone',
267 repo,
268 dir,
269 '--origin',
270 options.remote,
271 ]);
272 })
273 .then(() => new Git(dir, options.git));
274 });
275 }
276 });
277};
278
279module.exports = Git;