Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial work on "changelog" op. still lots to do

  • Loading branch information...
commit 2da5393ad6c80ad69ff7bfdd30fe5dc1b8fe9f66 1 parent 3ebea8b
@drewfish authored
Showing with 394 additions and 12 deletions.
  1. +5 −6 Readme.md
  2. +387 −6 lib/crank.js
  3. +2 −0  package.json
View
11 Readme.md
@@ -93,15 +93,15 @@ a tool to update version number and changelog, for npm module development
* format to use for change dates
* `filters`: list of regexes to modify changes
* `subject`: "message", "author", "date", "changeid"
- * `regex`: string
+ * `regexp`: string
* `replace`: "string"
* if results in "--CRANK:SKIP--" then change is skipped
* `versions`:
* `dateformat`: string
* format to use for revision dates
* `filters`: list of regeses to modify versions
- * `subject`: "version", "date"
- * `regex`: string
+ * `subject`: "version", "date", "changes"
+ * `regexp`: string
* `replace`: "string"
* if results in "--CRANK:SKIP--" then version is skipped
* `database` -- TODO
@@ -112,7 +112,7 @@ a tool to update version number and changelog, for npm module development
* separate json file
* `changelog`
* uses changelog file itself to find version:changeid pairs
- * TODO: need way to pull pairs out of changelog (regex?)
+ * TODO: need way to pull pairs out of changelog (regexp?)
* `scm-changes`
* uses SCM change log to find version:changeid pairs
* TODO: need way to pull pairs out of SCM change log
@@ -123,8 +123,7 @@ a tool to update version number and changelog, for npm module development
* good old mustache
* `{{version}}` string
-* `{{date}}` string
-* `{{crankdate}}` string, date that crank was run
+* `{{date}}` string, date that crank was run
* `{{changes}}` list of objects
* `{{changeid}}` string
* `{{date}}` string
View
393 lib/crank.js
@@ -24,11 +24,16 @@
var
+ libasync = require('async'),
libfs = require('fs'),
+ libmu = require('mu'),
libpath = require('path'),
+ libprocess = require('child_process'),
libsemver = require('semver'),
- OPS = {};
+ OPS = {},
+ SCMS = {},
+ CHANGELOG_DBS = {};
@@ -41,6 +46,16 @@ function objectMerge(dest, changes) {
}
+function stringPadLeft(len, str) {
+ var xtra = len - str.length;
+ if (xtra <= 0) {
+ return str;
+ }
+ return str + Array(xtra + 1).join(' ');
+}
+
+
+
//------------------------------------------------------------------------
// base functionality common to all commands
@@ -63,7 +78,11 @@ function Base(options) {
//TODO dateformat: 'TODO',
filters: []
},
- database: 'TODO'
+ database: {
+ type: 'regexp',
+ file: 'Changelog.md',
+ regexp: '\\bversion\\s+([0-9.]+)[\\s\\S]*\\bchange\\s+(\\S+)'
+ }
}
};
@@ -73,6 +92,15 @@ function Base(options) {
}
+Base.prototype.fileRead = function(path) {
+ var content;
+ if (! libpath.existsSync(path)) {
+ return null;
+ }
+ return libfs.readFileSync(path, 'utf-8');
+};
+
+
Base.prototype.fileReadJSON = function(path) {
var content;
if (! libpath.existsSync(path)) {
@@ -80,7 +108,7 @@ Base.prototype.fileReadJSON = function(path) {
}
content = libfs.readFileSync(path, 'utf-8');
return JSON.parse(content);
-}
+};
Base.prototype.fileWriteJSON = function(path, content) {
@@ -90,7 +118,196 @@ Base.prototype.fileWriteJSON = function(path, content) {
}
content = JSON.stringify(content, null, 4);
return libfs.writeFileSync(path, content, 'utf-8');
-}
+};
+
+
+Base.prototype.scmGetCurrentChangeID = function(cb) {
+ var me = this;
+ this._scmInit(function(error) {
+ if (error) {
+ cb(error);
+ }
+ else {
+ me._scm.getCurrentChangeID(cb);
+ }
+ });
+};
+
+
+Base.prototype.scmListChanges = function(fromChange, toChange, cb) {
+ var me = this;
+ this._scmInit(function(error) {
+ if (error) {
+ cb(error);
+ }
+ else {
+ me._scm.listChanges(fromChange, toChange, cb);
+ }
+ });
+};
+
+
+Base.prototype._scmInit = function(cb) {
+ var i, scm;
+ if (this._scm) {
+ cb(null);
+ return;
+ }
+ for (i in SCMS) {
+ if (SCMS.hasOwnProperty(i)) {
+ scm = new SCMS[i](this);
+ // TODO: make this interface async as well
+ if (scm.init(this.config.target)) {
+ this._scm = scm;
+ cb(null);
+ return;
+ }
+ }
+ }
+ cb(new Error('FAILED to find suitable SCM'));
+};
+
+
+ //--------------------------------------------------------
+ // changelog scms
+ // duck typing methods:
+ // * init(target)
+ // returns false if SCM doesn't apply
+ // * getCurrentChangeID(cb(error,changeID))
+ // returns the current change ID of the target
+ // * listChanges(fromChange, toChange, cb(error,list))
+ // returns changes as array, skipping fromChange including toCHange
+
+
+ //--------------------------------------------------------
+ // git
+ function SCMGit(base) {
+ this.base = base;
+ }
+ SCMS.git = SCMGit;
+
+
+ SCMGit.prototype.init = function(target) {
+ var parts, i, path, repo;
+ if ('/' !== target.substring(0, 1)) {
+ target = libpath.join(process.cwd(), target);
+ }
+ parts = target.split('/');
+ parts.shift(); // empty part before the first slash
+ for (i = parts.length; i > 0; i--) {
+ // TODO -- use libpath
+ path = '/' + parts.slice(0, i).join('/');
+ repo = libpath.join(path, '.git');
+ if (libpath.existsSync(repo)) {
+ this.target = target;
+ this.repo = repo;
+ return true;
+ }
+ }
+ return false;
+ };
+
+
+ SCMGit.prototype.getCurrentChangeID = function(cb) {
+ this._readRef('HEAD', cb);
+ };
+
+
+ SCMGit.prototype.listChanges = function(fromChange, toChange, cb) {
+ var me = this, cmd;
+ cmd = 'git log --pretty=raw --date-order ' + fromChange + '..' + toChange + ' ' + this.target;
+ libprocess.exec(cmd, function(error, stdout, stderr) {
+ if (error) {
+ cb(error);
+ return;
+ }
+ cb(null, me._parseLog(stdout));
+ });
+ };
+
+
+ SCMGit.prototype._readRef = function(ref, cb) {
+ var change;
+ var matches;
+ change = this.base.fileRead(libpath.join(this.repo, ref));
+ if (matches = change.match(/ref: (\S+)/)) {
+ this._readRef(matches[1], cb);
+ return;
+ }
+ cb(null, change.trim());
+ };
+
+
+ SCMGit.prototype._parseLog = function(raw) {
+ var lines, i, line, lineParts,
+ currentCommit = { message:[] },
+ changes = [];
+ lines = raw.split('\n');
+
+ // also converts to crank format
+ function saveCurrentCommit() {
+ var crank = {},
+ who, whoParts, whoTime;
+
+ who = currentCommit.committer || currentCommit.author;
+ whoParts = who.split(' ');
+ whoParts.pop(); // timezone offset
+ whoTime = whoParts.pop();
+ who = whoParts.join(' ');
+
+ // convert into crank format
+ crank.changeid = currentCommit.commit;
+ // FUTURE: support full message (probably a new config flag)
+ crank.message = currentCommit.message[0];
+ crank.author = who;
+ crank.date = new Date(parseInt(whoTime,10) * 1000);
+
+ changes.push(crank);
+ currentCommit = { message:[] };
+ }
+
+ for (i = 0; i < lines.length; i++) {
+ line = lines[i];
+ if (! line) {
+ continue;
+ }
+ lineParts = line.split(' ');
+ if ('' === lineParts[0]) {
+ currentCommit.message.push(line.substr(4));
+ continue;
+ }
+ if ('commit' === lineParts[0] && currentCommit.commit) {
+ saveCurrentCommit();
+ }
+ currentCommit[lineParts.shift()] = lineParts.join(' ');
+ }
+ if (currentCommit.commit) {
+ saveCurrentCommit();
+ }
+ return changes;
+ };
+
+
+ //--------------------------------------------------------
+ // svn
+ function SCMSvn() {}
+ SCMS.svn = SCMSvn;
+
+
+ SCMSvn.prototype.init = function(target) {
+ // TODO
+ return false;
+ };
+
+
+ SCMSvn.prototype.getCurrentChangeID = function(cb) {
+ // TODO
+ };
+
+
+ SCMSvn.prototype.listChanges = function(fromChange, toChange, cb) {
+ // TODO
+ };
@@ -138,6 +355,162 @@ OPVersion.prototype.run = function(command) {
//------------------------------------------------------------------------
+// crank the changelog
+
+function OPChangelog(base) {
+ this.base = base;
+}
+OPS.changelog = OPChangelog;
+
+
+OPChangelog.description = 'updates changelog with latest changes';
+
+
+// no options right now
+OPChangelog.options = {};
+
+
+OPChangelog.prototype.usage = function(command) {
+ // TODO
+};
+
+
+OPChangelog.prototype.run = function(command) {
+ var me = this;
+ var db;
+ var mapChangeVersion;
+ var currentChangeID;
+ var latestChangeID;
+ var changes;
+ var latestVersion;
+
+ db = makeChangelogDB(this.base, this.base.config.changelog.database);
+
+ libasync.series([
+
+ // read db
+ function step1(cb) {
+console.log('--STEP1-- read db');
+ db.read(function(error, map) {
+ if (error) {
+ cb(error);
+ return;
+ }
+ mapChangeVersion = map;
+console.log(mapChangeVersion);
+ cb();
+ });
+ },
+
+ // get changeid of target
+ function step2(cb) {
+console.log('--STEP2-- get changeid of target');
+ me.base.scmGetCurrentChangeID(function(error, changeid) {
+ if (error) {
+ cb(error);
+ return;
+ }
+ currentChangeID = changeid;
+ // if exists in db, bail:done
+ if (mapChangeVersion[currentChangeID]) {
+ return;
+ }
+ cb();
+ });
+ },
+
+ // get latest changeid in db
+ function step3(cb) {
+console.log('--STEP3-- get latest changeid in db');
+ latestChangeID = mapChangeVersion[Object.keys(mapChangeVersion)[0]];
+ cb();
+ },
+
+ // list log entries from last-rev to current-rev
+ function step4(cb) {
+console.log('--STEP4-- list log entries from last-rev to current-rev');
+ me.base.scmListChanges(latestChangeID, currentChangeID, function(error, list) {
+ if (error) {
+ cb(error);
+ return;
+ }
+ changes = list;
+//console.log(changes);
+ cb();
+ });
+ },
+
+ // get latest version
+ function step5(cb) {
+console.log('--STEP5-- get latest version');
+ cb();
+ },
+
+ // filter/transform/templatize changes
+ function step6(cb) {
+console.log('--STEP6-- filter/transform/templatize changes');
+ cb();
+ },
+
+ // update changelog
+ function step7(cb) {
+console.log('--STEP7-- update changelog');
+// fileRead
+// prepend
+// fileWrite
+ cb();
+ },
+
+ // update db
+ function step8(cb) {
+console.log('--STEP8-- update db');
+ db.update(latestVersion, latestChangeID, cb);
+ },
+
+ ]);
+};
+
+
+ //--------------------------------------------------------
+ // changelog database
+ function makeChangelogDB(base, config) {
+ return new CHANGELOG_DBS[config.type](base, config);
+ }
+ // duck typing methods:
+ // * read(cb(error,db))
+ // returns an array of pairs (TODO: what is a pair)
+ // * update(version, change, db(error))
+ // returns success as boolean
+
+
+ //--------------------------------------------------------
+ // data stored in changelog, pulled out via RegExps
+ function ChangelogDBRegexp(base, config) {
+ this.base = base;
+ this.config = config;
+ this.regexp = new RegExp(config.regexp, 'g');
+ }
+ CHANGELOG_DBS.regexp = ChangelogDBRegexp;
+
+
+ ChangelogDBRegexp.prototype.read = function(cb) {
+ var content, matches, db = {};
+ content = this.base.fileRead(this.config.file);
+ while (matches = this.regexp.exec(content)) {
+ db[matches[1]] = matches[2];
+ }
+ cb(null, db);
+ };
+
+
+ ChangelogDBRegexp.prototype.update = function(version, change, cb) {
+ // noop because the info has already been written to the changelog file
+ cb();
+ };
+
+
+
+//------------------------------------------------------------------------
// main public API
function parseArgs(args) {
@@ -181,7 +554,7 @@ function error(error) {
function usage(command) {
- var base, op;
+ var base, op, max;
if (command.error) {
error(command.error);
}
@@ -200,8 +573,16 @@ function usage(command) {
+ ' (defaults to config.json)\n'
+ '\n'
+ 'COMMANDS');
+ max = 0;
+ for (op in OPS) {
+ if (OPS.hasOwnProperty(op)) {
+ max = Math.max(max, op.length);
+ }
+ }
for (op in OPS) {
- console.log(' ' + op + ' ' + OPS[op].description);
+ if (OPS.hasOwnProperty(op)) {
+ console.log(' ' + stringPadLeft(max, op) + ' ' + OPS[op].description);
+ }
}
console.log();
}
View
2  package.json
@@ -27,6 +27,8 @@
},
"devDependencies": {},
"dependencies": {
+ "async": ">= 0.1.18",
+ "mu": ">= 0.5.2",
"semver": ">= 1.0.13"
},
"main": "lib/crank",

0 comments on commit 2da5393

Please sign in to comment.
Something went wrong with that request. Please try again.