diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..1168c28 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["nodejs-lts"] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c06c418 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.{json,*rc,yml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..dfce806 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +parser: babel-eslint + +parserOptions: + ecmaVersion": 6, + sourceType: module + +env: + node: true + es6: true + +rules: + # Possible Errors + # https://github.com/eslint/eslint/tree/master/docs/rules#possible-errors + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-semi: 2 + no-func-assign: 2 + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-proto: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-typeof: 2 + + # Best Practices + # https://github.com/eslint/eslint/tree/master/docs/rules#best-practices + no-fallthrough: 2 + no-redeclare: 2 + + # Variables + # http://eslint.org/docs/rules/#variables + no-undef: 2 + no-unused-vars: 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d04b17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +lib +coverage +.nyc_output diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d03276b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +sudo: false + +language: node_js + +matrix: + include: + - node_js: "4" + env: COVERALLS=1 + - node_js: "5" + +# This is a random private key used purely for testing. +before_script: + - echo -e "Host *\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config + - echo -e "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkTcgXnHuqR0gbwegnr9Zxz4hTkjjV/SpgJNPJz7mo/HKNbx0rqjj1P0yGR053R9GSFFim2ut4NK9DPPUkQdyucw+DoLkYRHJmlJ4BNa9NTCD0sl+eSXO2969kZojCYSOgbmkCJx8mdgTwhzdgE/jhBrsY0hPE6pRTlU+H68/zeNdJUAIJf0LLXOm3hpTKLA19VICltl/j9VvBJpgRHdBylXEyL8HokYpjkQQk1ZXj3m7Nlo8yDdg4VcljOJWC+Xh8kxRMfK5x/VRVsYKCQXN5QlzKeqf7USRDUS/7mFoPUBW+d4kwKtGxRsWuIL2yeqzifZUTOgsh9+ZWAWxWffQZ your_email@example.com" > ~/.ssh/id_rsa.pub + - echo -e "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA5E3IF5x7qkdIG8HoJ6/Wcc+IU5I41f0qYCTTyc+5qPxyjW8d\nK6o49T9MhkdOd0fRkhRYptrreDSvQzz1JEHcrnMPg6C5GERyZpSeATWvTUwg9LJf\nnklztvevZGaIwmEjoG5pAicfJnYE8Ic3YBP44Qa7GNITxOqUU5VPh+vP83jXSVAC\nCX9Cy1zpt4aUyiwNfVSApbZf4/VbwSaYER3QcpVxMi/B6JGKY5EEJNWV495uzZaP\nMg3YOFXJYziVgvl4fJMUTHyucf1UVbGCgkFzeUJcynqn+1EkQ1Ev+5haD1AVvneJ\nMCrRsUbFriC9snqs4n2VEzoLIffmVgFsVn30GQIDAQABAoIBAQDPQm2sQbti0mN8\nD4Uawl8D40v30n8WhUa7EbPTOmlqKAQ2sfDhex9KRbTLEmEBmImA/Eee8o9iCTIy\n8Fv8Fm6pUHt9G6Pti/XvemwW3Q3QNpSUkHqN0FDkgecQVqVBEb6uHo3mDm4RFINX\neOmkp30BjIK9/blEw1D0sFALLOEUPaDdPMwiXtFgqfrFSgpDET3TvQIwZ2LxxTm0\ncNmP3sCSlZHJNkZI4hBEWaaXR+V5/+C1qblDCo5blAWTcX3UzqrwUUJgFi6VnBuh\n7S9Q6+CEIU+4JRyWQNmY8YgZFaAp6IOr/kyfPxTP1+UEVVgcLn3WDYwfG9og0tmz\nfzlruAgBAoGBAPfz73Pey86tNZEanhJhbX8gVjzy2hvyhT0paHg0q/H6c1VWOtUH\nOwZ3Ns2xAZqJhlDqCHnQYSCZDly042U/theP4N8zo1APb4Yg4qdmXF9QE1+2M03r\nkS6138gU/CSCLf8pCYa6pA/GmsaXxloeJGLvT4fzOZRsVav80/92XHRhAoGBAOu2\nmKh4Gr1EjgN9QNbk9cQTSFDtlBEqO/0pTepvL73UvNp/BAn4iYZFU4WnklFVBSWc\nL84Sc732xU12TAbTTUsa6E7W29pS8u7zVTxlIdQIIU5pzDyU1pNNk2kpxzte5p3Y\nPDtniPFsoYLWoH0LpsKL93t2pLAj+IOkE6f3XBq5AoGAIKaYo5N1FxQr952frx/x\nQUpK0N/R5Ng8v18SiLG26rhmM5iVSrQXC7TrHI7wfR8a9tC6qP/NqnM9NuwC/bQ0\nEEo7/GhaWxKNRwZRkmWiSFLNGk9t1hbtGU+N1lUdFtmloPIQdRNiw0kN3JTj474Q\nYI7O1EItFORnK6yxZfR6HEECgYEA1CT7MGUoa8APsMRCXyaiq15Pb8bjxK8mXquW\nHLEFXuzhLCW1FORDoj0y9s/iuKC0iS0ROX8R/J7k5NrbgikbH8WP36UxKkYNr1IC\nHOFImPTYRSKjVsL+fIUNb1DSp3S6SsYbL7v3XJJQqtlQiDq8U8x1aQFXJ9C4EoLR\nzhKrKsECgYBtU/TSF/TATZY5XtrN9O+HX1Fbz70Ci8XgvioheVI2fezOcXPRzDcC\nOYPaCMNKA5E8gHdg4s0TN7uDvKTJ+KhSg2V7gZ39A28dHrJaRX7Nz4k6t2uEBjX9\na1JidpAIbJ+3w7+hj6L299tVZvS+Y/6Dz/uuEQGXfJg/l/5CCvQPsA==\n-----END RSA PRIVATE KEY-----" > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa* + - eval `ssh-agent -s` + - ssh-add ~/.ssh/id_rsa + - git config --global user.name travis + - git config --global user.email travis@locahost + +after_success: + - if [ "x$COVERALLS" = "x1" ]; then npm run coveralls; fi + +# It is necessary for nodegit +# https://github.com/nodegit/nodegit#getting-started +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - libstdc++-4.9-dev + - gcc-4.9 + - g++-4.9 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..55f6bf1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Andrew Abramov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..767fe30 --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +nodegit-clone +============= + +[![NPM Status][npm-img]][npm] +[![Travis Status][test-img]][travis] +[![Coverage Status][coverage-img]][coveralls] +[![Dependency Status][dependency-img]][david] + +[npm]: https://www.npmjs.org/package/nodegit-clone +[npm-img]: https://img.shields.io/npm/v/nodegit-clone.svg + +[travis]: https://travis-ci.org/blond/nodegit-clone +[test-img]: https://img.shields.io/travis/blond/nodegit-clone.svg?label=tests + +[coveralls]: https://coveralls.io/r/blond/nodegit-clone +[coverage-img]: https://img.shields.io/coveralls/blond/nodegit-clone.svg + +[david]: https://david-dm.org/blond/nodegit-clone +[dependency-img]: http://img.shields.io/david/blond/nodegit-clone.svg + +Clone git repository with [nodegit](http://www.nodegit.org/). + +Install +------- + +``` +$ npm install --save nodegit-clone +``` + +Usage +----- + +```js +import clone from 'nodegit-clone'; + +clone('https://github.com/owner/repo') + .then(repo => { + // Access any repository methods here. + console.log(repo.path()); + }); + +// path/to/repo/.git +``` + +API +--- + +### clone({ url, [localPath], [ghToken], [ssh] }) + +Returns a Promise, that resolves to instance of [Repository](http://www.nodegit.org/api/repository/). + +#### url + +Type: `string` + +The URL to the repository. + +**Note:** the following protocols are supported: `http`, `https`, `git` and `ssh`. + +```js +clone('http://github.com/owner/repo'); +clone('https://github.com/owner/repo'); +clone('git://github.com/owner/repo.git'); +clone('git@github.com:owner/repo.git'); +``` + +#### localPath + +Type: `string` + +The Local path to store repository. + +**Note:** if `localPath` is not specified then repository will be cloned to directory with repository name. + +#### ghToken + +Type: `string` + +The GitHub personal OAuth token. + +#### ssh + +Type: `object` + +The object with paths to ssh keys and passphrase. + +GitHub Private Repositories +--------------------------- + +Before you can clone a repository, you'll need a GitHub OAuth application token. You can find more information on generating one here: [Creating an access token for command-line use](https://help.github.com/articles/creating-an-access-token-for-command-line-use/). + +In this example we're going to clone one of our private test repositories from GitHub. This must be an https protocol URL for the clone to work. + +```js +// Keep this value a secret. If you accidentally commit +// this key to a public GitHub repository they will immediately revoke it. +const GITHUB_TOKEN = ''; + +clone({ + url: 'https://github.com/owner/private', + ghToken: GITHUB_TOKEN +}); +``` + +SSH Keys +-------- + +Before you can clone a repository, you'll need SSH keys. You can find more information on generating them here: [Generating an SSH key](https://help.github.com/articles/generating-an-ssh-key/). + +In this example we're going to clone one of our private test repositories. This must be an ssh protocol URL for the clone to work. + +```js +clone({ + url: 'git@github.com:owner/private.git', + ssh: { + publicKey: '/path/to/public-key', + privateKey: '/path/to/private-key' + } +}); +``` + +For encrypted keys you should specify the `passphrase` option: + +```js +clone({ + url: 'git@github.com:owner/private.git', + ssh: { + publicKey: '/path/to/public-key', + privateKey: '/path/to/private-key', + passphrase: 'password' + } +}); +``` + +License +------- + +MIT © [Andrew Abramov](https://github.com/blond) diff --git a/package.json b/package.json new file mode 100644 index 0000000..59ad670 --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "nodegit-clone", + "version": "1.0.0", + "description": "Clone git repository with nodegit", + "license": "MIT", + "repository": "blond/nodegit-clone", + "author": "Andrew Abramov (github.com/blond)", + "keywords": [ + "git", + "clone", + "http", + "https", + "ssh", + "github", + "token" + ], + "main": "lib/clone.js", + "files": [ + "libs/**", + "index.js" + ], + "dependencies": { + "is-ssh": "1.3.0", + "nodegit": "0.11.5", + "throw": "1.1.0" + }, + "devDependencies": { + "ava": "0.12.0", + "babel-cli": "6.5.1", + "babel-eslint": "5.0.0", + "babel-preset-nodejs-lts": "1.2.1", + "babel-register": "6.5.2", + "coveralls": "2.11.6", + "es6-promisify": "3.0.0", + "eslint": "2.2.0", + "fs-extra": "0.26.5", + "nyc": "5.6.0", + "tempfile": "1.1.1" + }, + "scripts": { + "test": "npm run lint && npm run cover", + "lint": "eslint .", + "unit-test": "ava", + "cover": "nyc ava", + "coveralls": "nyc report --reporter=text-lcov | coveralls", + "prepublish": "npm run clean && npm run compile", + "compile": "babel src -d lib", + "clean": "rm -rf lib/" + }, + "ava": { + "files": [ + "test/**/*.js", + "!test/fixtures/**", + "!test/utils/**" + ], + "verbose": true, + "require": [ + "babel-register" + ] + } +} diff --git a/src/clone.js b/src/clone.js new file mode 100644 index 0000000..b83abf5 --- /dev/null +++ b/src/clone.js @@ -0,0 +1,54 @@ +import os from 'os'; + +import git from 'nodegit'; +import isSSH from 'is-ssh'; + +import init from './init'; + +/** + * Clones repository to `localPath`. + * + * @param {Object} opts Options or url of the repository. + * @param {String} opts.url The URL to the repository. + * @param {String} [opts.localPath] The Local path to store repository. + * @param {String} [opts.ghToken] The GitHub personal OAuth token. + * @param {Object} [opts.ssh] The object with paths to ssh keys and passphrase. + * @param {String} [opts.ssh.publicKey] The path to the public key of the credential. + * @param {String} [opts.ssh.privateKey] The path to the private key of the credential. + * @param {String} [opts.ssh.passphrase] The passphrase of the credential. + * + * @returns {Promise} A promise `Git.Repository` instance. + * @see [Git.Repository]{@link http://www.nodegit.org/api/repository/} + */ +export default function clone(opts) { + const { url, localPath, ghToken, ssh } = init(opts); + + const cloneOpts = { fetchOpts: {} }; + const callbacks = cloneOpts.fetchOpts.callbacks = {}; + + if (os.type() === 'Darwin'/* OS X */) { + // This is a required callback for OS X machines. There is a known issue + // with libgit2 being able to verify certificates from GitHub. + callbacks.certificateCheck = () => 1; + } + + if (ghToken) { + callbacks.certificateCheck = () => 1; + callbacks.credentials = () => { + // In order to authorize the clone operation, we'll need to respond to a low-level callback + // that expects credentials to be passed. + // This function will respond back with the OAuth token. + return git.Cred.userpassPlaintextNew(ghToken, "x-oauth-basic"); + }; + } else if (ssh.publicKey || isSSH(url)) { + callbacks.credentials = (url, username) => { + // Forward user name to validate authentication. + return git.Cred.sshKeyFromAgent(username, ssh.publicKey, ssh.privateKey, ssh.passphrase); + }; + } + + // Convert NodeGit promise to ES Promise + return new Promise((resolve, reject) => { + git.Clone(url, localPath, cloneOpts).then(resolve, reject); + }); +} diff --git a/src/init.js b/src/init.js new file mode 100644 index 0000000..ef00c42 --- /dev/null +++ b/src/init.js @@ -0,0 +1,20 @@ +import path from 'path'; +import thr from 'throw'; + +import repoName from './repo-name'; + +/** + * Initializes options. + * + * @param {Object} opts + * @returns {String} + */ +export default function init(opts={}) { + const { + url = (typeof opts === 'string') ? opts : thr('You should specify url to repository.'), + localPath = repoName(url), + ghToken, ssh={} + } = opts; + + return { url, localPath: path.resolve(localPath), ghToken, ssh }; +} diff --git a/src/repo-name.js b/src/repo-name.js new file mode 100644 index 0000000..9d6b38b --- /dev/null +++ b/src/repo-name.js @@ -0,0 +1,11 @@ +import path from 'path'; + +/** + * Returns repository name. + * + * @param {String} url — url of the repository. + * @returns {String} + */ +export default function getRepoName(url) { + return path.basename(url, '.git'); +} diff --git a/test/errors.js b/test/errors.js new file mode 100644 index 0000000..d292c25 --- /dev/null +++ b/test/errors.js @@ -0,0 +1,45 @@ +import path from 'path'; + +import test from 'ava'; +import fs from 'fs-extra'; +import promisify from 'es6-promisify'; + +import clone from '../src/clone'; +import tmpdirs from './utils/tmpdirs'; + +const outputFile = promisify(fs.outputFile); +const tmps = tmpdirs(); +const tmpdir = tmps.tmpdir.bind(tmpdirs); + +test.after('clear', () => tmps.clear()); + +test('should throw error if local store of repository already exists', t => { + const url = 'git://github.com/nodegit/fake.git'; + const workdir = tmpdir(); + + return t.throws( + occupy(workdir).then(() => clone({ url: url, localPath: workdir })) + ); +}); + +test('should throw error if repository does not exist', t => { + return t.throws( + clone({ url: 'git://github.com/nodegit/fake.git', localPath: tmpdir() }) + ); +}); + +test('should throw error if page does not exist', t => { + return t.throws( + clone({ url: 'http://github.com/nodegit/fake', localPath: tmpdir() }) + ); +}); + +/** + * @param {String} dir — path to dir. + * @returns {Promise} + */ +function occupy(dir) { + const filename = path.join(dir, 'some-file.txt'); + + return outputFile(filename, 'text'); +} diff --git a/test/fixtures/ssh-keys/encrypted_rsa b/test/fixtures/ssh-keys/encrypted_rsa new file mode 100644 index 0000000..2ecf534 --- /dev/null +++ b/test/fixtures/ssh-keys/encrypted_rsa @@ -0,0 +1,54 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,76C5AF2E37D863AA3712FFD502FE63FF + +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 +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/ssh-keys/encrypted_rsa.pub b/test/fixtures/ssh-keys/encrypted_rsa.pub new file mode 100644 index 0000000..3ab84a7 --- /dev/null +++ b/test/fixtures/ssh-keys/encrypted_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC6x3pGhFSz+CT5bjeIWNvYNh4BBNFiGn+YzT09we58vwnyHWb940e6SJalrTqLH+6QWAXMcZpSZFBwnYia9Ja/XE2UWwxQU5rryGdc5Tju/u1kB0HuMjGpQDmFSpVrJcfRS5wXJCtoka5O0qvPUUNVAK9k/6HVX+2CGbafNjOMXnNtWgKKWlDd5eQAk3Ok3LvSrTv0kp9S3C4KdO8cz9NvtlWXHTFit94kOIzaL/v10f2u6V93VvV5jckJwZhjfyH1Q2WS/+j0ck7WqyGEBltPz6dubJipqrlAHRUacmWtI0ODdsoNwUrKCidiEkwheWA2SmbIdkTInt5vrcYvbOdNrn9aJ1aII15PSLE7eCqNdasEkp2G75hQ5DWYnprxVGY2FzqCCg2DwYev1qe4MzJ+m9PrUL+FKCrkhiCgpaOT+Noz5gX1gRvYtvg+BeRA2uoIi7rK1A3CapKRJmVM7kukgCZ5ZSMgXgXPtX4ttUgu43jVjEdcjrkoZqBSH8l72cmEcIyjahuJZqg859CyQbX5qAGDwxdz7Qjj0gwGU//bNd3/vZMzWEfkN49iDpYjipLZzg4MR946kFTT90X9S5ryjSXkyFZJWSyks8K8BOztL/o8avAu4RUn/4+ipISSz+5FoOi/pkH+KkvIoSzlcuGxAigDyUlev8wxn/FQj2tmGQ== your_email@example.com diff --git a/test/fixtures/ssh-keys/index.js b/test/fixtures/ssh-keys/index.js new file mode 100644 index 0000000..ae17f67 --- /dev/null +++ b/test/fixtures/ssh-keys/index.js @@ -0,0 +1,15 @@ +import path from 'path'; + +const absolutify = path.join.bind(path, __dirname); + +export default { + unsafe: { + publicKey: absolutify('./unsafe_rsa.pub'), + privateKey: absolutify('./unsafe_rsa') + }, + encrypted: { + publicKey: absolutify('./encrypted_rsa.pub'), + privateKey: absolutify('./encrypted_rsa') + }, + passphrase: 'test-password' +}; diff --git a/test/fixtures/ssh-keys/unsafe_rsa b/test/fixtures/ssh-keys/unsafe_rsa new file mode 100644 index 0000000..91ae663 --- /dev/null +++ b/test/fixtures/ssh-keys/unsafe_rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA5E3IF5x7qkdIG8HoJ6/Wcc+IU5I41f0qYCTTyc+5qPxyjW8d +K6o49T9MhkdOd0fRkhRYptrreDSvQzz1JEHcrnMPg6C5GERyZpSeATWvTUwg9LJf +nklztvevZGaIwmEjoG5pAicfJnYE8Ic3YBP44Qa7GNITxOqUU5VPh+vP83jXSVAC +CX9Cy1zpt4aUyiwNfVSApbZf4/VbwSaYER3QcpVxMi/B6JGKY5EEJNWV495uzZaP +Mg3YOFXJYziVgvl4fJMUTHyucf1UVbGCgkFzeUJcynqn+1EkQ1Ev+5haD1AVvneJ +MCrRsUbFriC9snqs4n2VEzoLIffmVgFsVn30GQIDAQABAoIBAQDPQm2sQbti0mN8 +D4Uawl8D40v30n8WhUa7EbPTOmlqKAQ2sfDhex9KRbTLEmEBmImA/Eee8o9iCTIy +8Fv8Fm6pUHt9G6Pti/XvemwW3Q3QNpSUkHqN0FDkgecQVqVBEb6uHo3mDm4RFINX +eOmkp30BjIK9/blEw1D0sFALLOEUPaDdPMwiXtFgqfrFSgpDET3TvQIwZ2LxxTm0 +cNmP3sCSlZHJNkZI4hBEWaaXR+V5/+C1qblDCo5blAWTcX3UzqrwUUJgFi6VnBuh +7S9Q6+CEIU+4JRyWQNmY8YgZFaAp6IOr/kyfPxTP1+UEVVgcLn3WDYwfG9og0tmz +fzlruAgBAoGBAPfz73Pey86tNZEanhJhbX8gVjzy2hvyhT0paHg0q/H6c1VWOtUH +OwZ3Ns2xAZqJhlDqCHnQYSCZDly042U/theP4N8zo1APb4Yg4qdmXF9QE1+2M03r +kS6138gU/CSCLf8pCYa6pA/GmsaXxloeJGLvT4fzOZRsVav80/92XHRhAoGBAOu2 +mKh4Gr1EjgN9QNbk9cQTSFDtlBEqO/0pTepvL73UvNp/BAn4iYZFU4WnklFVBSWc +L84Sc732xU12TAbTTUsa6E7W29pS8u7zVTxlIdQIIU5pzDyU1pNNk2kpxzte5p3Y +PDtniPFsoYLWoH0LpsKL93t2pLAj+IOkE6f3XBq5AoGAIKaYo5N1FxQr952frx/x +QUpK0N/R5Ng8v18SiLG26rhmM5iVSrQXC7TrHI7wfR8a9tC6qP/NqnM9NuwC/bQ0 +EEo7/GhaWxKNRwZRkmWiSFLNGk9t1hbtGU+N1lUdFtmloPIQdRNiw0kN3JTj474Q +YI7O1EItFORnK6yxZfR6HEECgYEA1CT7MGUoa8APsMRCXyaiq15Pb8bjxK8mXquW +HLEFXuzhLCW1FORDoj0y9s/iuKC0iS0ROX8R/J7k5NrbgikbH8WP36UxKkYNr1IC +HOFImPTYRSKjVsL+fIUNb1DSp3S6SsYbL7v3XJJQqtlQiDq8U8x1aQFXJ9C4EoLR +zhKrKsECgYBtU/TSF/TATZY5XtrN9O+HX1Fbz70Ci8XgvioheVI2fezOcXPRzDcC +OYPaCMNKA5E8gHdg4s0TN7uDvKTJ+KhSg2V7gZ39A28dHrJaRX7Nz4k6t2uEBjX9 +a1JidpAIbJ+3w7+hj6L299tVZvS+Y/6Dz/uuEQGXfJg/l/5CCvQPsA== +-----END RSA PRIVATE KEY----- diff --git a/test/fixtures/ssh-keys/unsafe_rsa.pub b/test/fixtures/ssh-keys/unsafe_rsa.pub new file mode 100644 index 0000000..bd84623 --- /dev/null +++ b/test/fixtures/ssh-keys/unsafe_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkTcgXnHuqR0gbwegnr9Zxz4hTkjjV/SpgJNPJz7mo/HKNbx0rqjj1P0yGR053R9GSFFim2ut4NK9DPPUkQdyucw+DoLkYRHJmlJ4BNa9NTCD0sl+eSXO2969kZojCYSOgbmkCJx8mdgTwhzdgE/jhBrsY0hPE6pRTlU+H68/zeNdJUAIJf0LLXOm3hpTKLA19VICltl/j9VvBJpgRHdBylXEyL8HokYpjkQQk1ZXj3m7Nlo8yDdg4VcljOJWC+Xh8kxRMfK5x/VRVsYKCQXN5QlzKeqf7USRDUS/7mFoPUBW+d4kwKtGxRsWuIL2yeqzifZUTOgsh9+ZWAWxWffQZ your_email@example.com diff --git a/test/init.js b/test/init.js new file mode 100644 index 0000000..56693db --- /dev/null +++ b/test/init.js @@ -0,0 +1,37 @@ +import path from 'path'; +import test from 'ava'; + +import init from '../src/init'; + +test('should support url as string', t => { + const { url } = init('url'); + + t.is(url, 'url'); +}); + +test('should throw error if options is not specified', t => { + t.throws(() => init()); +}); + +test('should throw error if url is not specified', t => { + t.throws(() => init({}), 'You should specify url to repository.'); +}); + +test('should resolve local path', t => { + const { localPath } = init({ url: 'url', localPath: './my-repo' }); + + t.is(localPath, path.resolve('./my-repo')); +}); + +test('should support absolute path', t => { + const workdir = path.resolve('./my-repo'); + const { localPath } = init({ url: 'url', localPath: workdir }); + + t.is(localPath, workdir); +}); + +test('should get local path by url', t => { + const { localPath } = init({ url: 'https://github.com/owner/my-repo' }); + + t.is(localPath, path.resolve('./my-repo')); +}); diff --git a/test/protocols.js b/test/protocols.js new file mode 100644 index 0000000..f8b56a9 --- /dev/null +++ b/test/protocols.js @@ -0,0 +1,62 @@ +import test from 'ava'; +import { Repository } from 'nodegit'; + +import clone from '../src/clone'; +import sshKeys from './fixtures/ssh-keys'; +import tmpdirs from './utils/tmpdirs'; + +const tmps = tmpdirs(); +const tmpdir = tmps.tmpdir.bind(tmpdirs); + +test.after('clear', () => tmps.clear()); + +test('should clone repository with http', async t => { + const repo = await clone({ url: 'http://github.com/nodegit/test', localPath: tmpdir() }); + + t.ok(repo instanceof Repository); +}); + +test('should clone repository with https', async t => { + const repo = await clone({ url: 'https://github.com/nodegit/test', localPath: tmpdir() }); + + t.ok(repo instanceof Repository); +}); + +test('should clone repository with git', async t => { + const repo = await clone({ url: 'git://github.com/nodegit/test.git', localPath: tmpdir() }); + + t.ok(repo instanceof Repository); +}); + +test('should clone repository with ssh', async t => { + const repo = await clone({ url: 'git@github.com:nodegit/test.git', localPath: tmpdir() }); + + t.ok(repo instanceof Repository); +}); + +test('should clone repository with ssh while manually loading a key', async t => { + const repo = await clone({ + url: 'git@github.com:nodegit/test.git', + localPath: tmpdir(), + ssh: { + publicKey: sshKeys.unsafe.publicKey, + privateKey: sshKeys.unsafe.privateKey + } + }); + + t.ok(repo instanceof Repository); +}); + +test('should clone repository with ssh while manually loading an encrypted key', async t => { + const repo = await clone({ + url: 'git@github.com:nodegit/test.git', + localPath: tmpdir(), + ssh: { + publicKey: sshKeys.encrypted.publicKey, + privateKey: sshKeys.encrypted.privateKey, + passphrase: sshKeys.passphrase + } + }); + + t.ok(repo instanceof Repository); +}); diff --git a/test/repo-name.js b/test/repo-name.js new file mode 100644 index 0000000..e8e682f --- /dev/null +++ b/test/repo-name.js @@ -0,0 +1,27 @@ +import test from 'ava'; + +import repoName from '../src/repo-name'; + +test('should get name for http', t => { + const url = 'http://github.com/user/repo'; + + t.is(repoName(url), 'repo'); +}); + +test('should get name for https', t => { + const url = 'https://github.com/user/repo'; + + t.is(repoName(url), 'repo'); +}); + +test('should get name for git', t => { + const url = 'git://github.com/user/repo.git'; + + t.is(repoName(url), 'repo'); +}); + +test('should get name for ssh', t => { + const url = 'git@github.com:user/repo.git'; + + t.is(repoName(url), 'repo'); +}); diff --git a/test/utils/tmpdirs.js b/test/utils/tmpdirs.js new file mode 100644 index 0000000..0a35abd --- /dev/null +++ b/test/utils/tmpdirs.js @@ -0,0 +1,36 @@ +import fs from 'fs-extra'; +import promisify from 'es6-promisify'; +import tempfile from 'tempfile'; + +const remove = promisify(fs.remove); + +/** + * Util for works with tmp dirs. + * + * It is necessary to clone repositories independently. + * + * @returns {{ tmpdir: Function, clear: Funciton }} + */ +export default function tmpdirs() { + const dirs = []; + + return { + /** + * Generate dirname. + * @returns {String} + */ + tmpdir: () => { + const dir = tempfile(); + + dirs.push(dir); + + return dir; + }, + /** + * Removed all tmp dirs. + */ + clear: () => { + return Promise.all(dirs.map(dir => remove(dir))); + } + }; +}