Implemented GitFsResolver, minor refactor to the GitRemoteResolver.

This commit is contained in:
André Cruz
2013-04-14 15:27:08 +01:00
parent 83d76a0781
commit a4fbe65259
6 changed files with 222 additions and 100 deletions

View File

@@ -1,43 +1,93 @@
var util = require('util');
var fs = require('fs');
var Q = require('q');
var Package = require('../Package');
var mout = require('mout');
var GitRemoteResolver = require('./GitRemoteResolver');
var copy = require('../../util/copy');
var cmd = require('../../util/cmd');
var GitFsPackage = function (endpoint, options) {
Package.call(this, endpoint, options);
var GitFsResolver = function (endpoint, options) {
GitRemoteResolver.call(this, endpoint, options);
};
util.inherits(GitFsPackage, Package);
util.inherits(GitFsResolver, GitRemoteResolver);
// -----------------
GitFsPackage.prototype._resolveSelf = function () {
var promise;
GitFsResolver.prototype._resolveSelf = function () {
var self = this.constructor;
console.log('_resolveSelf of git local package');
promise = this._copy()
return this._copy()
.then(this._fetch.bind(this))
.then(this._versions.bind(this))
.then(self.findResolution.bind(self, this._tempDir, this._target))
.then(this._checkout.bind(this));
return promise;
};
GitFsPackage.prototype._copy = function () {
// create temporary folder
// copy over
// -----------------
GitFsResolver.prototype._copy = function () {
var tempDir = this._tempDir;
// Copy folder permissions
return Q.nfcall(fs.stat, this._source)
.then(function (stat) {
return Q.nfcall(fs.chmod, tempDir, stat.mode);
})
// Copy folder contents
.then(copy.bind(copy, this._source, tempDir))
.then(function () {
this._source = tempDir;
}.bind(this));
};
GitFsPackage.prototype._fetch = function () {
// fetch origin
// reset --hard
GitFsResolver.prototype._fetch = function () {
var dir = this._tempDir;
// Check if there is at least one remote
cmd('git', ['remote'], { cwd: dir })
.then(function (stdout) {
var hasRemote = !!stdout.trim().length;
// If so, do a fetch to grab the new tags and refs
if (hasRemote) {
return cmd('git', ['fetch', '--prune']);
}
});
};
GitFsPackage.prototype._versions = function () {
// retrieve versions
// Override the checkout function to work with the local copy
GitFsResolver.prototype._checkout = function (resolution) {
var dir = this._tempDir;
console.log(resolution);
// Checkout resolution
cmd('git', ['checkout', '-f', resolution.tag || resolution.branch || resolution.commit], { cwd: dir })
// Cleanup unstagged files
.then(function () {
return cmd('git', ['clean', '-f', '-d'], { cwd: dir });
});
};
GitFsPackage.prototype._checkout = function () {
// resolve range to a specific version and check it out
// -----------------
// Copy static stuff
mout.object.mixIn(GitFsResolver, GitRemoteResolver);
// Override the fetch refs to grab them locally
GitFsResolver.fetchRefs = function (source) {
if (this._refs && this._refs[source]) {
return Q.resolve(this._refs[source]);
}
return cmd('git', ['show-ref', '--tags', '--heads'], { cwd : source })
.then(function (stdout) {
// Make them an array
var refs = stdout.toString().split('\n');
this._refs = this._refs || {};
return this._refs[source] = refs;
}.bind(this));
};
module.exports = GitFsPackage;
module.exports = GitFsResolver;

View File

@@ -14,8 +14,13 @@ util.inherits(GitRemoteResolver, Resolver);
// -----------------
GitRemoteResolver.prototype._resolveSelf = function () {
return this.constructor.findResolution(this._source, this._target)
.then(this._checkout.bind(this));
};
GitRemoteResolver.prototype.hasNew = function (oldTarget, oldResolution) {
return this._resolveTarget(this._target)
return this.constructor.findResolution(this._source, this._target)
.then(function (resolution) {
// Resolution types are different
if (oldResolution.type !== resolution.type) {
@@ -28,33 +33,43 @@ GitRemoteResolver.prototype.hasNew = function (oldTarget, oldResolution) {
return semver.neq(resolution.tag, oldResolution.tag);
}
// If resolved to a commit hash, just check if they are different
// Use the same strategy if it the resolution is to a branch
return resolution.commit !== oldResolution.commit;
});
};
GitRemoteResolver.prototype._resolveSelf = function () {
var promise;
// -----------------
promise = this._resolveTarget()
.then(this._checkout.bind(this));
GitRemoteResolver.prototype._checkout = function (resolution) {
var dir = this._tempDir,
branch;
return promise;
console.log(resolution);
// If resolution is a commit, we need to clone the entire repo and checkit out
// Because a commit is not a nammed ref, there's no better solution
if (resolution.type === 'commit') {
return cmd('git', ['clone', this._source, '.'], { cwd: dir })
.then(function () {
return cmd('git', ['checkout', resolution.commit], { cwd: dir });
});
// Otherwise we are checking out a named ref so we can optimize it
} else {
branch = resolution.tag || resolution.branch;
return cmd('git', ['clone', this._source, '-b', branch, '--depth', 1, '.'], { cwd: dir });
}
};
GitRemoteResolver.prototype._resolveTarget = function () {
var target = this._target,
source = this._source,
promise,
branches,
errorMessage,
errorDetails;
// ------------------------------
GitRemoteResolver.findResolution = function (source, target) {
var promise,
branches;
// Target is a range/version
if (semver.valid(target) || semver.validRange(target)) {
return GitRemoteResolver._fetchVersions(this._source)
return this.fetchVersions(source)
.then(function (versions) {
// Find the highest one that satifies the target
var version = mout.array.find(versions, function (version) {
@@ -62,13 +77,11 @@ GitRemoteResolver.prototype._resolveTarget = function () {
});
if (!version) {
errorMessage = !semver.validRange(target) ?
'Tag "' + target + '" does not exist' :
'No tag found that was able to satisfy "' + target + '"';
errorDetails = !versions.length ?
'No tags found in "' + source + '"' :
'Available tags in "' + source + '" are: ' + versions.join(', ');
throw createError(errorMessage, 'ENORESTARGET', errorDetails);
throw createError('No tag found that was able to satisfy "' + target + '"', 'ENORESTARGET', {
details: !versions.length ?
'No tags found in "' + source + '"' :
'Available tags in "' + source + '" are: ' + versions.join(', ')
});
}
return { type: 'tag', tag: version };
@@ -76,7 +89,7 @@ GitRemoteResolver.prototype._resolveTarget = function () {
}
// Resolve the rest to a commit version
promise = GitRemoteResolver._fetchHeads(this._source);
promise = this.fetchHeads(source);
// Target is a commit, so it's a stale target (not a moving target)
// There's nothing to do in this case
@@ -93,41 +106,23 @@ GitRemoteResolver.prototype._resolveTarget = function () {
return promise.then(function (heads) {
if (!heads[target]) {
branches = Object.keys(heads);
errorDetails = !branches.length ?
'No branches found in "' + source + '"' :
'Available branches in "' + source + '" are: ' + branches.join(', ');
throw createError('Branch "' + target + '" does not exist', 'ENORESTARGET', errorDetails);
throw createError('Branch "' + target + '" does not exist', 'ENORESTARGET', {
details: !branches.length ?
'No branches found in "' + source + '"' :
'Available branches in "' + source + '" are: ' + branches.join(', ')
});
}
return { type: 'branch', branch: target, commit: heads[target] };
});
};
GitRemoteResolver.prototype._checkout = function (resolution) {
var dir = this._tempDir,
branch;
console.log(resolution);
if (resolution.type === 'commit') {
return Q.nfcall(cmd, 'git', ['clone', this._source, dir])
.then(function () {
return Q.nfcall(cmd, 'git', ['checkout', resolution.commit], { cwd: dir });
});
} else {
branch = resolution.tag || resolution.branch;
return Q.nfcall(cmd, 'git', ['clone', this._source, '-b', branch, '--depth', 1], { cwd: dir });
}
};
// ------------------------------
GitRemoteResolver._fetchRefs = function (source) {
GitRemoteResolver.fetchRefs = function (source) {
if (this._refs && this._refs[source]) {
return Q.resolve(this._refs[source]);
}
return Q.nfcall(cmd, 'git', ['ls-remote', '--tags', '--heads', source])
return cmd('git', ['ls-remote', '--tags', '--heads', source])
.then(function (stdout) {
// Make them an array
var refs = stdout.toString().split('\n');
@@ -137,18 +132,18 @@ GitRemoteResolver._fetchRefs = function (source) {
}.bind(this));
};
GitRemoteResolver._fetchVersions = function (source) {
GitRemoteResolver.fetchVersions = function (source) {
if (this._versions && this._versions[source]) {
return Q.resolve(this._versions[source]);
}
return this._fetchRefs(source)
return this.fetchRefs(source)
.then(function (refs) {
var versions = [];
// Parse each ref line, extracting the tag
refs.forEach(function (line) {
var match = line.match(/^[a-f0-9]{40}\s+refs\/tags\/(\S+)$/),
var match = line.match(/^[a-f0-9]{40}\s+refs\/tags\/(\S+)/),
cleaned;
// Ensure it's valid
@@ -170,21 +165,21 @@ GitRemoteResolver._fetchVersions = function (source) {
}.bind(this));
};
GitRemoteResolver._fetchHeads = function (source) {
GitRemoteResolver.fetchHeads = function (source) {
if (this._heads && this._heads[source]) {
return Q.resolve(this._heads[source]);
}
return this._fetchRefs(source)
return this.fetchRefs(source)
.then(function (refs) {
this._heads = this._heads || {};
var heads = this._heads[source] = this._heads[source] || {};
// Foreach line in the refs, extract only the heads
// Organize them in an object where keys are branches and values
// the commit hash
mout.array.forEach(refs, function (line) {
var match = line.match(/^([a-f0-9]{40})\s+refs\/heads\/(\S+)$/);
// the commit hashes
refs.forEach(function (line) {
var match = line.match(/^([a-f0-9]{40})\s+refs\/heads\/(\S+)/);
if (match) {
heads[match[2]] = match[1];

View File

@@ -1,18 +1,16 @@
var cp = require('child_process');
var Q = require('q');
var createError = require('./createError');
// Executes a shell command
// Buffers the stdout and stderr
// If an error occurs, a meaningfull error is generated
// TODO: I think we need to use spawn because it escapes args
module.exports = function (command, args, options, callback) {
function cmd(command, args, options) {
var process,
stderr = '',
stdout = '';
if (!callback) {
callback = options || args;
}
stdout = '',
deferred = Q.defer();
process = cp.spawn(command, args, options);
@@ -40,13 +38,18 @@ module.exports = function (command, args, options, callback) {
fullCommand += args.length ? ' ' + args.join(' ') : '';
// Build the error instance
error = createError('Failed to execute "' + fullCommand + '", exit code of #' + code, 'ECMDERR');
error.details = stderr;
error.exitCode = code;
error = createError('Failed to execute "' + fullCommand + '", exit code of #' + code, 'ECMDERR', {
details: stderr,
exitCode: code
});
return callback(error);
return deferred.reject(error);
}
return callback(null, stdout, stderr);
return deferred.resolve(stdout);
});
};
return deferred.promise;
}
module.exports = cmd;

55
lib/util/copy.js Normal file
View File

@@ -0,0 +1,55 @@
var fstream = require('fstream');
var Q = require('Q');
// Simple function to copy files and folders
// It uses the awesome fstream library.
// The "options.reader" will be passed to the reader.
// The "options.writter" will be passed to the writter.
function copy(src, dst, opts) {
opts = opts || {};
var deferred = Q.defer(),
reader,
writter,
removeListeners;
// Simple function to remove all the listeners
removeListeners = function () {
reader.removeAllListeners();
writter.removeAllListeners();
};
// TODO: see isacs reply about the end and error events..
// Create writter
opts.writter = opts.writter || {};
opts.writter.path = dst;
opts.writter.type = 'Directory';
writter = fstream.Writer(opts.writter)
.on('error', function (err) {
removeListeners();
deferred.reject(err);
})
.on('close', function () {
removeListeners();
deferred.resolve();
});
// Create reader
opts.reader = opts.reader || {};
opts.reader.path = src;
reader = fstream.Reader(opts.reader)
.on('error', function (err) {
removeListeners();
deferred.reject(err);
});
// Pipe reader to writter
reader.pipe(writter);
return deferred.promise;
}
module.exports = copy;