npm: upgrade to v1.4.14
[platform/upstream/nodejs.git] / deps / npm / lib / cache / add-remote-git.js
1 var mkdir = require("mkdirp")
2   , assert = require("assert")
3   , spawn = require("child_process").spawn
4   , exec = require("child_process").execFile
5   , once = require("once")
6   , fs = require("graceful-fs")
7   , log = require("npmlog")
8   , path = require("path")
9   , url = require("url")
10   , chownr = require("chownr")
11   , zlib = require("zlib")
12   , which = require("which")
13   , crypto = require("crypto")
14   , chmodr = require("chmodr")
15   , npm = require("../npm.js")
16   , rm = require("../utils/gently-rm.js")
17   , inflight = require("inflight")
18   , locker = require("../utils/locker.js")
19   , lock = locker.lock
20   , unlock = locker.unlock
21   , getCacheStat = require("./get-stat.js")
22   , addLocalTarball = require("./add-local-tarball.js")
23
24
25 // 1. cacheDir = path.join(cache,'_git-remotes',sha1(u))
26 // 2. checkGitDir(cacheDir) ? 4. : 3. (rm cacheDir if necessary)
27 // 3. git clone --mirror u cacheDir
28 // 4. cd cacheDir && git fetch -a origin
29 // 5. git archive /tmp/random.tgz
30 // 6. addLocalTarball(/tmp/random.tgz) <gitref> --format=tar --prefix=package/
31 // silent flag is used if this should error quietly
32 module.exports = function addRemoteGit (u, parsed, silent, cb_) {
33   assert(typeof u === "string", "must have git URL")
34   assert(typeof parsed === "object", "must have parsed query")
35   assert(typeof cb_ === "function", "must have callback")
36
37   function cb (er, data) {
38     unlock(u, function () { cb_(er, data) })
39   }
40
41   cb_ = inflight(u, cb_)
42
43   if (!cb_) return
44
45   // git is so tricky!
46   // if the path is like ssh://foo:22/some/path then it works, but
47   // it needs the ssh://
48   // If the path is like ssh://foo:some/path then it works, but
49   // only if you remove the ssh://
50   var origUrl = u
51   u = u.replace(/^git\+/, "")
52        .replace(/#.*$/, "")
53
54   // ssh paths that are scp-style urls don't need the ssh://
55   if (parsed.pathname.match(/^\/?:/)) {
56     u = u.replace(/^ssh:\/\//, "")
57   }
58
59   lock(u, function (er) {
60     if (er) return cb(er)
61
62     // figure out what we should check out.
63     var co = parsed.hash && parsed.hash.substr(1) || "master"
64
65     var v = crypto.createHash("sha1").update(u).digest("hex").slice(0, 8)
66     v = u.replace(/[^a-zA-Z0-9]+/g, '-') + '-' + v
67
68     log.verbose("addRemoteGit", [u, co])
69
70     var p = path.join(npm.config.get("cache"), "_git-remotes", v)
71
72     checkGitDir(p, u, co, origUrl, silent, function(er, data) {
73       chmodr(p, npm.modes.file, function(erChmod) {
74         if (er) return cb(er, data)
75         return cb(erChmod, data)
76       })
77     })
78   })
79 }
80
81 function checkGitDir (p, u, co, origUrl, silent, cb) {
82   fs.stat(p, function (er, s) {
83     if (er) return cloneGitRemote(p, u, co, origUrl, silent, cb)
84     if (!s.isDirectory()) return rm(p, function (er){
85       if (er) return cb(er)
86       cloneGitRemote(p, u, co, origUrl, silent, cb)
87     })
88
89     var git = npm.config.get("git")
90     var args = [ "config", "--get", "remote.origin.url" ]
91     var env = gitEnv()
92
93     // check for git
94     which(git, function (err) {
95       if (err) {
96         err.code = "ENOGIT"
97         return cb(err)
98       }
99       exec(git, args, {cwd: p, env: env}, function (er, stdout, stderr) {
100         var stdoutTrimmed = (stdout + "\n" + stderr).trim()
101         if (er || u !== stdout.trim()) {
102           log.warn( "`git config --get remote.origin.url` returned "
103                   + "wrong result ("+u+")", stdoutTrimmed )
104           return rm(p, function (er){
105             if (er) return cb(er)
106             cloneGitRemote(p, u, co, origUrl, silent, cb)
107           })
108         }
109         log.verbose("git remote.origin.url", stdoutTrimmed)
110         archiveGitRemote(p, u, co, origUrl, cb)
111       })
112     })
113   })
114 }
115
116 function checkGitDir (p, u, co, origUrl, silent, cb) {
117   fs.stat(p, function (er, s) {
118     if (er) return cloneGitRemote(p, u, co, origUrl, silent, cb)
119     if (!s.isDirectory()) return rm(p, function (er){
120       if (er) return cb(er)
121       cloneGitRemote(p, u, co, origUrl, silent, cb)
122     })
123
124     var git = npm.config.get("git")
125     var args = [ "config", "--get", "remote.origin.url" ]
126     var env = gitEnv()
127
128     // check for git
129     which(git, function (err) {
130       if (err) {
131         err.code = "ENOGIT"
132         return cb(err)
133       }
134       exec(git, args, {cwd: p, env: env}, function (er, stdout, stderr) {
135         var stdoutTrimmed = (stdout + "\n" + stderr).trim()
136         if (er || u !== stdout.trim()) {
137           log.warn( "`git config --get remote.origin.url` returned "
138                   + "wrong result ("+u+")", stdoutTrimmed )
139           return rm(p, function (er){
140             if (er) return cb(er)
141             cloneGitRemote(p, u, co, origUrl, silent, cb)
142           })
143         }
144         log.verbose("git remote.origin.url", stdoutTrimmed)
145         archiveGitRemote(p, u, co, origUrl, cb)
146       })
147     })
148   })
149 }
150
151 function cloneGitRemote (p, u, co, origUrl, silent, cb) {
152   mkdir(p, function (er) {
153     if (er) return cb(er)
154
155     var git = npm.config.get("git")
156     var args = [ "clone", "--mirror", u, p ]
157     var env = gitEnv()
158
159     // check for git
160     which(git, function (err) {
161       if (err) {
162         err.code = "ENOGIT"
163         return cb(err)
164       }
165       exec(git, args, {cwd: p, env: env}, function (er, stdout, stderr) {
166         stdout = (stdout + "\n" + stderr).trim()
167         if (er) {
168           if (silent) {
169             log.verbose("git clone " + u, stdout)
170           } else {
171             log.error("git clone " + u, stdout)
172           }
173           return cb(er)
174         }
175         log.verbose("git clone " + u, stdout)
176         archiveGitRemote(p, u, co, origUrl, cb)
177       })
178     })
179   })
180 }
181
182 function archiveGitRemote (p, u, co, origUrl, cb) {
183   var git = npm.config.get("git")
184   var archive = [ "fetch", "-a", "origin" ]
185   var resolve = [ "rev-list", "-n1", co ]
186   var env = gitEnv()
187
188   var resolved = null
189   var tmp
190
191   exec(git, archive, {cwd: p, env: env}, function (er, stdout, stderr) {
192     stdout = (stdout + "\n" + stderr).trim()
193     if (er) {
194       log.error("git fetch -a origin ("+u+")", stdout)
195       return cb(er)
196     }
197     log.verbose("git fetch -a origin ("+u+")", stdout)
198     tmp = path.join(npm.tmp, Date.now()+"-"+Math.random(), "tmp.tgz")
199     verifyOwnership()
200   })
201
202   function verifyOwnership() {
203     if (process.platform === "win32") {
204       log.silly("verifyOwnership", "skipping for windows")
205       resolveHead()
206     } else {
207       getCacheStat(function(er, cs) {
208         if (er) {
209           log.error("Could not get cache stat")
210           return cb(er)
211         }
212         chownr(p, cs.uid, cs.gid, function(er) {
213           if (er) {
214             log.error("Failed to change folder ownership under npm cache for %s", p)
215             return cb(er)
216           }
217           resolveHead()
218         })
219       })
220     }
221   }
222
223   function resolveHead () {
224     exec(git, resolve, {cwd: p, env: env}, function (er, stdout, stderr) {
225       stdout = (stdout + "\n" + stderr).trim()
226       if (er) {
227         log.error("Failed resolving git HEAD (" + u + ")", stderr)
228         return cb(er)
229       }
230       log.verbose("git rev-list -n1 " + co, stdout)
231       var parsed = url.parse(origUrl)
232       parsed.hash = stdout
233       resolved = url.format(parsed)
234
235       // https://github.com/npm/npm/issues/3224
236       // node incorrectly sticks a / at the start of the path
237       // We know that the host won't change, so split and detect this
238       var spo = origUrl.split(parsed.host)
239       var spr = resolved.split(parsed.host)
240       if (spo[1].charAt(0) === ':' && spr[1].charAt(0) === '/')
241         spr[1] = spr[1].slice(1)
242       resolved = spr.join(parsed.host)
243
244       log.verbose('resolved git url', resolved)
245       next()
246     })
247   }
248
249   function next () {
250     mkdir(path.dirname(tmp), function (er) {
251       if (er) return cb(er)
252       var gzip = zlib.createGzip({ level: 9 })
253       var git = npm.config.get("git")
254       var args = ["archive", co, "--format=tar", "--prefix=package/"]
255       var out = fs.createWriteStream(tmp)
256       var env = gitEnv()
257       cb = once(cb)
258       var cp = spawn(git, args, { env: env, cwd: p })
259       cp.on("error", cb)
260       cp.stderr.on("data", function(chunk) {
261         log.silly(chunk.toString(), "git archive")
262       })
263
264       cp.stdout.pipe(gzip).pipe(out).on("close", function() {
265         addLocalTarball(tmp, null, null, function(er, data) {
266           if (data) data._resolved = resolved
267           cb(er, data)
268         })
269       })
270     })
271   }
272 }
273
274 var gitEnv_
275 function gitEnv () {
276   // git responds to env vars in some weird ways in post-receive hooks
277   // so don't carry those along.
278   if (gitEnv_) return gitEnv_
279   gitEnv_ = {}
280   for (var k in process.env) {
281     if (!~['GIT_PROXY_COMMAND','GIT_SSH','GIT_SSL_NO_VERIFY'].indexOf(k) && k.match(/^GIT/)) continue
282     gitEnv_[k] = process.env[k]
283   }
284   return gitEnv_
285 }