multiple milestones; refs #23

This commit is contained in:
Radek Stepan 2013-10-05 12:45:07 +01:00
parent 863db506d1
commit 0dad78e225
14 changed files with 204 additions and 60 deletions

View File

@ -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. 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. 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. 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) ![image](https://raw.github.com/radekstepan/github-burndown-chart/master/example.png)

View File

@ -601,11 +601,11 @@ h2 {
padding: 0 20px 20px; padding: 0 20px 20px;
} }
.box p { .box p {
margin: 0; margin: 5px 0;
padding: 0 20px; padding: 0 20px;
} }
.box p.description { .box p.description {
margin-top: -10px; margin: -10px 0 0 0;
} }
#graph { #graph {
height: 200px; height: 200px;

View File

@ -29098,17 +29098,22 @@ render = require('./modules/render');
repo = require('./modules/repo'); repo = require('./modules/repo');
route = function() { route = function() {
var match, path; var m, match, opts, path, r, u, _ref;
if (match = window.location.hash.match(regex.location)) { if (match = window.location.hash.match(regex.location)) {
path = match.slice(1, 4).join('/'); path = match[1].slice(1);
render('body', 'loading', { render('body', 'loading', {
path: path 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([ return async.waterfall([
config, function(conf, cb) { config, function(conf, cb) {
return repo(_.extend({ return repo(_.extend(opts, conf), cb);
path: path
}, conf), cb);
} }
], function(err) { ], function(err) {
if (err) { if (err) {
@ -29446,9 +29451,6 @@ module.exports = {
if (err) { if (err) {
return cb(err); return cb(err);
} }
if (data.message) {
return cb(data.message);
}
if (!data.length) { if (!data.length) {
return cb(null, results); return cb(null, results);
} }
@ -29518,16 +29520,31 @@ marked = require('marked');
request = require('./request'); request = require('./request');
module.exports = { module.exports = function(repo, cb) {
'get_current': 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) { return request.all_milestones(repo, function(err, data) {
var m; var m;
if (err) { if (err) {
return cb(err); return cb(err);
} }
if (data.message) {
return cb(data.message);
}
if (!data.length) { if (!data.length) {
return cb(null, "No open milestones for repo " + repo.path); return cb(null, "No open milestones for repo " + repo.path);
} }
@ -29537,11 +29554,9 @@ module.exports = {
}); });
m = m[0] ? m[0] : data[0]; m = m[0] ? m[0] : data[0];
if (m.open_issues + m.closed_issues === 0) { if (m.open_issues + m.closed_issues === 0) {
return cb(null, "No issues for milestone " + m.title); return cb(null, "No issues for milestone `" + m.title + "`");
}
if (m.description) {
m.description = marked(m.description).slice(3, -5);
} }
m = parse(m);
return cb(null, null, m); return cb(null, null, m);
}); });
} }
@ -29552,7 +29567,7 @@ require.register("app/modules/regex.js", function(exports, require, module){
module.exports = { module.exports = {
'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/, 'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/,
'size_label': /^size (\d+)$/, 'size_label': /^size (\d+)$/,
'location': /^#!\/([^\/]+)\/([^\/]+)$/ 'location': /^#!((\/[^\/]+){2,3})$/
}; };
}); });
@ -29585,6 +29600,15 @@ module.exports = {
}; };
return request(repo, query, 'milestones', cb); 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) { 'all_issues': function(repo, query, cb) {
_.extend(query, { _.extend(query, {
'per_page': '100' 'per_page': '100'
@ -29616,10 +29640,14 @@ request = function(_arg, query, noun, cb) {
}; };
respond = function(data, cb) { respond = function(data, cb) {
var _ref;
if (data.statusType !== 2) { 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(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) { module.exports = function(opts, cb) {
return async.waterfall([ return async.waterfall([
function(cb) { function(cb) {
return milestones.get_current(opts, function(err, warn, milestone) { return milestones(opts, function(err, warn, milestone) {
if (err) { if (err) {
return cb(err); return cb(err);
} }
@ -29877,7 +29905,7 @@ module.exports = function(__obj) {
} }
(function() { (function() {
(function() { (function() {
__out.push('<div class="box info">\n <h2>GitHub Burndown Chart</h2>\n <p>Use your browser\'s location hash to specify a repo: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>\n</div>'); __out.push('<div class="box info">\n <h2>GitHub Burndown Chart</h2>\n <p>Use your browser\'s location hash to specify a <strong>repo</strong>: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>\n <p>You can choose a specific <strong>milestone</strong> like so: <a href="#!/radekstepan/disposable/1">#!/radekstepan/disposable/1</a>.</p>\n</div>');
}).call(this); }).call(this);

View File

@ -11,17 +11,21 @@ repo = require './modules/repo'
route = -> route = ->
# Do we have a location match? # Do we have a location match?
if match = window.location.hash.match regex.location if match = window.location.hash.match regex.location
# Get the user/repo pair then. # User/repo/(milestone) path
path = match[1..3].join('/') path = match[1][1...]
# Say we are loading this repo then. # Say we are loading this repo then.
render 'body', 'loading', { path } 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. # Get config/cache.
return async.waterfall [ config return async.waterfall [ config
# Render this repo. # Render this repo.
, (conf, cb) -> , (conf, cb) ->
repo _.extend({ path }, conf), cb repo _.extend(opts, conf), cb
], (err) -> ], (err) ->
render 'body', 'error', { 'text': err.toString() } if err render 'body', 'error', { 'text': err.toString() } if err

View File

@ -20,10 +20,8 @@ module.exports =
state state
page page
}, (err, data) -> }, (err, data) ->
# Request errors. # Errors?
return cb err if err return cb err if err
# GitHub errors.
return cb data.message if data.message
# Empty? # Empty?
return cb null, results unless data.length return cb null, results unless data.length
# Concat sorted (API does not sort on closed_at!). # Concat sorted (API does not sort on closed_at!).

View File

@ -4,15 +4,31 @@ marked = require 'marked'
request = require './request' request = require './request'
module.exports = # 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 current milestones for a repo.. # Get a specific milestone.
'get_current': (repo, cb) -> if repo.milestone
request.all_milestones repo, (err, data) -> request.one_milestone repo, repo.milestone, (err, m) ->
# Request errors? # 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 return cb err if err
# GitHub errors?
return cb data.message if data.message
# Empty warning? # Empty warning?
return cb null, "No open milestones for repo #{repo.path}" unless data.length return cb null, "No open milestones for repo #{repo.path}" unless data.length
# The first milestone should be ending soonest. # 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. # The first milestone should be ending soonest. Prefer milestones with due dates.
m = if m[0] then m[0] else data[0] m = if m[0] then m[0] else data[0]
# Empty milestone? # Empty milestone?
return cb null, "No issues for milestone #{m.title}" if m.open_issues + m.closed_issues is 0 if m.open_issues + m.closed_issues is 0
# Has description? Parse GFM. return cb null, "No issues for milestone `#{m.title}`"
m.description = marked(m.description)[3...-5] if m.description # Parse GFM.
m = parse m
cb null, null, m cb null, null, m

View File

@ -4,5 +4,5 @@ module.exports =
'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/ 'datetime': /^(\d{4}-\d{2}-\d{2})T(.*)/
# How does a size label look like? # How does a size label look like?
'size_label': /^size (\d+)$/ 'size_label': /^size (\d+)$/
# How do we specify which user/repo we want? # How do we specify which user/repo/(milestone) we want?
'location': /^#!\/([^\/]+)\/([^\/]+)$/ 'location': /^#!((\/[^\/]+){2,3})$/

View File

@ -11,9 +11,9 @@ render = require './render'
# Setup a repo and render it. # Setup a repo and render it.
module.exports = (opts, cb) -> module.exports = (opts, cb) ->
# Get the current milestone. # Get the current/specified milestone.
async.waterfall [ (cb) -> async.waterfall [ (cb) ->
milestones.get_current opts, (err, warn, milestone) -> milestones opts, (err, warn, milestone) ->
return cb err if err return cb err if err
return cb warn if warn return cb warn if warn
opts.milestone = milestone opts.milestone = milestone

View File

@ -17,6 +17,11 @@ module.exports =
query = { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' } query = { 'state': 'open', 'sort': 'due_date', 'direction': 'asc' }
request repo, query, 'milestones', cb 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. # Get all issues for a state.
'all_issues': (repo, query, cb) -> 'all_issues': (repo, query, cb) ->
_.extend query, { 'per_page': '100' } _.extend query, { 'per_page': '100' }
@ -51,6 +56,10 @@ request = ({ protocol, host, token, path }, query, noun, cb) ->
# How do we respond to a response? # How do we respond to a response?
respond = (data, cb) -> respond = (data, cb) ->
# 2xx? # 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. # All good.
cb null, data?.body cb null, data.body

View File

@ -67,11 +67,11 @@ h2
padding: 0 20px 20px padding: 0 20px 20px
p p
margin: 0 margin: 5px 0
padding: 0 20px padding: 0 20px
&.description &.description
margin-top: -10px margin: -10px 0 0 0
// where D3 renders to // where D3 renders to
#graph #graph

View File

@ -1,4 +1,5 @@
<div class="box info"> <div class="box info">
<h2>GitHub Burndown Chart</h2> <h2>GitHub Burndown Chart</h2>
<p>Use your browser's location hash to specify a repo: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p> <p>Use your browser's location hash to specify a <strong>repo</strong>: <a href="#!/radekstepan/disposable">#!/radekstepan/disposable</a>.</p>
<p>You can choose a specific <strong>milestone</strong> like so: <a href="#!/radekstepan/disposable/1">#!/radekstepan/disposable/1</a>.</p>
</div> </div>

View File

@ -140,7 +140,7 @@ module.exports =
called = 0 called = 0
req.all_issues = (repo, opts, cb) -> req.all_issues = (repo, opts, cb) ->
called += 1 called += 1
cb null, { 'message': 'Not Found' } cb 'Not Found'
issues.get_all repo, (err, [ open, closed ]) -> issues.get_all repo, (err, [ open, closed ]) ->
assert.equal err, 'Not Found' assert.equal err, 'Not Found'

View File

@ -20,7 +20,7 @@ module.exports =
} }
] ]
milestones.get_current {}, (err, warn, milestone) -> milestones {}, (err, warn, milestone) ->
assert.ifError err assert.ifError err
assert.equal milestone.number, 1 assert.equal milestone.number, 1
do done do done
@ -35,7 +35,7 @@ module.exports =
} }
] ]
milestones.get_current {}, (err, warn, milestone) -> milestones {}, (err, warn, milestone) ->
assert.ifError err assert.ifError err
assert.equal milestone.number, 1 assert.equal milestone.number, 1
do done do done
@ -61,7 +61,7 @@ module.exports =
} }
] ]
milestones.get_current {}, (err, warn, milestone) -> milestones {}, (err, warn, milestone) ->
assert.ifError err assert.ifError err
assert.equal milestone.number, 2 assert.equal milestone.number, 2
do done do done
@ -86,7 +86,7 @@ module.exports =
} }
] ]
milestones.get_current {}, (err, warn, milestone) -> milestones {}, (err, warn, milestone) ->
assert.ifError err assert.ifError err
assert.equal milestone.number, 1 assert.equal milestone.number, 1
do done do done
@ -95,16 +95,16 @@ module.exports =
req.all_milestones = (opts, cb) -> req.all_milestones = (opts, cb) ->
cb null, [] cb null, []
milestones.get_current { 'path': 'some/repo' }, (err, warn, milestone) -> milestones { 'path': 'some/repo' }, (err, warn, milestone) ->
assert.ifError err assert.ifError err
assert.equal warn, 'No open milestones for repo some/repo' assert.equal warn, 'No open milestones for repo some/repo'
do done do done
'milestones - get current when not found': (done) -> 'milestones - get current when not found': (done) ->
req.all_milestones = (opts, cb) -> 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' assert.equal err, 'Not Found'
do done do done
@ -121,9 +121,48 @@ module.exports =
} }
] ]
milestones.get_current {}, (err, warn, milestone) -> milestones {}, (err, warn, milestone) ->
assert.ifError err 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 do done
'milestones - has description': (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.ifError err
assert.equal milestone.description, 'A description of this <strong>milestone</strong> goes <em>here</em>' assert.equal milestone.description, 'A description of this <strong>milestone</strong> goes <em>here</em>'
do done do done

47
test/request.coffee Normal file
View File

@ -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