gitane
Version:
Easy Node.JS Git wrapper with support for SSH keys
196 lines (173 loc) • 5.24 kB
JavaScript
var crypto = require('crypto')
var EventEmitter = require('events').EventEmitter
var fs = require('fs')
var path = require('path')
var os = require('os')
var spawn = require('child_process').spawn
var Step = require('step')
var isWindows = /^win/.test(process.platform)
var PATH = isWindows ? process.env.Path : process.env.PATH;
var SEPARATOR = isWindows ? ';' : ':';
var emitter
// Template string for wrapper script.
var GIT_SSH_TEMPLATE = '#!/bin/sh\n' +
'exec ssh -i $key -o StrictHostKeyChecking=no "$@"\n'
function mkTempFile(prefix, suffix) {
var randomStr = crypto.randomBytes(4).toString('hex')
var name = prefix + randomStr + suffix
var file = path.join(os.tmpDir(), name)
// Hack for weird environments (nodejitsu didn't guarantee os.tmpDir() to already exist)
try {
fs.mkdirSync(os.tmpDir())
} catch (e) {}
return file
}
//
// Write the Git script template to enable use of the SSH private key
//
// *privKey* SSH private key.
// *file* (optional) filename of script.
// *keyMode* (optional) mode of key.
// *cb* callback function of signature function(err, tempateFile, keyFile).
//
function writeFiles(privKey, file, keyMode, cb) {
// No file name - generate a random one under the system temp dir
if (!file) {
file = mkTempFile("_gitane", ".sh")
}
if (typeof(keyMode) === 'function') {
cb = keyMode
keyMode = 0600
}
var keyfile = mkTempFile("_gitaneid", ".key")
var keyfileName = keyfile
if (isWindows) {
keyfileName = "\"" + keyfile.replace(/\\/g,"\\\\") + "\"";
}
var data = GIT_SSH_TEMPLATE.replace('$key', keyfileName)
Step(
function() {
fs.writeFile(file, data, this.parallel())
fs.writeFile(keyfile, privKey, this.parallel())
},
function(err) {
if (err) {
return cb(err, null)
}
// make script executable
fs.chmod(file, 0755, this.parallel())
// make key secret
fs.chmod(keyfile, keyMode, this.parallel())
},
function(err) {
if (err) {
return cb(err, null)
}
return cb(null, file, keyfile)
}
)
}
//
// Run a command in a subprocess with GIT_SSH set to the correct value for
// SSH key.
//
// *baseDir* current working dir from which to execute git
// *privKey* SSH private key to use
// *cmd* command to run
// *keyMode* optional unix file mode of key
// *cb* callback function of signature function(err, stdout, stderr)
//
// or first argument may be an object with params same as above,
// with addition of *emitter* which is an EventEmitter for real-time stdout
// and stderr events. An optional *detached* option specifies whether the
// spawned process should be detached from this one, and defaults to true.
// Detachment means the git process won't hang trying to prompt for a password.
function run(baseDir, privKey, cmd, keyMode, cb) {
var detached = true
var spawnFn = spawn
if (typeof(keyMode) === 'function') {
cb = keyMode
keyMode = 0600
}
if (typeof(baseDir) === 'object') {
var opts = baseDir
cb = privKey
cmd = opts.cmd
privKey = opts.privKey
keyMode = opts.keyMode || 0600
emitter = opts.emitter
baseDir = opts.baseDir
spawnFn = opts.spawn || spawn
if (typeof(opts.detached) !== 'undefined') {
detached = opts.detached
}
}
var split = cmd.split(/\s+/)
var cmd = split[0]
var args = split.slice(1)
Step(
function() {
writeFiles(privKey, null, keyMode, this)
},
function(err, file, keyfile) {
if (err) {
console.log("Error writing files: %s", err)
return cb(err, null)
}
this.file = file
this.keyfile = keyfile
var proc = spawnFn(cmd, args, {cwd: baseDir, env: {GIT_SSH: file, PATH:PATH}, detached: detached})
proc.stdoutBuffer = ""
proc.stderrBuffer = ""
proc.stdout.setEncoding('utf8')
proc.stderr.setEncoding('utf8')
proc.stdout.on('data', function(buf) {
if (typeof(emitter) === 'object') {
emitter.emit('stdout', buf)
}
proc.stdoutBuffer += buf
})
proc.stderr.on('data', function(buf) {
if (typeof(emitter) === 'object') {
emitter.emit('stderr', buf)
}
proc.stderrBuffer += buf
})
var self = this
proc.on('close', function(exitCode) {
var err = null
if (exitCode !== 0) {
err = "process exited with status " + exitCode
}
self(err, proc.stdoutBuffer, proc.stderrBuffer, exitCode)
})
proc.on('error', function (err) {
// prevent node from throwing an error. The error handling is
// done in the 'close' handler.
})
},
function(err, stdout, stderr, exitCode) {
// cleanup temp files
try {
fs.unlink(this.file)
fs.unlink(this.keyfile)
} catch(e) {}
cb(err, stdout, stderr, exitCode)
}
)
}
//
// convenience wrapper for clone. maybe add more later.
//
function clone(args, baseDir, privKey, cb) {
run(baseDir, privKey, "git clone " + args, cb)
}
function addPath(str) {
PATH = PATH + SEPARATOR + str
}
module.exports = {
clone:clone,
run:run,
writeFiles:writeFiles,
addPath:addPath,
}