Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract publish task logic from grunt-gh-pages #1

Merged
merged 1 commit into from
May 30, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 199 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,201 @@
var path = require('path');

var Q = require('q');
var wrench = require('wrench');
var _ = require('lodash');
var grunt = require('grunt');

/**
* Generate promises for spawned git commands.
*/
exports.git = require('./git');
var pkg = require('../package.json');
var git = require('./git');

var copy = require('./util').copy;

function getCacheDir() {
return '.gh-pages';
}

function getRemoteUrl(dir, remote) {
var repo;
return git(['config', '--get', 'remote.' + remote + '.url'], dir)
.progress(function(chunk) {
repo = String(chunk).split(/[\n\r]/).shift();
})
.then(function() {
if (repo) {
return Q.resolve(repo);
} else {
return Q.reject(new Error(
'Failed to get repo URL from options or current directory.'));
}
})
.fail(function(err) {
return Q.reject(new Error(
'Failed to get remote.origin.url (task must either be run in a ' +
'git repository with a configured origin remote or must be ' +
'configured with the "repo" option).'));
});
}

function getRepo(options) {
if (options.repo) {
return Q.resolve(options.repo);
} else {
return getRemoteUrl(process.cwd(), 'origin');
}
}


exports.publish = function publish(config, done) {
var defaults = {
add: false,
git: 'git',
clone: getCacheDir(),
dotfiles: false,
branch: 'gh-pages',
remote: 'origin',
base: process.cwd(),
src: '**/*',
only: '.',
push: true,
message: 'Updates',
silent: false,
logger: function(){}
};

// override defaults with any task options
var options = _.extend({}, defaults, config);

if (!grunt.file.isDir(options.base)) {
return done(new Error('The "base" option must be an existing directory'));
}

var files = grunt.file.expand({
filter: 'isFile',
cwd: options.base,
dot: options.dotfiles
}, options.src);

if (!Array.isArray(files) || files.length === 0) {
return done(new Error('Files must be provided in the "src" property.'));
}

var only = grunt.file.expand({cwd: options.base}, options.only);

function log(message) {
if (!options.silent) {
options.logger(message);
}
}

git.exe(options.git);

var repoUrl;
getRepo(options)
.then(function(repo) {
repoUrl = repo;
log('Cloning ' + repo + ' into ' + options.clone);
return git.clone(repo, options.clone, options.branch, options);
})
.then(function() {
return getRemoteUrl(options.clone, options.remote)
.then(function(url) {
if (url !== repoUrl) {
var message = 'Remote url mismatch. Got "' + url + '" ' +
'but expected "' + repoUrl + '" in ' + options.clone +
'. If you have changed your "repo" option, try ' +
'running `grunt gh-pages-clean` first.';
return Q.reject(new Error(message));
} else {
return Q.resolve();
}
});
})
.then(function() {
// only required if someone mucks with the checkout between builds
log('Cleaning');
return git.clean(options.clone);
})
.then(function() {
log('Fetching ' + options.remote);
return git.fetch(options.remote, options.clone);
})
.then(function() {
log('Checking out ' + options.remote + '/' +
options.branch);
return git.checkout(options.remote, options.branch,
options.clone);
})
.then(function() {
if (!options.add) {
log('Removing files');
return git.rm(only.join(' '), options.clone);
} else {
return Q.resolve();
}
})
.then(function() {
log('Copying files');
return copy(files, options.base, options.clone);
})
.then(function() {
log('Adding all');
return git.add('.', options.clone);
})
.then(function() {
if (options.user) {
return git(['config', 'user.email', options.user.email],
options.clone)
.then(function() {
return git(['config', 'user.name', options.user.name],
options.clone);
});
} else {
return Q.resolve();
}
})
.then(function() {
log('Committing');
return git.commit(options.message, options.clone);
})
.then(function() {
if (options.tag) {
log('Tagging');
var deferred = Q.defer();
git.tag(options.tag, options.clone)
.then(function() {
return deferred.resolve();
})
.fail(function(error) {
// tagging failed probably because this tag alredy exists
log('Tagging failed, continuing');
options.logger(error);
return deferred.resolve();
});
return deferred.promise;
} else {
return Q.resolve();
}
})
.then(function() {
if (options.push) {
log('Pushing');
return git.push(options.remote, options.branch,
options.clone);
} else {
return Q.resolve();
}
})
.then(function() {
done();
}, function(error) {
if (options.silent) {
error = new Error(
'Unspecified error (run without silent option for detail)');
}
done(error);
});
};

exports.clean = function clean() {
wrench.rmdirSyncRecursive(getCacheDir(), true);
};
164 changes: 164 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
var path = require('path');

var async = require('async');
var fs = require('graceful-fs');
var Q = require('q');


/**
* Generate a list of unique directory paths given a list of file paths.
* @param {Array.<string>} files List of file paths.
* @return {Array.<string>} List of directory paths.
*/
var uniqueDirs = exports.uniqueDirs = function(files) {
var dirs = {};
files.forEach(function(filepath) {
var parts = path.dirname(filepath).split(path.sep);
var partial = parts[0];
dirs[partial] = true;
for (var i = 1, ii = parts.length; i < ii; ++i) {
partial = path.join(partial, parts[i]);
dirs[partial] = true;
}
});
return Object.keys(dirs);
};


/**
* Sort function for paths. Sorter paths come first. Paths of equal length are
* sorted alphanumerically in path segment order.
* @param {string} a First path.
* @param {string} b Second path.
* @return {number} Comparison.
*/
var byShortPath = exports.byShortPath = function(a, b) {
var aParts = a.split(path.sep);
var bParts = b.split(path.sep);
var aLength = aParts.length;
var bLength = bParts.length;
var cmp = 0;
if (aLength < bLength) {
cmp = -1;
} else if (aLength > bLength) {
cmp = 1;
} else {
var aPart, bPart;
for (var i = 0; i < aLength; ++i) {
aPart = aParts[i];
bPart = bParts[i];
if (aPart < bPart) {
cmp = -1;
break;
} else if (aPart > bPart) {
cmp = 1;
break;
}
}
}
return cmp;
};


/**
* Generate a list of directories to create given a list of file paths.
* @param {Array.<string>} files List of file paths.
* @return {Array.<string>} List of directory paths ordered by path length.
*/
var dirsToCreate = exports.dirsToCreate = function(files) {
return uniqueDirs(files).sort(byShortPath);
};


/**
* Copy a file.
* @param {Object} obj Object with src and dest properties.
* @param {function(Error)} callback Callback
*/
var copyFile = exports.copyFile = function(obj, callback) {
var called = false;
function done(err) {
if (!called) {
called = true;
callback(err);
}
}

var read = fs.createReadStream(obj.src);
read.on('error', function(err) {
done(err);
});

var write = fs.createWriteStream(obj.dest);
write.on('error', function(err) {
done(err);
});
write.on('close', function(ex) {
done();
});

read.pipe(write);
};


/**
* Make directory, ignoring errors if directory already exists.
* @param {string} path Directory path.
* @param {function(Error)} callback Callback.
*/
function makeDir(path, callback) {
fs.mkdir(path, function(err) {
if (err) {
// check if directory exists
fs.stat(path, function(err2, stat) {
if (err2 || !stat.isDirectory()) {
callback(err);
} else {
callback();
}
});
} else {
callback();
}
});
}


/**
* Copy a list of files.
* @param {Array.<string>} files Files to copy.
* @param {string} base Base directory.
* @param {string} dest Destination directory.
* @return {Promise} A promise.
*/
var copy = exports.copy = function(files, base, dest) {
var deferred = Q.defer();

var pairs = [];
var destFiles = [];
files.forEach(function(file) {
var src = path.resolve(base, file);
var relative = path.relative(base, src);
var target = path.join(dest, relative);
pairs.push({
src: src,
dest: target
});
destFiles.push(target);
});

async.eachSeries(dirsToCreate(destFiles), makeDir, function(err) {
if (err) {
return deferred.reject(err);
}
async.each(pairs, copyFile, function(err) {
if (err) {
return deferred.reject(err);
} else {
return deferred.resolve();
}
});
});

return deferred.promise;
};
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
},
"dependencies": {
"q": "~1.0.1",
"q-io": "~1.11.0"
"q-io": "~1.11.0",
"graceful-fs": "2.0.1",
"async": "0.2.9",
"wrench": "1.5.1",
"lodash": "~2.4.1",
"grunt": "~0.4.5"
},
"devDependencies": {
"glob": "~3.2.9",
Expand Down