git.js 7.1 KB

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