From 0dad78e2258b417e6ef8c8e813167b7867448c5b Mon Sep 17 00:00:00 2001 From: Radek Stepan Date: Sat, 5 Oct 2013 12:45:07 +0100 Subject: [PATCH] multiple milestones; refs #23 --- README.md | 1 + build/build.css | 4 +- build/build.js | 70 ++++++++++++++++++++++++----------- src/app.coffee | 10 +++-- src/modules/issues.coffee | 4 +- src/modules/milestones.coffee | 39 +++++++++++++------ src/modules/regex.coffee | 4 +- src/modules/repo.coffee | 4 +- src/modules/request.coffee | 13 ++++++- src/styles/app.styl | 4 +- src/templates/info.eco | 3 +- test/issues.coffee | 2 +- test/milestones.coffee | 59 ++++++++++++++++++++++++----- test/request.coffee | 47 +++++++++++++++++++++++ 14 files changed, 204 insertions(+), 60 deletions(-) create mode 100644 test/request.coffee diff --git a/README.md b/README.md index 4775371..67eb772 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Displays a burndown chart from a set of GitHub issues in the current milestone. 1. Private repos; use your GitHub API Token hiding it from public view if need be. 1. Off days; specify which days of the week to leave out from ideal burndown progression line. 1. Trend line; to see if you can make it to the deadline at this pace. +1. Multiple milestones; watch multiple milestones per repo, e.g. when using them for tracking epics. ![image](https://raw.github.com/radekstepan/github-burndown-chart/master/example.png) diff --git a/build/build.css b/build/build.css index 968c6a3..b399fae 100644 --- a/build/build.css +++ b/build/build.css @@ -601,11 +601,11 @@ h2 { padding: 0 20px 20px; } .box p { - margin: 0; + margin: 5px 0; padding: 0 20px; } .box p.description { - margin-top: -10px; + margin: -10px 0 0 0; } #graph { height: 200px; diff --git a/build/build.js b/build/build.js index 146f7d9..44ddbfc 100644 --- a/build/build.js +++ b/build/build.js @@ -29098,17 +29098,22 @@ render = require('./modules/render'); repo = require('./modules/repo'); route = function() { - var match, path; + var m, match, opts, path, r, u, _ref; if (match = window.location.hash.match(regex.location)) { - path = match.slice(1, 4).join('/'); + path = match[1].slice(1); render('body', 'loading', { path: path }); + _ref = path.split('/'), u = _ref[0], r = _ref[1], m = _ref[2]; + opts = m ? { + 'path': "" + u + "/" + r, + 'milestone': m + } : { + path: path + }; return async.waterfall([ config, function(conf, cb) { - return repo(_.extend({ - path: path - }, conf), cb); + return repo(_.extend(opts, conf), cb); } ], function(err) { if (err) { @@ -29446,9 +29451,6 @@ module.exports = { if (err) { return cb(err); } - if (data.message) { - return cb(data.message); - } if (!data.length) { return cb(null, results); } @@ -29518,16 +29520,31 @@ marked = require('marked'); request = require('./request'); -module.exports = { - 'get_current': function(repo, cb) { +module.exports = function(repo, cb) { + var parse; + parse = function(data) { + if (data.description) { + data.description = marked(data.description).slice(3, -5); + } + return data; + }; + if (repo.milestone) { + return request.one_milestone(repo, repo.milestone, function(err, m) { + if (err) { + return cb(err); + } + if (m.open_issues + m.closed_issues === 0) { + return cb(null, "No issues for milestone `" + m.title + "`"); + } + m = parse(m); + return cb(null, null, m); + }); + } else { return request.all_milestones(repo, function(err, data) { var m; if (err) { return cb(err); } - if (data.message) { - return cb(data.message); - } if (!data.length) { return cb(null, "No open milestones for repo " + repo.path); } @@ -29537,11 +29554,9 @@ module.exports = { }); m = m[0] ? m[0] : data[0]; if (m.open_issues + m.closed_issues === 0) { - return cb(null, "No issues for milestone " + m.title); - } - if (m.description) { - m.description = marked(m.description).slice(3, -5); + return cb(null, "No issues for milestone `" + m.title + "`"); } + m = parse(m); return cb(null, null, m); }); } @@ -29552,7 +29567,7 @@ require.register("app/modules/regex.js", function(exports, require, module){ module.exports = { 'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/, 'size_label': /^size (\d+)$/, - 'location': /^#!\/([^\/]+)\/([^\/]+)$/ + 'location': /^#!((\/[^\/]+){2,3})$/ }; }); @@ -29585,6 +29600,15 @@ module.exports = { }; return request(repo, query, 'milestones', cb); }, + 'one_milestone': function(repo, number, cb) { + var query; + query = { + 'state': 'open', + 'sort': 'due_date', + 'direction': 'asc' + }; + return request(repo, query, "milestones/" + number, cb); + }, 'all_issues': function(repo, query, cb) { _.extend(query, { 'per_page': '100' @@ -29616,10 +29640,14 @@ request = function(_arg, query, noun, cb) { }; respond = function(data, cb) { + var _ref; if (data.statusType !== 2) { + if ((data != null ? (_ref = data.body) != null ? _ref.message : void 0 : void 0) != null) { + return cb(data.body.message); + } return cb(data.error.message); } - return cb(null, data != null ? data.body : void 0); + return cb(null, data.body); }; }); @@ -29654,7 +29682,7 @@ render = require('./render'); module.exports = function(opts, cb) { return async.waterfall([ function(cb) { - return milestones.get_current(opts, function(err, warn, milestone) { + return milestones(opts, function(err, warn, milestone) { if (err) { return cb(err); } @@ -29877,7 +29905,7 @@ module.exports = function(__obj) { } (function() { (function() { - __out.push('
\n

GitHub Burndown Chart

\n

Use your browser\'s location hash to specify a repo: #!/radekstepan/disposable.

\n
'); + __out.push('
\n

GitHub Burndown Chart

\n

Use your browser\'s location hash to specify a repo: #!/radekstepan/disposable.

\n

You can choose a specific milestone like so: #!/radekstepan/disposable/1.

\n
'); }).call(this); diff --git a/src/app.coffee b/src/app.coffee index d250398..409e9d8 100644 --- a/src/app.coffee +++ b/src/app.coffee @@ -11,17 +11,21 @@ repo = require './modules/repo' route = -> # Do we have a location match? if match = window.location.hash.match regex.location - # Get the user/repo pair then. - path = match[1..3].join('/') + # User/repo/(milestone) path + path = match[1][1...] # Say we are loading this repo then. render 'body', 'loading', { path } + # Did we specify a milestone? + [ u, r, m ] = path.split('/') + opts = if m then { 'path': "#{u}/#{r}", 'milestone': m } else { path } + # Get config/cache. return async.waterfall [ config # Render this repo. , (conf, cb) -> - repo _.extend({ path }, conf), cb + repo _.extend(opts, conf), cb ], (err) -> render 'body', 'error', { 'text': err.toString() } if err diff --git a/src/modules/issues.coffee b/src/modules/issues.coffee index 4202c86..5c4d31d 100644 --- a/src/modules/issues.coffee +++ b/src/modules/issues.coffee @@ -20,10 +20,8 @@ module.exports = state page }, (err, data) -> - # Request errors. + # Errors? return cb err if err - # GitHub errors. - return cb data.message if data.message # Empty? return cb null, results unless data.length # Concat sorted (API does not sort on closed_at!). diff --git a/src/modules/milestones.coffee b/src/modules/milestones.coffee index 50e80ea..76855d2 100644 --- a/src/modules/milestones.coffee +++ b/src/modules/milestones.coffee @@ -4,15 +4,31 @@ marked = require 'marked' request = require './request' -module.exports = - - # Get current milestones for a repo.. - 'get_current': (repo, cb) -> - request.all_milestones repo, (err, data) -> - # Request errors? +# Get current/specified milestone for a repo. +module.exports = (repo, cb) -> + # Has description? Parse GFM. + parse = (data) -> + data.description = marked(data.description)[3...-5] if data.description + data + + # Get a specific milestone. + if repo.milestone + request.one_milestone repo, repo.milestone, (err, m) -> + # Errors? + return cb err if err + # Empty milestone? + if m.open_issues + m.closed_issues is 0 + return cb null, "No issues for milestone `#{m.title}`" + # Parse GFM. + m = parse m + + cb null, null, m + + # Get the current milestone out of many. + else + request.all_milestones repo, (err, data) -> + # Errors? return cb err if err - # GitHub errors? - return cb data.message if data.message # Empty warning? return cb null, "No open milestones for repo #{repo.path}" unless data.length # The first milestone should be ending soonest. @@ -22,8 +38,9 @@ module.exports = # The first milestone should be ending soonest. Prefer milestones with due dates. m = if m[0] then m[0] else data[0] # Empty milestone? - return cb null, "No issues for milestone #{m.title}" if m.open_issues + m.closed_issues is 0 - # Has description? Parse GFM. - m.description = marked(m.description)[3...-5] if m.description + if m.open_issues + m.closed_issues is 0 + return cb null, "No issues for milestone `#{m.title}`" + # Parse GFM. + m = parse m cb null, null, m \ No newline at end of file diff --git a/src/modules/regex.coffee b/src/modules/regex.coffee index 6852f1d..0e641ea 100644 --- a/src/modules/regex.coffee +++ b/src/modules/regex.coffee @@ -4,5 +4,5 @@ module.exports = 'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/ # How does a size label look like? 'size_label': /^size (\d+)$/ - # How do we specify which user/repo we want? - 'location': /^#!\/([^\/]+)\/([^\/]+)$/ \ No newline at end of file + # How do we specify which user/repo/(milestone) we want? + 'location': /^#!((\/[^\/]+){2,3})$/ \ No newline at end of file diff --git a/src/modules/repo.coffee b/src/modules/repo.coffee index e3c94fe..7dc34b5 100644 --- a/src/modules/repo.coffee +++ b/src/modules/repo.coffee @@ -11,9 +11,9 @@ render = require './render' # Setup a repo and render it. module.exports = (opts, cb) -> - # Get the current milestone. + # Get the current/specified milestone. async.waterfall [ (cb) -> - milestones.get_current opts, (err, warn, milestone) -> + milestones opts, (err, warn, milestone) -> return cb err if err return cb warn if warn opts.milestone = milestone diff --git a/src/modules/request.coffee b/src/modules/request.coffee index df9c89b..f96d7a2 100644 --- a/src/modules/request.coffee +++ b/src/modules/request.coffee @@ -17,6 +17,11 @@ module.exports = query = { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } request repo, query, 'milestones', cb + # Get one milestone. + 'one_milestone': (repo, number, cb) -> + query = { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } + request repo, query, "milestones/#{number}", cb + # Get all issues for a state. 'all_issues': (repo, query, cb) -> _.extend query, { 'per_page': '100' } @@ -51,6 +56,10 @@ request = ({ protocol, host, token, path }, query, noun, cb) -> # How do we respond to a response? respond = (data, cb) -> # 2xx? - return cb data.error.message if data.statusType isnt 2 + if data.statusType isnt 2 + # Do we have a message from GitHub? + return cb data.body.message if data?.body?.message? + # Use SA one. + return cb data.error.message # All good. - cb null, data?.body \ No newline at end of file + cb null, data.body \ No newline at end of file diff --git a/src/styles/app.styl b/src/styles/app.styl index 7443a56..e876587 100644 --- a/src/styles/app.styl +++ b/src/styles/app.styl @@ -67,11 +67,11 @@ h2 padding: 0 20px 20px p - margin: 0 + margin: 5px 0 padding: 0 20px &.description - margin-top: -10px + margin: -10px 0 0 0 // where D3 renders to #graph diff --git a/src/templates/info.eco b/src/templates/info.eco index ac59a52..84ed654 100644 --- a/src/templates/info.eco +++ b/src/templates/info.eco @@ -1,4 +1,5 @@

GitHub Burndown Chart

-

Use your browser's location hash to specify a repo: #!/radekstepan/disposable.

+

Use your browser's location hash to specify a repo: #!/radekstepan/disposable.

+

You can choose a specific milestone like so: #!/radekstepan/disposable/1.

\ No newline at end of file diff --git a/test/issues.coffee b/test/issues.coffee index 29b940d..640ad16 100644 --- a/test/issues.coffee +++ b/test/issues.coffee @@ -140,7 +140,7 @@ module.exports = called = 0 req.all_issues = (repo, opts, cb) -> called += 1 - cb null, { 'message': 'Not Found' } + cb 'Not Found' issues.get_all repo, (err, [ open, closed ]) -> assert.equal err, 'Not Found' diff --git a/test/milestones.coffee b/test/milestones.coffee index 5343e77..f28c7a5 100644 --- a/test/milestones.coffee +++ b/test/milestones.coffee @@ -20,7 +20,7 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err assert.equal milestone.number, 1 do done @@ -35,7 +35,7 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err assert.equal milestone.number, 1 do done @@ -61,7 +61,7 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err assert.equal milestone.number, 2 do done @@ -86,7 +86,7 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err assert.equal milestone.number, 1 do done @@ -95,16 +95,16 @@ module.exports = req.all_milestones = (opts, cb) -> cb null, [] - milestones.get_current { 'path': 'some/repo' }, (err, warn, milestone) -> + milestones { 'path': 'some/repo' }, (err, warn, milestone) -> assert.ifError err assert.equal warn, 'No open milestones for repo some/repo' do done 'milestones - get current when not found': (done) -> req.all_milestones = (opts, cb) -> - cb null, { 'message': 'Not Found' } + cb 'Not Found' - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.equal err, 'Not Found' do done @@ -121,9 +121,48 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err - assert.equal warn, 'No issues for milestone No issues' + assert.equal warn, 'No issues for milestone `No issues`' + do done + + 'milestones - get one': (done) -> + m = + 'number': 1 + 'created_at': '2013-01-01T00:00:00Z' + 'due_on': '2013-02-01T00:00:00Z' + + req.one_milestone = (opts, number, cb) -> + cb null, m + + milestones { 'milestone': 1 }, (err, warn, milestone) -> + assert.ifError err + assert.equal warn, null + assert.deepEqual milestone, m + do done + + 'milestones - get one (404)': (done) -> + req.one_milestone = (opts, number, cb) -> + cb 'Not Found' + + milestones { 'milestone': 9 }, (err, warn, milestone) -> + assert.equal err, 'Not Found' + do done + + 'milestones - get one when no issues': (done) -> + req.one_milestone = (opts, number, cb) -> + cb null, { + 'title': 'No issues' + 'number': 1 + 'created_at': '2013-01-01T00:00:00Z' + 'due_on': '2013-02-01T00:00:00Z', + 'open_issues': 0, + 'closed_issues': 0 + } + + milestones { 'milestone': 9 }, (err, warn, milestone) -> + assert.ifError err + assert.equal warn, 'No issues for milestone `No issues`' do done 'milestones - has description': (done) -> @@ -139,7 +178,7 @@ module.exports = } ] - milestones.get_current {}, (err, warn, milestone) -> + milestones {}, (err, warn, milestone) -> assert.ifError err assert.equal milestone.description, 'A description of this milestone goes here' do done \ No newline at end of file diff --git a/test/request.coffee b/test/request.coffee new file mode 100644 index 0000000..51386ca --- /dev/null +++ b/test/request.coffee @@ -0,0 +1,47 @@ +#!/usr/bin/env coffee +proxy = do require('proxyquire').noCallThru +assert = require 'assert' +path = require 'path' + +class Superagent + + get: -> @ + set: -> @ + end: (cb) -> cb @response + +request = proxy path.resolve(__dirname, '../src/modules/request.coffee'), + 'superagent': sa = new Superagent() + +module.exports = + + 'request - all milestones (ok)': (done) -> + sa.response = + 'statusType': 2 + 'error': no + 'body': [ null ] + + request.all_milestones {}, (err) -> + assert.ifError err + do done + + 'request - one milestone (404)': (done) -> + sa.response = + 'statusType': 4 + 'error': Error "cannot GET undefined (404)" + 'body': + 'documentation_url': "http://developer.github.com/v3" + 'message': "Not Found" + + request.one_milestone {}, 9, (err) -> + assert.equal err, 'Not Found' + do done + + 'request - one milestone (500)': (done) -> + sa.response = + 'statusType': 5 + 'error': Error "Error" + 'body': null + + request.one_milestone {}, 9, (err) -> + assert.equal err, 'Error' + do done \ No newline at end of file