1/* eslint-disable camelcase */ 2var execFile = require('child_process').execFile 3var path = require('path') 4var zlib = require('zlib') 5 6var asyncMap = require('slide').asyncMap 7var deepEqual = require('deep-equal') 8var fs = require('graceful-fs') 9var mkdirp = require('mkdirp') 10var once = require('once') 11var requireInject = require('require-inject') 12var rimraf = require('rimraf') 13var tar = require('tar') 14var test = require('tap').test 15var tmpdir = require('osenv').tmpdir 16var which = require('which') 17 18var wd = path.resolve(tmpdir(), 'git-races') 19var fixtures = path.resolve(__dirname, '../fixtures') 20var testcase = 'github-com-BryanDonovan-npm-git-test' 21var testcase_git = path.resolve(wd, testcase + '.git') 22var testcase_path = path.resolve(wd, testcase) 23var testcase_tgz = path.resolve(fixtures, testcase + '.git.tar.gz') 24 25var testtarballs = [] 26var testrepos = {} 27var testurls = {} 28 29/* 30This test is specifically for #7202, where the bug was if you tried installing multiple git urls that 31pointed at the same repo but had different comittishes, you'd sometimes get the wrong version. 32The test cases, provided by @BryanDonovan, have a dependency tree like this: 33 34 top 35 bar#4.0.0 36 buzz#3.0.0 37 foo#3.0.0 38 buzz#3.0.0 39 foo#4.0.0 40 buzz#2.0.0 41 42But what would happen is that buzz#2.0.0 would end up installed under bar#4.0.0. 43 44bar#4.0.0 shouldn't have gotten its own copy if buzz, and if it did, it shouldn've been buzz#3.0.0 45*/ 46 47;['bar', 'foo', 'buzz'].forEach(function (name) { 48 var mockurl = 'ssh://git@github.com/BryanDonovan/dummy-npm-' + name + '.git' 49 var realrepo = path.resolve(wd, 'github-com-BryanDonovan-dummy-npm-' + name + '.git') 50 var tgz = path.resolve(fixtures, 'github-com-BryanDonovan-dummy-npm-' + name + '.git.tar.gz') 51 52 testrepos[mockurl] = realrepo 53 testtarballs.push(tgz) 54}) 55 56function cleanup () { 57 process.chdir(tmpdir()) 58 rimraf.sync(wd) 59} 60 61var npm = requireInject.installGlobally('../../lib/npm.js', { 62 'child_process': { 63 'execFile': function (cmd, args, options, cb) { 64 // on win 32, the following prefix is added in utils/git.js 65 // $ git -c core.longpaths=true clone 66 var i = process.platform === 'win32' ? 2 : 0 67 // If it's a clone we swap any requests for any of the urls we're mocking 68 // with the path to the bare repo 69 if (args[i] === 'clone') { 70 var m2 = args.length - 2 71 var m1 = args.length - 1 72 if (testrepos[args[m2]]) { 73 testurls[args[m1]] = args[m2] 74 args[m2] = testrepos[args[m2]] 75 } 76 execFile(cmd, args, options, cb) 77 // here, we intercept npm validating the remote origin url on one of the 78 // clones we've done previously and return the original url that was requested 79 } else if (args[i] === 'config' && args[i + 1] === '--get' && args[i + 2] === 'remote.origin.url') { 80 process.nextTick(function () { 81 cb(null, testurls[options.cwd], '') 82 }) 83 } else { 84 execFile(cmd, args, options, cb) 85 } 86 } 87 } 88}) 89 90function extract (tarball, target, cb) { 91 cb = once(cb) 92 fs.createReadStream(tarball).on('error', function (er) { cb(er) }) 93 .pipe(zlib.createGunzip()).on('error', function (er) { cb(er) }) 94 .pipe(tar.Extract({path: target})).on('error', function (er) { cb(er) }) 95 .on('end', function () { 96 cb() 97 }) 98} 99 100// Copied from lib/utils/git, because we need to use 101// it before calling npm.load and lib/utils/git uses npm.js 102// which doesn't allow that. =( =( 103 104function prefixGitArgs () { 105 return process.platform === 'win32' ? ['-c', 'core.longpaths=true'] : [] 106} 107 108var gitcmd 109 110function execGit (args, options, cb) { 111 var fullArgs = prefixGitArgs().concat(args || []) 112 return execFile(gitcmd, fullArgs, options, cb) 113} 114 115function gitWhichAndExec (args, options, cb) { 116 if (gitcmd) return execGit(args, options, cb) 117 118 which('git', function (err, pathtogit) { 119 if (err) { 120 err.code = 'ENOGIT' 121 return cb(err) 122 } 123 gitcmd = pathtogit 124 125 execGit(args, options, cb) 126 }) 127} 128 129function andClone (gitdir, repodir, cb) { 130 return function (er) { 131 if (er) return cb(er) 132 gitWhichAndExec(['clone', gitdir, repodir], {}, cb) 133 } 134} 135 136function setup (cb) { 137 cleanup() 138 mkdirp.sync(wd) 139 140 extract(testcase_tgz, wd, andClone(testcase_git, testcase_path, andExtractPackages)) 141 142 function andExtractPackages (er) { 143 if (er) return cb(er) 144 asyncMap(testtarballs, function (tgz, done) { 145 extract(tgz, wd, done) 146 }, andChdir) 147 } 148 function andChdir (er) { 149 if (er) return cb(er) 150 process.chdir(testcase_path) 151 andLoadNpm() 152 } 153 function andLoadNpm () { 154 var opts = { 155 cache: path.resolve(wd, 'cache') 156 } 157 npm.load(opts, cb) 158 } 159} 160 161// there are two (sic) valid trees that can result we don't care which one we 162// get in npm@2 163var oneTree = [ 164 'npm-git-test@1.0.0', [ 165 ['dummy-npm-bar@4.0.0', [ 166 ['dummy-npm-foo@3.0.0', []], 167 ['dummy-npm-buzz@3.0.0', []] 168 ]], 169 ['dummy-npm-buzz@3.0.0', []], 170 ['dummy-npm-foo@4.0.0', [ 171 ['dummy-npm-buzz@2.0.0', []] 172 ]] 173 ] 174] 175var otherTree = [ 176 'npm-git-test@1.0.0', [ 177 ['dummy-npm-bar@4.0.0', [ 178 ['dummy-npm-buzz@3.0.0', []], 179 ['dummy-npm-foo@3.0.0', []] 180 ]], 181 ['dummy-npm-buzz@3.0.0', []], 182 ['dummy-npm-foo@4.0.0', [ 183 ['dummy-npm-buzz@2.0.0', []] 184 ]] 185 ] 186] 187 188function toSimple (tree) { 189 var deps = [] 190 Object.keys(tree.dependencies || {}).forEach(function (dep) { 191 deps.push(toSimple(tree.dependencies[dep])) 192 }) 193 return [ tree['name'] + '@' + tree['version'], deps ] 194} 195 196test('setup', function (t) { 197 setup(function (er) { 198 t.ifError(er, 'setup ran OK') 199 t.end() 200 }) 201}) 202 203test('correct versions are installed for git dependency', function (t) { 204 t.comment('test for https://github.com/npm/npm/issues/7202') 205 npm.commands.install([], function (er) { 206 t.ifError(er, 'installed OK') 207 npm.commands.ls([], true, function (er, result) { 208 t.ifError(er, 'ls OK') 209 var simplified = toSimple(result) 210 if (deepEqual(simplified, oneTree) || deepEqual(simplified, otherTree)) { 211 t.pass('install tree is correct') 212 } else { 213 t.isDeeply(simplified, oneTree, 'oneTree matches') 214 t.isDeeply(simplified, otherTree, 'otherTree matches') 215 } 216 t.done() 217 }) 218 }) 219}) 220