multiple milestones; refs #23
This commit is contained in:
parent
863db506d1
commit
0dad78e225
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!).
|
||||
|
|
|
@ -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
|
|
@ -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})$/
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue