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. 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)

View File

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

View File

@ -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('<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);

View File

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

View File

@ -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!).

View File

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

View File

@ -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': /^#!\/([^\/]+)\/([^\/]+)$/
# How do we specify which user/repo/(milestone) we want?
'location': /^#!((\/[^\/]+){2,3})$/

View File

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

View File

@ -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
cb null, data.body

View File

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

View File

@ -1,4 +1,5 @@
<div class="box info">
<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>

View File

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

View File

@ -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 <strong>milestone</strong> goes <em>here</em>'
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