From a4fbe65259d7aec402020ff9072ec8937f6f68ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Cruz?= Date: Sun, 14 Apr 2013 15:27:08 +0100 Subject: [PATCH] Implemented GitFsResolver, minor refactor to the GitRemoteResolver. --- lib/resolve/resolvers/GitFsResolver.js | 94 +++++++++++++----- lib/resolve/resolvers/GitRemoteResolver.js | 107 ++++++++++----------- lib/util/cmd.js | 31 +++--- lib/util/copy.js | 55 +++++++++++ package.json | 3 +- test/test.js | 32 ++++-- 6 files changed, 222 insertions(+), 100 deletions(-) create mode 100644 lib/util/copy.js diff --git a/lib/resolve/resolvers/GitFsResolver.js b/lib/resolve/resolvers/GitFsResolver.js index 78fdc394..79be5043 100644 --- a/lib/resolve/resolvers/GitFsResolver.js +++ b/lib/resolve/resolvers/GitFsResolver.js @@ -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; diff --git a/lib/resolve/resolvers/GitRemoteResolver.js b/lib/resolve/resolvers/GitRemoteResolver.js index c8022a34..aee670ef 100644 --- a/lib/resolve/resolvers/GitRemoteResolver.js +++ b/lib/resolve/resolvers/GitRemoteResolver.js @@ -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]; diff --git a/lib/util/cmd.js b/lib/util/cmd.js index 77c20eb7..f2c9df14 100644 --- a/lib/util/cmd.js +++ b/lib/util/cmd.js @@ -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); }); -}; \ No newline at end of file + + return deferred.promise; +} + +module.exports = cmd; \ No newline at end of file diff --git a/lib/util/copy.js b/lib/util/copy.js new file mode 100644 index 00000000..e1a3886d --- /dev/null +++ b/lib/util/copy.js @@ -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; \ No newline at end of file diff --git a/package.json b/package.json index d7901230..9a8742d6 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "rc": "~0.1.0", "mkdirp": "~0.3.5", "nopt": "~2.1.1", - "semver": "~1.1.4" + "semver": "~1.1.4", + "fstream": "~0.1.22" }, "devDependencies": { "mocha": "~1.8.2", diff --git a/test/test.js b/test/test.js index 9140e657..e7f89441 100644 --- a/test/test.js +++ b/test/test.js @@ -1,11 +1,12 @@ -var GitResolver = require('../lib/resolve/resolvers/GitRemoteResolver'); +var GitRemoteResolver = require('../lib/resolve/resolvers/GitRemoteResolver'); +var GitFsResolver = require('../lib/resolve/resolvers/GitFsResolver'); -function testGitResolver() { - var dejavuResolver = new GitResolver('git://github.com/IndigoUnited/dejavu.git', { +function testGitRemoteResolver() { + var dejavuResolver = new GitRemoteResolver('git://github.com/IndigoUnited/dejavu.git', { name: 'dejavu', - //target: '962be0f7b779b061eccce6a661928cb719031964' - //target: 'master' - target: '~0.4.1' + //target: '7d07190ca6fb7ffa63642526537e0c314cbaab12' + target: 'master' + //target: '~0.4.1' }); return dejavuResolver.resolve() @@ -16,6 +17,23 @@ function testGitResolver() { }); } +function testGitLocalResolver() { + var bowerResolver = new GitFsResolver('.', { + name: 'bower', + target: '*' + }); + + return bowerResolver.resolve() + .then(function () { + console.log('ok!'); + }, function (err) { + console.log('failed to resolve', err); + }); +} + if (process.argv[1] && !/mocha/.test(process.argv[1])) { - testGitResolver(); + testGitRemoteResolver() + .then(testGitLocalResolver); + + //testGitLocalResolver(); } \ No newline at end of file