Update codebase to the almost finalised architecture.

The GitRemoteResolver is almost done.
This commit is contained in:
André Cruz
2013-04-14 03:40:25 +01:00
parent 1642a7714f
commit 59fbc308b0
18 changed files with 409 additions and 343 deletions

0
lib/resolve/Manager.js Normal file
View File

216
lib/resolve/UnitOfWork.js Normal file
View File

@@ -0,0 +1,216 @@
var Q = require('q');
var util = require('util');
var mout = require('mout');
var events = require('events');
var createError = require('../util/createError');
var UnitOfWork = function (options) {
// Ensure options defaults
this._options = mout.object.mixIn({
failFast: true,
maxConcurrent: 5
}, options);
// Parse some of the options
this._options.failFast = !!this._options.failFast;
this._options.maxConcurrent = this._options.maxConcurrent > 0 ? this._options.maxConcurrent : 0;
// Initialize some needed properties
this._queue = [];
this._beingResolved = [];
this._beingResolvedEndpoints = {};
this._resolved = {};
this._failed = {};
this._completed = {};
};
util.inherits(UnitOfWork, events.EventEmitter);
// -----------------
UnitOfWork.prototype.enqueue = function (pkg) {
var deferred = Q.defer(),
index;
// Throw it if already queued
index = this._indexOf(this._queue, pkg);
if (index !== -1) {
throw new Error('Package is already queued');
}
// Throw if already resolving
index = this._indexOf(this._beingResolved, pkg);
if (index !== -1) {
throw new Error('Package is already being resolved');
}
// Add to the queue
this._queue.push({
pkg: pkg,
deferred: deferred
});
this.emit('enqueue', pkg);
// Process the queue shortly later so that handlers can be attached to the returned promise
Q.fcall(this._processQueue.bind(this));
return deferred.promise;
};
UnitOfWork.prototype.dequeue = function (pkg) {
var index;
// Throw if the package is already is being resolved
index = this._indexOf(this._beingResolved, pkg);
if (index !== -1) {
throw new Error('Package is already being resolved');
}
// Attempt to remove from the queue
index = this._indexOf(this._queue, pkg);
if (index !== -1) {
this._queue.splice(index, 1);
this.emit('dequeue', pkg);
}
return this;
};
UnitOfWork.prototype.getResolved = function (name) {
return name ? this._resolved[name] || [] : this._resolved;
};
UnitOfWork.prototype.getFailed = function (name) {
return name ? this._failed[name] || [] : this._failed;
};
// -----------------
UnitOfWork.prototype._processQueue = function () {
// If marked to fail all, reject everything
if (this._failAll) {
return this._rejectAll();
}
// Check if the number of allowed packages being resolved reached the maximum
if (this._options.maxConcurrent && this._beingResolved.length >= this._options.maxConcurrent) {
return;
}
// Find candidates for the free spots
var freeSpots = this._options.maxConcurrent ? this._options.maxConcurrent - this._beingResolved.length : -1,
endpoint,
duplicate,
entry,
x;
for (x = 0; x < this._queue.length && freeSpots; ++x) {
entry = this._queue[x];
endpoint = entry.pkg.getEndpoint();
// Skip if there is a package being resolved with the same endpoint
if (this._beingResolvedEndpoints[endpoint]) {
continue;
}
// Remove from the queue
this._queue.splice(x--, 1);
this.emit('dequeue', entry.pkg);
// Check if the exact same package has been resolved (same endpoint and range)
// If so, we reject the promise with an appropriate error
duplicate = this._findDuplicate(entry.pkg);
if (duplicate) {
entry.deferred.reject(createError('Package with same endpoint and range was already resolved', 'EDUPL', { pkg: duplicate }));
continue;
}
// Package is ok to resolve
// Put it in the being resolved list
this._beingResolved.push(entry);
this._beingResolvedEndpoints[endpoint] = true;
// Decrement the free spots available
freeSpots--;
// Resolve the promise to let the package know that it can proceed
this.emit('before_resolve', entry.pkg);
entry.deferred.resolve(this._onPackageDone.bind(this, entry.pkg));
}
};
UnitOfWork.prototype._rejectAll = function () {
var error,
queue;
// Reset the queue and being resolved list
queue = this._queue;
this._queue = [];
this._beingResolved = [];
this._beingResolvedEndpoints = {};
// Reject every deferred
error = createError('Package rejected to be resolved', 'EFFAST');
queue.forEach(function (entry) {
entry.deferred.reject(error);
});
};
UnitOfWork.prototype._onPackageDone = function (pkg, err) {
var pkgName = pkg.getName(),
pkgEndpoint = pkg.getEndpoint(),
arr,
index;
// Ignore if already completed
if (this._completed[pkgEndpoint] && this._completed[pkgEndpoint].indexOf(pkg) !== -1) {
return;
}
// Add it as completed
arr = this._completed[pkgEndpoint] = this._completed[pkgEndpoint] || [];
arr.push(pkg);
// Remove the package from the being resolved list
index = this._indexOf(this._beingResolved, pkg);
this._beingResolved.splice(index, 1);
delete this._beingResolvedEndpoints[pkg.getEndpoint()];
// If called with no error then add it as resolved
if (!err) {
arr = this._resolved[pkgName] = this._resolved[pkgName] || [];
arr.push(pkg);
this.emit('resolve', pkg);
// Otherwise, it failed to resolve so we mark it as failed
} else {
arr = this._failed[pkgName] = this._failed[pkgName] || [];
arr.push(pkg);
this.emit('failed', pkg);
// If fail fast is enabled, make every other package in the queue to fail
this._failAll = this._options.failFast;
}
// Call process queue in order to allow packages to take over the free spots in the queue
this._processQueue();
};
UnitOfWork.prototype._indexOf = function (arr, pkg) {
return mout.array.findIndex(arr, function (item) {
return item.pkg === pkg;
});
};
UnitOfWork.prototype._findDuplicate = function (pkg) {
var arr = this._completed[pkg.getEndpoint()];
if (!arr) {
return null;
}
return mout.array.find(arr, function (item) {
return item.getRange() === pkg.getRange();
});
};
module.exports = UnitOfWork;

View File

@@ -0,0 +1,24 @@
var Q = require('Q');
var GitFsResolver = require('./resolvers/GitFsResolver');
var GitRemoteResolver = require('./resolvers/GitRemoteResolver');
var LocalResolver = require('./resolvers/LocalResolver');
var UrlResolver = require('./resolvers/UrlResolver');
var GitFsResolver = require('./resolvers/GitFsResolver');
function createResolver(endpoint, options) {
var split = endpoint.split('#'),
range;
// Extract the range from the endpoint
endpoint = split[0];
range = split[1];
// Ensure options
options = options || {};
options.range = options.range || range;
// TODO: analyze endpoint and create appropriate package
return Q.fcall(new packages.UrlPackage(endpoint, options));
}
module.exports = createPackage;

View File

View File

@@ -0,0 +1,43 @@
var util = require('util');
var Q = require('q');
var Package = require('../Package');
var GitFsPackage = function (endpoint, options) {
Package.call(this, endpoint, options);
};
util.inherits(GitFsPackage, Package);
// -----------------
GitFsPackage.prototype._resolveSelf = function () {
var promise;
console.log('_resolveSelf of git local package');
promise = this._copy()
.then(this._fetch.bind(this))
.then(this._versions.bind(this))
.then(this._checkout.bind(this));
return promise;
};
GitFsPackage.prototype._copy = function () {
// create temporary folder
// copy over
};
GitFsPackage.prototype._fetch = function () {
// fetch origin
// reset --hard
};
GitFsPackage.prototype._versions = function () {
// retrieve versions
};
GitFsPackage.prototype._checkout = function () {
// resolve range to a specific version and check it out
};
module.exports = GitFsPackage;

View File

@@ -0,0 +1,200 @@
var util = require('util');
var Q = require('q');
var semver = require('semver');
var mout = require('mout');
var Resolver = require('./Resolver');
var cmd = require('../../util/cmd');
var createError = require('../../util/createError');
var GitRemoteResolver = function (source, options) {
Resolver.call(this, source, options);
};
util.inherits(GitRemoteResolver, Resolver);
// -----------------
GitRemoteResolver.prototype.hasNew = function (oldTarget, oldResolution) {
return this._resolveTarget(this._target)
.then(function (resolution) {
// Resolution types are different
if (oldResolution.type !== resolution.type) {
return true;
}
// If resolved to a tag
// There is new content if the tags are not equal
if (resolution.type === 'tag') {
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));
return promise;
};
GitRemoteResolver.prototype._resolveTarget = function () {
var target = this._target,
source = this._source,
promise,
branches,
errorMessage,
errorDetails;
// Target is a range/version
if (semver.valid(target) || semver.validRange(target)) {
return GitRemoteResolver._fetchVersions(this._source)
.then(function (versions) {
// Find the highest one that satifies the target
var version = mout.array.find(versions, function (version) {
return semver.satisfies(version, target);
});
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);
}
return { type: 'tag', tag: version };
});
}
// Resolve the rest to a commit version
promise = GitRemoteResolver._fetchHeads(this._source);
// Target is a commit, so it's a stale target (not a moving target)
// There's nothing to do in this case
if ((/^[a-f0-9]{40}$/).test(target)) {
return Q.resolve({ type: 'commit', commit: target });
}
// If target is *, use master branch
if (target === '*') {
target = 'master';
}
// Target is a branch
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);
}
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) {
if (this._refs && this._refs[source]) {
return Q.resolve(this._refs[source]);
}
return Q.nfcall(cmd, 'git', ['ls-remote', '--tags', '--heads', 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));
};
GitRemoteResolver._fetchVersions = function (source) {
if (this._versions && this._versions[source]) {
return Q.resolve(this._versions[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+)$/),
cleaned;
// Ensure it's valid
if (match) {
cleaned = semver.clean(match[1]);
if (cleaned) {
versions.push(cleaned);
}
}
});
// Sort them by desc order
versions = versions.sort(function (a, b) {
return semver.gt(a, b) ? -1 : 1;
});
this._versions = this._versions || {};
return this._versions[source] = versions;
}.bind(this));
};
GitRemoteResolver._fetchHeads = function (source) {
if (this._heads && this._heads[source]) {
return Q.resolve(this._heads[source]);
}
// Request heads of the source of only the specified branch
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+)$/);
if (match) {
heads[match[2]] = match[1];
}
});
return heads;
}.bind(this));
};
module.exports = GitRemoteResolver;

View File

@@ -0,0 +1,138 @@
var util = require('util');
var fs = require('fs');
var path = require('path');
var events = require('events');
var Q = require('q');
var tmp = require('tmp');
var config = require('../../config');
var createError = require('../../util/createError');
var Resolver = function (source, options) {
options = options || {};
this._source = source;
this._target = options.target || '*';
this._name = options.name;
this._guessedName = !this.name;
this._config = options.config || config;
};
util.inherits(Resolver, events.EventEmitter);
// -----------------
Resolver.prototype.getSource = function () {
return this._name;
};
Resolver.prototype.getTarget = function () {
return this._target;
};
Resolver.prototype.getTempDir = function () {
return this._tempDir;
};
Resolver.prototype.resolve = function () {
// 1nd - Create temporary dir
return this._createTempDir()
// 2nd - Resolve self
.then(this._resolveSelf.bind(this))
// 3th - Read json
.then(this._readJson.bind(this))
// 4th - Parse json
.then(this._parseJson.bind(this));
};
Resolver.prototype.isCacheable = function () {
return false;
};
Resolver.prototype.getDependencies = function () {
return this._json.dependencies;
};
// -----------------
Resolver.prototype.hasNew = function (oldTarget, oldResolution) {
return true;
};
Resolver.prototype._resolveSelf = function () {};
// -----------------
Resolver.prototype._createTempDir = function () {
return Q.nfcall(tmp.dir, {
prefix: 'bower-' + this._name + '-', // TODO: add a bower folder as the prefix
mode: parseInt('0777', 8) & (~process.umask()),
unsafeCleanup: true
})
.then(function (dir) {
this._tempDir = dir;
return dir;
}.bind(this));
};
Resolver.prototype._readJson = function () {
var jsonFile;
// Try bower.json
jsonFile = path.join(this.getTempDir(), 'bower.json');
return Q.nfcall(fs.readFile, jsonFile)
// Try component.json
.then(null, function (err) {
if (err.code !== 'ENOENT') {
throw err;
}
jsonFile = path.join(this.getTempDir(), 'component.json');
return Q.nfcall(fs.readFile, jsonFile)
// Issue a deprecation message if it exists
.then(function (contents) {
this.emit('warn', 'Package "' + this.name + '" is using the deprecated component.json file');
return contents;
}.bind(this));
}.bind(this))
// If we got the file contents, validate them
.then(function (contents) {
// TODO: change the validation to a separate module in the bower organization
try {
this._json = JSON.parse(contents);
return this._json;
} catch (e) {
throw createError('Unable to parse "' + jsonFile + '" file', e.code, {
details: e.message
});
}
// Otherwise there was an error
}.bind(this), function (err) {
// If no json file was found, return one just with the name
if (err.code === 'ENOENT') {
this._json = { name: this.name };
return this._json;
}
// If we got here, the error code is something else so we re-throw it
throw err;
}.bind(this));
};
Resolver.prototype._parseJson = function () {
// Check if name defined in the json is different
// If so and if the name was "guessed", assume the json name
if (this._guessedName && this._json.name !== this.name) {
this.name = this._json.name;
this.emit('name_change', this.name);
}
this._json.dependencies = this._json.dependencies || {};
// Handle ignore property, deleting all files from the temporary directory
return Q.fcall(function () {
// Delete all the files specified in the ignore from the temporary directory
// TODO:
}.bind(this));
};
module.exports = Resolver;

View File

@@ -0,0 +1,47 @@
var util = require('util');
var Q = require('q');
var Package = require('../Package');
var UrlPackage = function (endpoint, options) {
Package.call(this, endpoint, options);
};
util.inherits(UrlPackage, Package);
// -----------------
UrlPackage.prototype._resolveSelf = function () {
var promise;
console.log('_resolveSelf of url package');
promise = this._download()
.then(this._extract.bind(this));
return promise;
};
UrlPackage.prototype._download = function () {
var deferred = Q.defer();
console.log('_download');
setTimeout(function () {
deferred.resolve();
}, 1000);
return deferred.promise;
};
UrlPackage.prototype._extract = function () {
var deferred = Q.defer();
// If the file extension is not a zip and a tar, resolve the promise on next tick
console.log('_extract');
setTimeout(function () {
deferred.resolve();
}, 1000);
return deferred.promise;
};
module.exports = UrlPackage;