mirror of
https://github.com/bower/bower.git
synced 2026-04-24 03:00:19 -04:00
492 lines
14 KiB
JavaScript
492 lines
14 KiB
JavaScript
var util = require('util');
|
|
var Q = require('q');
|
|
var which = require('../../util/which');
|
|
var LRU = require('lru-cache');
|
|
var mout = require('mout');
|
|
var Resolver = require('./Resolver');
|
|
var semver = require('../../util/semver');
|
|
var createError = require('../../util/createError');
|
|
var cmd = require('../../util/cmd');
|
|
|
|
var hasSvn;
|
|
|
|
// Check if svn is installed
|
|
try {
|
|
which.sync('svn');
|
|
hasSvn = true;
|
|
} catch (ex) {
|
|
hasSvn = false;
|
|
}
|
|
|
|
function SvnResolver(decEndpoint, config, logger) {
|
|
Resolver.call(this, decEndpoint, config, logger);
|
|
|
|
if (!hasSvn) {
|
|
throw createError('svn is not installed or not in the PATH', 'ENOSVN');
|
|
}
|
|
}
|
|
|
|
util.inherits(SvnResolver, Resolver);
|
|
mout.object.mixIn(SvnResolver, Resolver);
|
|
|
|
// -----------------
|
|
|
|
SvnResolver.getSource = function(source) {
|
|
var uri = this._source || source;
|
|
|
|
return uri
|
|
.replace(/^svn\+(https?|file):\/\//i, '$1://') // Change svn+http or svn+https or svn+file to http(s), file respectively
|
|
.replace('svn://', 'http://') // Change svn to http
|
|
.replace(/\/+$/, ''); // Remove trailing slashes
|
|
};
|
|
|
|
SvnResolver.prototype._hasNew = function(pkgMeta) {
|
|
var oldResolution = pkgMeta._resolution || {};
|
|
|
|
return this._findResolution().then(function(resolution) {
|
|
// Check if resolution types are different
|
|
if (oldResolution.type !== resolution.type) {
|
|
return true;
|
|
}
|
|
|
|
// If resolved to a version, there is new content if the tags are not equal
|
|
if (
|
|
resolution.type === 'version' &&
|
|
semver.neq(resolution.tag, oldResolution.tag)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
// As last check, we compare both commit hashes
|
|
return resolution.commit !== oldResolution.commit;
|
|
});
|
|
};
|
|
|
|
SvnResolver.prototype._resolve = function() {
|
|
var that = this;
|
|
|
|
return this._findResolution().then(function() {
|
|
return that._export();
|
|
});
|
|
};
|
|
|
|
// -----------------
|
|
|
|
SvnResolver.prototype._export = function() {
|
|
var promise;
|
|
var timer;
|
|
var reporter;
|
|
var that = this;
|
|
var resolution = this._resolution;
|
|
|
|
this.source = SvnResolver.getSource(this._source);
|
|
|
|
this._logger.action(
|
|
'export',
|
|
resolution.tag || resolution.branch || resolution.commit,
|
|
{
|
|
resolution: resolution,
|
|
to: this._tempDir
|
|
}
|
|
);
|
|
|
|
if (resolution.type === 'commit') {
|
|
promise = cmd('svn', [
|
|
'export',
|
|
'--force',
|
|
'--non-interactive',
|
|
this._source + '/trunk',
|
|
'-r' + resolution.commit,
|
|
this._tempDir
|
|
]);
|
|
} else if (resolution.type === 'branch' && resolution.branch === 'trunk') {
|
|
promise = cmd('svn', [
|
|
'export',
|
|
'--force',
|
|
'--non-interactive',
|
|
this._source + '/trunk',
|
|
this._tempDir
|
|
]);
|
|
} else if (resolution.type === 'branch') {
|
|
promise = cmd('svn', [
|
|
'export',
|
|
'--force',
|
|
'--non-interactive',
|
|
this._source + '/branches/' + resolution.branch,
|
|
this._tempDir
|
|
]);
|
|
} else {
|
|
promise = cmd('svn', [
|
|
'export',
|
|
'--force',
|
|
'--non-interactive',
|
|
this._source + '/tags/' + resolution.tag,
|
|
this._tempDir
|
|
]);
|
|
}
|
|
|
|
// Throttle the progress reporter to 1 time each sec
|
|
reporter = mout.fn.throttle(function(data) {
|
|
var lines;
|
|
|
|
lines = data.split(/[\r\n]+/);
|
|
lines.forEach(function(line) {
|
|
if (/\d{1,3}\%/.test(line)) {
|
|
// TODO: There are some strange chars that appear once in a while (\u001b[K)
|
|
// Trim also those?
|
|
that._logger.info('progress', line.trim());
|
|
}
|
|
});
|
|
}, 1000);
|
|
|
|
// Start reporting progress after a few seconds
|
|
timer = setTimeout(function() {
|
|
promise.progress(reporter);
|
|
}, 8000);
|
|
|
|
return (
|
|
promise
|
|
// Add additional proxy information to the error if necessary
|
|
.fail(function(err) {
|
|
throw err;
|
|
})
|
|
// Clear timer at the end
|
|
.fin(function() {
|
|
clearTimeout(timer);
|
|
reporter.cancel();
|
|
})
|
|
);
|
|
};
|
|
|
|
// -----------------
|
|
|
|
SvnResolver.prototype._findResolution = function(target) {
|
|
var err;
|
|
var self = this.constructor;
|
|
var that = this;
|
|
|
|
target = target || this._target || '*';
|
|
|
|
this._source = SvnResolver.getSource(this._source);
|
|
|
|
// Target is a revision, so it's a stale target (not a moving target)
|
|
// There's nothing to do in this case
|
|
if (/^r\d+/.test(target)) {
|
|
target = target.split('r');
|
|
|
|
this._resolution = { type: 'commit', commit: target[1] };
|
|
return Q.resolve(this._resolution);
|
|
}
|
|
|
|
// Target is a range/version
|
|
if (semver.validRange(target)) {
|
|
return self.versions(this._source, true).then(function(versions) {
|
|
var versionsArr, version, index;
|
|
|
|
versionsArr = versions.map(function(obj) {
|
|
return obj.version;
|
|
});
|
|
|
|
// If there are no tags and target is *,
|
|
// fallback to the latest commit on trunk
|
|
if (!versions.length && target === '*') {
|
|
return that._findResolution('trunk');
|
|
}
|
|
|
|
versionsArr = versions.map(function(obj) {
|
|
return obj.version;
|
|
});
|
|
// Find a satisfying version, enabling strict match so that pre-releases
|
|
// have lower priority over normal ones when target is *
|
|
index = semver.maxSatisfyingIndex(versionsArr, target, true);
|
|
if (index !== -1) {
|
|
version = versions[index];
|
|
return (that._resolution = {
|
|
type: 'version',
|
|
tag: version.tag,
|
|
commit: version.commit
|
|
});
|
|
}
|
|
|
|
// Check if there's an exact branch/tag with this name as last resort
|
|
return Q.all([
|
|
self.branches(that._source),
|
|
self.tags(that._source)
|
|
]).spread(function(branches, tags) {
|
|
// Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
|
|
if (mout.object.hasOwn(tags, target)) {
|
|
return (that._resolution = {
|
|
type: 'tag',
|
|
tag: target,
|
|
commit: tags[target]
|
|
});
|
|
}
|
|
if (mout.object.hasOwn(branches, target)) {
|
|
return (that._resolution = {
|
|
type: 'branch',
|
|
branch: target,
|
|
commit: branches[target]
|
|
});
|
|
}
|
|
|
|
throw createError(
|
|
'No tag found that was able to satisfy ' + target,
|
|
'ENORESTARGET',
|
|
{
|
|
details: !versions.length
|
|
? 'No versions found in ' + that._source
|
|
: 'Available versions in ' +
|
|
that._source +
|
|
': ' +
|
|
versions
|
|
.map(function(version) {
|
|
return version.version;
|
|
})
|
|
.join(', ')
|
|
}
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Otherwise, target is either a tag or a branch
|
|
return Q.all([self.branches(that._source), self.tags(that._source)]).spread(
|
|
function(branches, tags) {
|
|
// Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
|
|
if (mout.object.hasOwn(tags, target)) {
|
|
return (that._resolution = {
|
|
type: 'tag',
|
|
tag: target,
|
|
commit: tags[target]
|
|
});
|
|
}
|
|
if (mout.object.hasOwn(branches, target)) {
|
|
return (that._resolution = {
|
|
type: 'branch',
|
|
branch: target,
|
|
commit: branches[target]
|
|
});
|
|
}
|
|
|
|
branches = Object.keys(branches);
|
|
tags = Object.keys(tags);
|
|
|
|
err = createError(
|
|
'target ' + target + ' does not exist',
|
|
'ENORESTARGET'
|
|
);
|
|
err.details = !tags.length
|
|
? 'No tags found in ' + that._source
|
|
: 'Available tags: ' + tags.join(', ');
|
|
err.details += '\n';
|
|
err.details += !branches.length
|
|
? 'No branches found in ' + that._source
|
|
: 'Available branches: ' + branches.join(', ');
|
|
|
|
throw err;
|
|
}
|
|
);
|
|
};
|
|
|
|
SvnResolver.prototype._savePkgMeta = function(meta) {
|
|
var version;
|
|
|
|
if (this._resolution.type === 'version') {
|
|
version = semver.clean(this._resolution.tag);
|
|
|
|
// Warn if the package meta version is different than the resolved one
|
|
if (
|
|
typeof meta.version === 'string' &&
|
|
semver.neq(meta.version, version)
|
|
) {
|
|
this._logger.warn(
|
|
'mismatch',
|
|
'Version declared in the json (' +
|
|
meta.version +
|
|
') is different than the resolved one (' +
|
|
version +
|
|
')',
|
|
{
|
|
resolution: this._resolution,
|
|
pkgMeta: meta
|
|
}
|
|
);
|
|
}
|
|
|
|
// Ensure package meta version is the same as the resolution
|
|
meta.version = version;
|
|
} else {
|
|
// If resolved to a target that is not a version,
|
|
// remove the version from the meta
|
|
delete meta.version;
|
|
}
|
|
|
|
// Save version/tag/commit in the release
|
|
// Note that we can't store branches because _release is supposed to be
|
|
// an unique id of this ref.
|
|
meta._release = version || this._resolution.tag || this._resolution.commit;
|
|
|
|
// Save resolution to be used in hasNew later
|
|
meta._resolution = this._resolution;
|
|
|
|
return Resolver.prototype._savePkgMeta.call(this, meta);
|
|
};
|
|
|
|
// ------------------------------
|
|
|
|
SvnResolver.versions = function(source, extra) {
|
|
source = SvnResolver.getSource(source);
|
|
|
|
var value = this._cache.versions.get(source);
|
|
|
|
if (value) {
|
|
return Q.resolve(value).then(
|
|
function() {
|
|
var versions = this._cache.versions.get(source);
|
|
|
|
// If no extra information was requested,
|
|
// resolve simply with the versions
|
|
if (!extra) {
|
|
versions = versions.map(function(version) {
|
|
return version.version;
|
|
});
|
|
}
|
|
|
|
return versions;
|
|
}.bind(this)
|
|
);
|
|
}
|
|
|
|
value = this.tags(source).then(
|
|
function(tags) {
|
|
var tag;
|
|
var version;
|
|
var versions = [];
|
|
|
|
// For each tag
|
|
for (tag in tags) {
|
|
version = semver.clean(tag);
|
|
if (version) {
|
|
versions.push({
|
|
version: version,
|
|
tag: tag,
|
|
commit: tags[tag]
|
|
});
|
|
}
|
|
}
|
|
|
|
// Sort them by DESC order
|
|
versions.sort(function(a, b) {
|
|
return semver.rcompare(a.version, b.version);
|
|
});
|
|
|
|
this._cache.versions.set(source, versions);
|
|
|
|
// Call the function again to keep it DRY
|
|
return this.versions(source, extra);
|
|
}.bind(this)
|
|
);
|
|
|
|
// Store the promise to be reused until it resolves
|
|
// to a specific value
|
|
this._cache.versions.set(source, value);
|
|
|
|
return value;
|
|
};
|
|
|
|
SvnResolver.tags = function(source) {
|
|
source = SvnResolver.getSource(source);
|
|
|
|
var value = this._cache.tags.get(source);
|
|
|
|
if (value) {
|
|
return Q.resolve(value);
|
|
}
|
|
|
|
value = cmd('svn', [
|
|
'list',
|
|
source + '/tags',
|
|
'--verbose',
|
|
'--non-interactive'
|
|
]).spread(
|
|
function(stout) {
|
|
var tags = SvnResolver.parseSubversionListOutput(stout.toString());
|
|
|
|
this._cache.tags.set(source, tags);
|
|
return tags;
|
|
}.bind(this)
|
|
);
|
|
|
|
// Store the promise to be reused until it resolves
|
|
// to a specific value
|
|
this._cache.tags.set(source, value);
|
|
|
|
return value;
|
|
};
|
|
|
|
SvnResolver.branches = function(source) {
|
|
source = SvnResolver.getSource(source);
|
|
|
|
var value = this._cache.branches.get(source);
|
|
|
|
if (value) {
|
|
return Q.resolve(value);
|
|
}
|
|
|
|
value = cmd('svn', [
|
|
'list',
|
|
source + '/branches',
|
|
'--verbose',
|
|
'--non-interactive'
|
|
]).spread(
|
|
function(stout) {
|
|
var branches = SvnResolver.parseSubversionListOutput(
|
|
stout.toString()
|
|
);
|
|
|
|
// trunk is a branch!
|
|
branches.trunk = '*';
|
|
|
|
this._cache.branches.set(source, branches);
|
|
return branches;
|
|
}.bind(this)
|
|
);
|
|
|
|
// Store the promise to be reused until it resolves
|
|
// to a specific value
|
|
this._cache.branches.set(source, value);
|
|
|
|
return value;
|
|
};
|
|
|
|
SvnResolver.parseSubversionListOutput = function(stout) {
|
|
var entries = {};
|
|
var lines = stout.trim().split(/[\r\n]+/);
|
|
|
|
// For each line in the refs, match only the branches
|
|
lines.forEach(function(line) {
|
|
var match = line.match(/\s+([0-9]+)\s.+\s([\w.$-]+)\//i);
|
|
|
|
if (match && match[2] !== '.') {
|
|
entries[match[2]] = match[1];
|
|
}
|
|
});
|
|
|
|
return entries;
|
|
};
|
|
|
|
SvnResolver.clearRuntimeCache = function() {
|
|
// Reset cache for branches, tags, etc
|
|
mout.object.forOwn(SvnResolver._cache, function(lru) {
|
|
lru.reset();
|
|
});
|
|
};
|
|
|
|
SvnResolver._cache = {
|
|
branches: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
|
|
tags: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
|
|
versions: new LRU({ max: 50, maxAge: 5 * 60 * 1000 })
|
|
};
|
|
|
|
module.exports = SvnResolver;
|