mirror of
https://github.com/bower/bower.git
synced 2026-01-14 08:47:54 -05:00
331 lines
11 KiB
JavaScript
331 lines
11 KiB
JavaScript
var util = require('util');
|
|
var url = require('url');
|
|
var Q = require('q');
|
|
var mout = require('mout');
|
|
var LRU = require('lru-cache');
|
|
var GitResolver = require('./GitResolver');
|
|
var cmd = require('../../util/cmd');
|
|
|
|
function GitRemoteResolver(decEndpoint, config, logger) {
|
|
GitResolver.call(this, decEndpoint, config, logger);
|
|
|
|
if (!mout.string.startsWith(this._source, 'file://')) {
|
|
// Trim trailing slashes
|
|
this._source = this._source.replace(/\/+$/, '');
|
|
}
|
|
|
|
// If the name was guessed, remove the trailing .git
|
|
if (this._guessedName && mout.string.endsWith(this._name, '.git')) {
|
|
this._name = this._name.slice(0, -4);
|
|
}
|
|
|
|
// Get the remote of this source
|
|
if (!/:\/\//.test(this._source)) {
|
|
this._remote = url.parse('ssh://' + this._source);
|
|
} else {
|
|
this._remote = url.parse(this._source);
|
|
}
|
|
|
|
this._host = this._remote.host;
|
|
|
|
// Verify whether the server supports shallow cloning
|
|
this._shallowClone = this._supportsShallowCloning;
|
|
}
|
|
|
|
util.inherits(GitRemoteResolver, GitResolver);
|
|
mout.object.mixIn(GitRemoteResolver, GitResolver);
|
|
|
|
// -----------------
|
|
|
|
GitRemoteResolver.prototype._checkout = function() {
|
|
var promise;
|
|
var timer;
|
|
var reporter;
|
|
var that = this;
|
|
var resolution = this._resolution;
|
|
|
|
this._logger.action(
|
|
'checkout',
|
|
resolution.tag || resolution.branch || resolution.commit,
|
|
{
|
|
resolution: resolution,
|
|
to: this._tempDir
|
|
}
|
|
);
|
|
|
|
// If resolution is a commit, we need to clone the entire repo and check it out
|
|
// Because a commit is not a named ref, there's no better solution
|
|
if (resolution.type === 'commit') {
|
|
promise = this._slowClone(resolution);
|
|
// Otherwise we are checking out a named ref so we can optimize it
|
|
} else {
|
|
promise = this._fastClone(resolution);
|
|
}
|
|
|
|
// 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) {
|
|
that._suggestProxyWorkaround(err);
|
|
throw err;
|
|
})
|
|
// Clear timer at the end
|
|
.fin(function() {
|
|
clearTimeout(timer);
|
|
reporter.cancel();
|
|
})
|
|
);
|
|
};
|
|
|
|
GitRemoteResolver.prototype._findResolution = function(target) {
|
|
var that = this;
|
|
|
|
// Override this function to include a meaningful message related to proxies
|
|
// if necessary
|
|
return GitResolver.prototype._findResolution
|
|
.call(this, target)
|
|
.fail(function(err) {
|
|
that._suggestProxyWorkaround(err);
|
|
throw err;
|
|
});
|
|
};
|
|
|
|
// ------------------------------
|
|
|
|
GitRemoteResolver.prototype._slowClone = function(resolution) {
|
|
return cmd('git', [
|
|
'clone',
|
|
this._source,
|
|
this._tempDir,
|
|
'--progress'
|
|
]).then(
|
|
cmd.bind(cmd, 'git', ['checkout', resolution.commit], {
|
|
cwd: this._tempDir
|
|
})
|
|
);
|
|
};
|
|
|
|
GitRemoteResolver.prototype._fastClone = function(resolution) {
|
|
var branch,
|
|
args,
|
|
that = this;
|
|
|
|
branch = resolution.tag || resolution.branch;
|
|
args = ['clone', this._source, '-b', branch, '--progress', '.'];
|
|
|
|
return this._shallowClone().then(function(shallowCloningSupported) {
|
|
// If the host does not support shallow clones, we don't use --depth=1
|
|
if (
|
|
shallowCloningSupported &&
|
|
!GitRemoteResolver._noShallow.get(that._host)
|
|
) {
|
|
args.push('--depth', 1);
|
|
}
|
|
|
|
return cmd('git', args, { cwd: that._tempDir }).spread(
|
|
function(stdout, stderr) {
|
|
// Only after 1.7.10 --branch accepts tags
|
|
// Detect those cases and inform the user to update git otherwise it's
|
|
// a lot slower than newer versions
|
|
if (!/branch .+? not found/i.test(stderr)) {
|
|
return;
|
|
}
|
|
|
|
that._logger.warn(
|
|
'old-git',
|
|
'It seems you are using an old version of git, it will be slower and propitious to errors!'
|
|
);
|
|
return cmd('git', ['checkout', resolution.commit], {
|
|
cwd: that._tempDir
|
|
});
|
|
},
|
|
function(err) {
|
|
// Some git servers do not support shallow clones
|
|
// When that happens, we mark this host and try again
|
|
if (
|
|
!GitRemoteResolver._noShallow.has(that._source) &&
|
|
err.details &&
|
|
/(rpc failed|shallow|--depth)/i.test(err.details)
|
|
) {
|
|
GitRemoteResolver._noShallow.set(that._host, true);
|
|
return that._fastClone(resolution);
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
);
|
|
});
|
|
};
|
|
|
|
GitRemoteResolver.prototype._suggestProxyWorkaround = function(err) {
|
|
if (
|
|
(this._config.proxy || this._config.httpsProxy) &&
|
|
mout.string.startsWith(this._source, 'git://') &&
|
|
err.code === 'ECMDERR' &&
|
|
err.details
|
|
) {
|
|
err.details = err.details.trim();
|
|
err.details +=
|
|
'\n\nWhen under a proxy, you must configure git to use https:// instead of git://.';
|
|
err.details +=
|
|
'\nYou can configure it for every endpoint or for this specific host as follows:';
|
|
err.details += '\ngit config --global url."https://".insteadOf git://';
|
|
err.details +=
|
|
'\ngit config --global url."https://' +
|
|
this._host +
|
|
'".insteadOf git://' +
|
|
this._host;
|
|
err.details +=
|
|
'Ignore this suggestion if you already have this configured.';
|
|
}
|
|
};
|
|
|
|
// Verifies whether the server supports shallow cloning.
|
|
// This is done according to the rules found in the following links:
|
|
// * https://github.com/dimitri/el-get/pull/1921/files
|
|
// * http://stackoverflow.com/questions/9270488/is-it-possible-to-detect-whether-a-http-git-remote-is-smart-or-dumb
|
|
//
|
|
// Summary of the rules:
|
|
// * Protocols like ssh or git always support shallow cloning
|
|
// * HTTP-based protocols can be verified by sending a HEAD or GET request to the URI (appended to the URL of the Git repo):
|
|
// /info/refs?service=git-upload-pack
|
|
// * If the server responds with a 'Content-Type' header of 'application/x-git-upload-pack-advertisement',
|
|
// the server supports shallow cloning ("smart server")
|
|
// * If the server responds with a different content type, the server does not support shallow cloning ("dumb server")
|
|
// * Instead of doing the HEAD or GET request using an HTTP client, we're letting Git and Curl do the heavy lifting.
|
|
// Calling Git with the GIT_CURL_VERBOSE=2 env variable will provide the Git and Curl output, which includes
|
|
// the content type. This has the advantage that Git will take care of using stored credentials and any additional
|
|
// negotiation that needs to take place.
|
|
//
|
|
// The above should cover most cases, including BitBucket.
|
|
GitRemoteResolver.prototype._supportsShallowCloning = function() {
|
|
var value = true;
|
|
|
|
// Verify that the remote could be parsed and that a protocol is set
|
|
// This case is unlikely, but let's still cover it.
|
|
if (this._remote == null || this._remote.protocol == null) {
|
|
return Q.resolve(false);
|
|
}
|
|
|
|
if (
|
|
!this._host ||
|
|
!this._config.shallowCloneHosts ||
|
|
this._config.shallowCloneHosts.indexOf(this._host) === -1
|
|
) {
|
|
return Q.resolve(false);
|
|
}
|
|
|
|
// Check for protocol - the remote check for hosts supporting shallow cloning is only required for
|
|
// HTTP or HTTPS, not for Git or SSH.
|
|
// Also check for hosts that have been checked in a previous request and have been found to support
|
|
// shallow cloning.
|
|
if (
|
|
mout.string.startsWith(this._remote.protocol, 'http') &&
|
|
!GitRemoteResolver._canShallow.get(this._host)
|
|
) {
|
|
// Provide GIT_CURL_VERBOSE=2 environment variable to capture curl output.
|
|
// Calling ls-remote includes a call to the git-upload-pack service, which returns the content type in the response.
|
|
var processEnv = mout.object.merge(process.env, {
|
|
GIT_CURL_VERBOSE: '2'
|
|
});
|
|
|
|
value = cmd('git', ['ls-remote', '--heads', this._source], {
|
|
env: processEnv
|
|
}).spread(
|
|
function(stdout, stderr) {
|
|
// Check stderr for content-type, ignore stdout
|
|
var isSmartServer;
|
|
|
|
// If the content type is 'x-git', then the server supports shallow cloning
|
|
isSmartServer = mout.string.contains(
|
|
stderr,
|
|
'Content-Type: application/x-git-upload-pack-advertisement'
|
|
);
|
|
|
|
this._logger.debug(
|
|
'detect-smart-git',
|
|
'Smart Git host detected: ' + isSmartServer
|
|
);
|
|
|
|
if (isSmartServer) {
|
|
// Cache this host
|
|
GitRemoteResolver._canShallow.set(this._host, true);
|
|
}
|
|
|
|
return isSmartServer;
|
|
}.bind(this)
|
|
);
|
|
} else {
|
|
// One of the following cases:
|
|
// * A non-HTTP/HTTPS protocol
|
|
// * A host that has been checked before and that supports shallow cloning
|
|
return Q.resolve(true);
|
|
}
|
|
|
|
return value;
|
|
};
|
|
|
|
// ------------------------------
|
|
|
|
// Grab refs remotely
|
|
GitRemoteResolver.refs = function(source) {
|
|
var value;
|
|
|
|
// TODO: Normalize source because of the various available protocols?
|
|
value = this._cache.refs.get(source);
|
|
if (value) {
|
|
return Q.resolve(value);
|
|
}
|
|
|
|
// Store the promise in the refs object
|
|
value = cmd('git', ['ls-remote', '--tags', '--heads', source]).spread(
|
|
function(stdout) {
|
|
var refs;
|
|
|
|
refs = stdout
|
|
.toString()
|
|
.trim() // Trim trailing and leading spaces
|
|
.replace(/[\t ]+/g, ' ') // Standardize spaces (some git versions make tabs, other spaces)
|
|
.split(/[\r\n]+/); // Split lines into an array
|
|
|
|
// Update the refs with the actual refs
|
|
this._cache.refs.set(source, refs);
|
|
|
|
return refs;
|
|
}.bind(this)
|
|
);
|
|
|
|
// Store the promise to be reused until it resolves
|
|
// to a specific value
|
|
this._cache.refs.set(source, value);
|
|
|
|
return value;
|
|
};
|
|
|
|
// Store hosts that do not support shallow clones here
|
|
GitRemoteResolver._noShallow = new LRU({ max: 50, maxAge: 5 * 60 * 1000 });
|
|
|
|
// Store hosts that support shallow clones here
|
|
GitRemoteResolver._canShallow = new LRU({ max: 50, maxAge: 5 * 60 * 1000 });
|
|
|
|
module.exports = GitRemoteResolver;
|