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('
');
+ __out.push('');
}).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 @@
\ 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