mirror of
https://github.com/bower/bower.git
synced 2026-01-14 16:57:57 -05:00
435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
var util = require('util');
|
|
var path = require('path');
|
|
var Q = require('q');
|
|
var rimraf = require('../../util/rimraf');
|
|
var mkdirp = require('mkdirp');
|
|
var which = require('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 hasGit;
|
|
|
|
// Check if git is installed
|
|
try {
|
|
which.sync('git');
|
|
hasGit = true;
|
|
} catch (ex) {
|
|
hasGit = false;
|
|
}
|
|
|
|
function GitResolver(decEndpoint, config, logger) {
|
|
// Set template dir to the empty directory so that user templates are not run
|
|
// This environment variable is not multiple config aware but it's not documented
|
|
// anyway
|
|
mkdirp.sync(config.storage.empty);
|
|
process.env.GIT_TEMPLATE_DIR = config.storage.empty;
|
|
|
|
if (!config.strictSsl) {
|
|
process.env.GIT_SSL_NO_VERIFY = 'true';
|
|
}
|
|
|
|
if (!config.interactive) {
|
|
process.env.GIT_TERMINAL_PROMPT = '0';
|
|
|
|
if (!process.env.SSH_ASKPASS) {
|
|
process.env.SSH_ASKPASS = 'echo';
|
|
}
|
|
}
|
|
|
|
Resolver.call(this, decEndpoint, config, logger);
|
|
|
|
if (!hasGit) {
|
|
throw createError('git is not installed or not in the PATH', 'ENOGIT');
|
|
}
|
|
}
|
|
|
|
util.inherits(GitResolver, Resolver);
|
|
mout.object.mixIn(GitResolver, Resolver);
|
|
|
|
// -----------------
|
|
|
|
GitResolver.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;
|
|
});
|
|
};
|
|
|
|
GitResolver.prototype._resolve = function() {
|
|
var that = this;
|
|
|
|
return this._findResolution().then(function() {
|
|
return (
|
|
that
|
|
._checkout()
|
|
// Always run cleanup after checkout to ensure that .git is removed!
|
|
// If it's not removed, problems might arise when the "tmp" module attempts
|
|
// to delete the temporary folder
|
|
.fin(function() {
|
|
return that._cleanup();
|
|
})
|
|
);
|
|
});
|
|
};
|
|
|
|
// -----------------
|
|
|
|
// Abstract functions that should be implemented by concrete git resolvers
|
|
GitResolver.prototype._checkout = function() {
|
|
throw new Error('_checkout not implemented');
|
|
};
|
|
|
|
GitResolver.refs = function(source) {
|
|
throw new Error('refs not implemented');
|
|
};
|
|
|
|
// -----------------
|
|
|
|
GitResolver.prototype._findResolution = function(target) {
|
|
var err;
|
|
var self = this.constructor;
|
|
var that = this;
|
|
|
|
target = target || this._target || '*';
|
|
|
|
// 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)) {
|
|
this._resolution = { type: 'commit', commit: target };
|
|
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;
|
|
|
|
// If there are no tags and target is *,
|
|
// fallback to the latest commit on master
|
|
if (!versions.length && target === '*') {
|
|
return that._findResolution('master');
|
|
}
|
|
|
|
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]
|
|
});
|
|
}
|
|
|
|
if (/^[a-f0-9]{4,40}$/.test(target)) {
|
|
if (target.length < 12) {
|
|
that._logger.warn(
|
|
'short-sha',
|
|
'Consider using longer commit SHA to avoid conflicts'
|
|
);
|
|
}
|
|
|
|
that._resolution = { type: 'commit', commit: target };
|
|
return that._resolution;
|
|
}
|
|
|
|
branches = Object.keys(branches);
|
|
tags = Object.keys(tags);
|
|
|
|
err = createError(
|
|
'Tag/branch ' + 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;
|
|
}
|
|
);
|
|
};
|
|
|
|
GitResolver.prototype._cleanup = function() {
|
|
var gitFolder = path.join(this._tempDir, '.git');
|
|
|
|
return Q.nfcall(rimraf, gitFolder);
|
|
};
|
|
|
|
GitResolver.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.valid(meta.version) &&
|
|
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.substr(0, 10);
|
|
|
|
// Save resolution to be used in hasNew later
|
|
meta._resolution = this._resolution;
|
|
|
|
return Resolver.prototype._savePkgMeta.call(this, meta);
|
|
};
|
|
|
|
// ------------------------------
|
|
|
|
GitResolver.versions = function(source, extra) {
|
|
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;
|
|
};
|
|
|
|
GitResolver.tags = function(source) {
|
|
var value = this._cache.tags.get(source);
|
|
|
|
if (value) {
|
|
return Q.resolve(value);
|
|
}
|
|
|
|
value = this.refs(source).then(
|
|
function(refs) {
|
|
var tags = {};
|
|
|
|
// For each line in the refs, match only the tags
|
|
refs.forEach(function(line) {
|
|
var match = line.match(/^([a-f0-9]{40})\s+refs\/tags\/(\S+)/);
|
|
|
|
if (match && !mout.string.endsWith(match[2], '^{}')) {
|
|
tags[match[2]] = match[1];
|
|
}
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
GitResolver.branches = function(source) {
|
|
var value = this._cache.branches.get(source);
|
|
|
|
if (value) {
|
|
return Q.resolve(value);
|
|
}
|
|
|
|
value = this.refs(source).then(
|
|
function(refs) {
|
|
var branches = {};
|
|
|
|
// For each line in the refs, extract only the heads
|
|
// Organize them in an object where keys are branches and values
|
|
// the commit hashes
|
|
refs.forEach(function(line) {
|
|
var match = line.match(/^([a-f0-9]{40})\s+refs\/heads\/(\S+)/);
|
|
|
|
if (match) {
|
|
branches[match[2]] = match[1];
|
|
}
|
|
});
|
|
|
|
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;
|
|
};
|
|
|
|
GitResolver.clearRuntimeCache = function() {
|
|
// Reset cache for branches, tags, etc
|
|
mout.object.forOwn(GitResolver._cache, function(lru) {
|
|
lru.reset();
|
|
});
|
|
};
|
|
|
|
GitResolver._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 }),
|
|
refs: new LRU({ max: 50, maxAge: 5 * 60 * 1000 })
|
|
};
|
|
|
|
module.exports = GitResolver;
|