├── README.md ├── index.js ├── package-lock.json ├── package.json ├── private ├── impl.js └── util.js ├── promise.js └── test ├── .gitignore ├── progress.js └── test.js /README.md: -------------------------------------------------------------------------------- 1 | # git-clone 2 | 3 | Clone a git repository via `git` shell command. 4 | 5 | ## Installation 6 | 7 | Install: 8 | 9 | $ npm install git-clone 10 | 11 | To use the original callback-based API: 12 | 13 | const clone = require('git-clone'); 14 | 15 | As of 0.2.0 there's a promised-based API for use with `async`/`await`: 16 | 17 | const clone = require('git-clone/promise'); 18 | 19 | ## API 20 | 21 | ## Common Options 22 | 23 | * `git`: path to `git` binary; default: `git` (expected to be in your `$PATH`) 24 | * `shallow`: when `true`, clone with depth 1 25 | * `checkout`: revision/branch/tag to check out after clone 26 | * `args`: additional array of arguments to pass to `git clone` 27 | 28 | **NOTE:** the `args` option allows arbitrary arguments to be passed to `git`; this is inherently insecure if used in 29 | combination with untrusted input. **Only use the `args` option with static/trusted input!** 30 | 31 | ## Callback 32 | 33 | #### `clone(repo, targetPath, [options], cb)` 34 | 35 | Clone `repo` to `targetPath`, calling `cb` on completion; any error that occurred will be passed as the first argument. If no error is passed the `git clone` operation was successful. 36 | 37 | ## Promise 38 | 39 | #### `async clone(repo, targetPath, [options])` 40 | 41 | Clone `repo` to `targetPath`, throwing an exception on failure. 42 | 43 | ## Contributors 44 | 45 | - [AntiMoron](https://github.com/AntiMoron) 46 | 47 | ## Copyright & License 48 | 49 | © 2014-2021 Jason Frame & Contributors [ [@jaz303](http://twitter.com/jaz303) / [jason@onehackoranother.com](mailto:jason@onehackoranother.com) ] 50 | 51 | Released under the ISC license. 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const impl = require('./private/impl'); 2 | 3 | module.exports = function(repo, targetPath, opts, cb) { 4 | if (typeof opts === 'function') { 5 | cb = opts; 6 | opts = null; 7 | } 8 | 9 | opts = opts || {}; 10 | cb = cb || function() {}; 11 | 12 | impl(repo, targetPath, opts, cb, cb); 13 | } 14 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-clone", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "0.2.0", 9 | "license": "ISC" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-clone", 3 | "version": "0.2.0", 4 | "description": "Clone a git repository", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/jaz303/git-clone.git" 12 | }, 13 | "keywords": [ 14 | "git", 15 | "clone", 16 | "shell" 17 | ], 18 | "author": "Jason Frame (http://jasonframe.co.uk)", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/jaz303/git-clone/issues" 22 | }, 23 | "homepage": "https://github.com/jaz303/git-clone" 24 | } 25 | -------------------------------------------------------------------------------- /private/impl.js: -------------------------------------------------------------------------------- 1 | const { 2 | buildCloneCommand, 3 | buildCheckoutCommand 4 | } = require('./util'); 5 | 6 | const spawn = require('child_process').spawn; 7 | 8 | module.exports = function clone(repo, targetPath, opts, onSuccess, onError) { 9 | const [cmd, args] = buildCloneCommand(repo, targetPath, opts); 10 | const proc = spawn(cmd, args); 11 | 12 | if (opts.progress) { 13 | proc.stderr.on('data', (evt) => { 14 | const line = evt.toString(); 15 | if (line.match(/Receiving objects:\s+(\d+)%/)) { 16 | opts.progress({ 17 | phase: 'receivingObjects', 18 | percent: Number(RegExp.$1) 19 | }); 20 | } else if (line.match(/Resolving deltas:\s+(\d+)%/)) { 21 | opts.progress({ 22 | phase: 'resolvingDeltas', 23 | percent: Number(RegExp.$1) 24 | }); 25 | } 26 | }); 27 | } 28 | 29 | proc.on('close', (status) => { 30 | if (status == 0) { 31 | if (opts.checkout) { 32 | _checkout(); 33 | } else { 34 | onSuccess(); 35 | } 36 | } else { 37 | onError(new Error("'git clone' failed with status " + status)); 38 | } 39 | }); 40 | 41 | function _checkout() { 42 | const [cmd, args] = buildCheckoutCommand(opts.checkout, opts); 43 | const proc = spawn(cmd, args, { cwd: targetPath }); 44 | proc.on('close', function(status) { 45 | if (status == 0) { 46 | onSuccess(); 47 | } else { 48 | onError(new Error("'git checkout' failed with status " + status)); 49 | } 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /private/util.js: -------------------------------------------------------------------------------- 1 | function git(opts) { 2 | return opts.git || 'git'; 3 | } 4 | 5 | exports.buildCloneCommand = function(repo, targetPath, opts) { 6 | let args = ['clone']; 7 | const userArgs = opts.args || []; 8 | 9 | if (opts.shallow) { 10 | if (userArgs.indexOf('--depth') >= 0) { 11 | throw new Error("'--depth' cannot be specified when shallow is set to 'true'"); 12 | } 13 | args.push('--depth', '1'); 14 | } 15 | 16 | if (opts.progress) { 17 | args.push('--progress'); 18 | } 19 | 20 | args = args.concat(userArgs); 21 | args.push('--', repo, targetPath); 22 | 23 | return [git(opts), args]; 24 | } 25 | 26 | exports.buildCheckoutCommand = function(ref, opts) { 27 | return [git(opts), ['checkout', ref]]; 28 | } 29 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | const impl = require('./private/impl'); 2 | 3 | module.exports = function(repo, targetPath, opts) { 4 | return new Promise((yes, no) => { 5 | impl(repo, targetPath, opts || {}, yes, no); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | test-checkout-* 2 | -------------------------------------------------------------------------------- /test/progress.js: -------------------------------------------------------------------------------- 1 | const gitClone = require('../'); 2 | 3 | gitClone('git@github.com:torvalds/linux.git', 'linux', { 4 | progress: (evt) => { 5 | console.log(evt); 6 | } 7 | }, () => { 8 | console.log("done!"); 9 | }); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const {execSync} = require('child_process'); 3 | 4 | const gitClone = { 5 | callback: require('../'), 6 | promise: require('../promise') 7 | }; 8 | 9 | function assertGitRepoSync(dir, revision) { 10 | const stats = fs.statSync(`${dir}/.git`); 11 | return stats.isDirectory(); 12 | } 13 | 14 | function assertCheckout(dir, expectedCheckout) { 15 | const checkedOut = execSync('git rev-parse HEAD', {cwd: dir, encoding: 'utf8'}).trim(); 16 | return checkedOut === expectedCheckout; 17 | } 18 | 19 | let nextCheckoutId = 1; 20 | function runTestCase(name, opts, fn) { 21 | const id = nextCheckoutId++; 22 | const targetDir = `./test-checkout-${Date.now()}-${id}`; 23 | 24 | fn(targetDir, opts, (err) => { 25 | if (err) { 26 | console.error(`Test '${name}' failed: ${err.message}`); 27 | } else if (!assertGitRepoSync(targetDir)) { 28 | console.error(`Test '${name}' failed: target directory is not a git repository`); 29 | } else if (opts && opts.checkout && !assertCheckout(targetDir, opts.checkout)) { 30 | console.error(`Test '${name}' failed: incorrect checkout`); 31 | } else { 32 | console.error(`Test '${name}': OK`); 33 | } 34 | execSync(`rm -rf ${targetDir}`); 35 | }); 36 | } 37 | 38 | const callbackTest = (targetDir, options, onComplete) => { 39 | if (options === null) { 40 | gitClone.callback('git@github.com:jaz303/git-clone.git', targetDir, onComplete); 41 | } else { 42 | gitClone.callback('git@github.com:jaz303/git-clone.git', targetDir, options, onComplete); 43 | } 44 | }; 45 | 46 | const promiseTest = async (targetDir, options, onComplete) => { 47 | try { 48 | await gitClone.promise('git@github.com:jaz303/git-clone.git', targetDir, options); 49 | onComplete(null); 50 | } catch (err) { 51 | onComplete(err); 52 | } 53 | } 54 | 55 | runTestCase('callback', {}, callbackTest); 56 | runTestCase('callback (null options)', null, callbackTest); // tests argument juggling 57 | runTestCase('callback with checkout', {checkout: 'ddb88d4d1dca74e8330eccb7997badd6a6906b98'}, callbackTest); 58 | runTestCase('promise', {}, promiseTest); 59 | runTestCase('promise with checkout', {checkout: 'ddb88d4d1dca74e8330eccb7997badd6a6906b98'}, promiseTest); 60 | --------------------------------------------------------------------------------