From 1c5b100b23a041ac77aa817ba6a187ae00cddf8e Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Thu, 15 Jun 2017 13:41:16 +0200 Subject: [PATCH 1/4] Drop interactive mode --- gad.js | 10 ++++++-- src/CLI.js | 37 +++++++++++++++++++++++------- src/Display/BurnDownChart.js | 2 +- src/GitHub/Milestone.js | 10 +++++--- src/GitHub/Project.js | 31 +++++++++++++++++++++---- src/GithubAgileDashboard.js | 44 ++++++++++++++++++++---------------- 6 files changed, 96 insertions(+), 38 deletions(-) diff --git a/gad.js b/gad.js index d909f0b..fe0a411 100644 --- a/gad.js +++ b/gad.js @@ -2,12 +2,18 @@ const { execSync } = require('child_process'); const { homedir } = require('os'); +const minimist = require('minimist'); +const GithubAgileDashboard = require('./src/GithubAgileDashboard'); +const { red } = require('./src/Util/colors'); function lookup(command) { try { return execSync(command).toString().trim(); } catch (error) { return ''; } } function remote(url) { return (new RegExp('git@github.com:(.+)\\/(.+)\\.git', 'ig').exec(url) || new Array(3).fill(null)).slice(1); } +process.on('uncaughtException', function onError(error) { console.error(red(error.message)); process.exit(1); }); + const [defaultOwner, defaultRepo] = remote(lookup('git -C . config --get remote.origin.url')); -const { owner, repo, user, password, cacheDir, _: commands} = require('minimist')(process.argv.slice(2), { +const { owner, repo, user, password, cacheDir, _: command} = minimist(process.argv.slice(2), { + stopEarly: true, default: { owner: defaultOwner, repo: defaultRepo, @@ -26,4 +32,4 @@ const { owner, repo, user, password, cacheDir, _: commands} = require('minimist' } }); -module.exports = new (require('./src/GithubAgileDashboard'))(owner, repo, user, password, cacheDir, commands); +module.exports = new GithubAgileDashboard(owner, repo, user, password, cacheDir, command.join(' ')); diff --git a/src/CLI.js b/src/CLI.js index d81967e..e752091 100644 --- a/src/CLI.js +++ b/src/CLI.js @@ -1,5 +1,6 @@ const EventEmitter = require('events'); const Readline = require('readline'); +const minimist = require('minimist'); class CLI extends EventEmitter { /** @@ -16,7 +17,7 @@ class CLI extends EventEmitter { const { stdin, stdout } = process; - this.commands = new Set(); + this.commands = new Map(); this.readline = Readline.createInterface({ input: stdin, output: stdout, prompt }); this.commandStack = commandStack; this.ready = false; @@ -37,15 +38,31 @@ class CLI extends EventEmitter { * * @param {String} name * @param {Function} callback + * @param {Object} options */ - on(name, callback) { + on(name, callback, options = {}) { super.on(name, callback); if (CLI.RESERVED.indexOf(name) < 0) { - this.commands.add(name); + this.commands.set(name, { default: options, alias: this.getAlias(options) }); } } + /** + * Create alias automatically + * + * @param {Object} options + * + * @return {Object} + */ + getAlias(options) { + const alias = {}; + + Object.keys(options).forEach(option => alias[option[0]] = option); + + return alias; + } + /** * Mark as ready */ @@ -78,7 +95,7 @@ class CLI extends EventEmitter { */ result(message) { this.write(typeof message === 'string' ? message : message.join('\r\n')); - this.readline.prompt(); + setImmediate(this.close); } /** @@ -96,11 +113,13 @@ class CLI extends EventEmitter { * @return {String} */ getCommand(input) { - if (this.commands.has(input.trim())) { - return input; + const [ command, ...options] = input.split(' '); + + if (!this.commands.has(command)) { + return { command: 'unknown' }; } - return 'unknown'; + return { command, options: minimist(options, this.commands.get(command)) }; } /** @@ -118,7 +137,9 @@ class CLI extends EventEmitter { * @param {String} line */ onLine(line) { - this.emit(this.getCommand(line) || 'unknown'); + const { command, options } = this.getCommand(line); + + this.emit(command || 'unknown', options); } /** diff --git a/src/Display/BurnDownChart.js b/src/Display/BurnDownChart.js index 6e21522..bac61fc 100644 --- a/src/Display/BurnDownChart.js +++ b/src/Display/BurnDownChart.js @@ -40,7 +40,7 @@ class BurnDownChart { display() { const { dayWidth, gutter } = this.constructor; const burnDown = this.getBurnDown(this.milestone); - const labelLength = Array.from(burnDown.keys()).reduce((max, label) => Math.max(label.length, max), 0); + const labelLength = Math.max(Array.from(burnDown.keys()).reduce((max, label) => Math.max(label.length, max), 0), 1); const maxPoints = Math.ceil(this.milestone.points); const pointsPerDay = maxPoints / burnDown.size; const lines = [ diff --git a/src/GitHub/Milestone.js b/src/GitHub/Milestone.js index a8116ab..18e5978 100644 --- a/src/GitHub/Milestone.js +++ b/src/GitHub/Milestone.js @@ -170,11 +170,15 @@ class Milestone { /** * Returns a changelog of the sprint * + * @param {Boolean} all Display all issue (and not just those that are done) + * * @return {Array} */ - displayChangelog() { - return ['## Changelog:'].concat( - this.getIssueByStatus('done') + displayChangelog(all = false) { + const issues = all ? this.issues : this.getIssueByStatus('done'); + + return [`# ${this.title}`, '## Changelog '].concat( + issues .sort(Issue.sortByPoint) .map(issue => `- ${issue.title}`) ); diff --git a/src/GitHub/Project.js b/src/GitHub/Project.js index 71d01b8..273b787 100644 --- a/src/GitHub/Project.js +++ b/src/GitHub/Project.js @@ -24,14 +24,36 @@ class Project { } /** - * Get current milestone + * Get sprint by index * + * @param {Number} index If null: current sprint. If negative: previous sprint from current. If position: number of the sprint. * @param {Date} date * * @return {Milestone} */ - getCurrentMilestone(date = DateUtil.day()) { - return this.getSprints().find(milestone => milestone.isCurrent(date)); + getSprint(number = null, date = DateUtil.day()) { + const sprints = this.getSprints(); + + if (!number) { + return sprints.find(milestone => milestone.isCurrent(date)); + } + + if (number > 0) { + if (typeof sprints[number - 1] === 'undefined') { + throw new Error(`No sprint ${number}: only ${sprints.length} sprints found.`); + } + + return sprints[number - 1]; + } else { + const date = DateUtil.day(); + const current = sprints.findIndex(milestone => milestone.isCurrent(date)); + + if (typeof sprints[current + number] === 'undefined') { + throw new Error(`No sprint ${number}.`); + } + + return sprints[current + number]; + } } /** @@ -40,7 +62,7 @@ class Project { * @return {Milestone[]} */ getSprints() { - return this.milestones.sort(Milestone.sort).filter(milestone => !milestone.isBacklog()); + return this.milestones.filter(milestone => !milestone.isBacklog()); } /** @@ -82,6 +104,7 @@ class Project { load(issues) { issues.filter(data => typeof data.pull_request === 'undefined').forEach(this.loadIssue); issues.filter(data => typeof data.pull_request !== 'undefined').forEach(this.loadPullRequest); + this.milestones.sort(Milestone.sort); this.getSprints().forEach((milestone, index, sprints) => milestone.previous = sprints[index - 1] || null); } diff --git a/src/GithubAgileDashboard.js b/src/GithubAgileDashboard.js index f9de3c4..236bb4e 100644 --- a/src/GithubAgileDashboard.js +++ b/src/GithubAgileDashboard.js @@ -10,10 +10,10 @@ class GithubAgileDashboard { * @param {String} username * @param {String} password * @param {String} cacheDir - * @param {Array} commands + * @param {String} command */ - constructor(owner, repo, username, password, cacheDir, commands = ['status']) { - this.cli = new CLI('gad> ', commands); + constructor(owner, repo, username, password, cacheDir, command = 'status') { + this.cli = new CLI('gad> ', [command]); this.loader = new HttpLoader(this.setProject.bind(this), owner, repo, username.trim(), password, cacheDir); this.user = username.trim(); this.project = null; @@ -37,11 +37,11 @@ class GithubAgileDashboard { if (!this.cli.ready) { this.cli.on('help', this.helpCommand); this.cli.on('status', this.statusCommand); - this.cli.on('sprint', this.sprintCommand); - this.cli.on('sprints', this.sprintsCommand); + this.cli.on('sprint', this.sprintCommand, { sprint: 0 }); + this.cli.on('sprints', this.sprintsCommand, { limit: null }); this.cli.on('backlog', this.backlogCommand); this.cli.on('review', this.reviewCommand); - this.cli.on('changelog', this.changelogCommand); + this.cli.on('changelog', this.changelogCommand, { sprint: 0, all: false }); this.cli.on('estimate', this.estimateCommand); this.cli.on('unknown', this.helpCommand); this.cli.on('refresh', this.loader.load); @@ -82,16 +82,25 @@ class GithubAgileDashboard { /** * Show the state of the current sprint */ - sprintCommand() { - this.cli.result(this.project.getCurrentMilestone().display()); + sprintCommand(options) { + const { sprint } = options; + this.cli.result(this.project.getSprint(sprint).display()); } /** - * Show the state of all sprints + * Generate a markdown changelog of the current sprint */ - sprintsCommand() { - const milestones = this.project.getSprints(); + changelogCommand(options) { + const { all, sprint } = options; + this.cli.result(this.project.getSprint(sprint).displayChangelog(all)); + } + /** + * Show the state of all sprints + */ + sprintsCommand(options) { + const { limit } = options; + const milestones = this.project.getSprints().reverse().slice(0, limit || undefined); this.cli.result(milestones.map(milestone => milestone.display())); } @@ -106,16 +115,11 @@ class GithubAgileDashboard { return this.cli.result('Nothing to review. Good job! 👍'); } - this.cli.result([`🔍 ${green(length)} pull requests awaiting your review:`] + this.cli.result( + [`🔍 ${green(length)} pull requests awaiting your review:`] .concat(pullRequests.map(pullRequest => pullRequest.display())) - .join('\r\n')); - } - - /** - * Generate a markdown changelog of the current sprint - */ - changelogCommand() { - this.cli.result(this.project.getCurrentMilestone().displayChangelog()); + .join('\r\n') + ); } /** From bfa7d2f021ab2559569d95e7ce784725a4fb8d9d Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Tue, 27 Jun 2017 14:02:21 +0200 Subject: [PATCH 2/4] Output style --- src/CLI.js | 5 +++-- src/GitHub/Milestone.js | 24 +++++++++++++++++------- src/GithubAgileDashboard.js | 24 +++++++++++++++++------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/CLI.js b/src/CLI.js index e752091..069edf1 100644 --- a/src/CLI.js +++ b/src/CLI.js @@ -76,7 +76,8 @@ class CLI extends EventEmitter { let line; while (line = this.commandStack.shift()) { - this.readline.write(`${line}\r\n`); + this.readline.prompt(); + this.readline.write(`${line.trim()}\r\n`); } } } @@ -94,7 +95,7 @@ class CLI extends EventEmitter { * Display command result */ result(message) { - this.write(typeof message === 'string' ? message : message.join('\r\n')); + this.write('\r\n' + (typeof message === 'string' ? message : message.join('\r\n')) + '\r\n'); setImmediate(this.close); } diff --git a/src/GitHub/Milestone.js b/src/GitHub/Milestone.js index 18e5978..baaa502 100644 --- a/src/GitHub/Milestone.js +++ b/src/GitHub/Milestone.js @@ -1,6 +1,7 @@ const Issue = require('./Issue'); -const DateUtil = require('../Util/DateUtil'); const BurnDownChart = require('../Display/BurnDownChart'); +const DateUtil = require('../Util/DateUtil'); +const { green, yellow } = require('../Util/colors'); class Milestone { /** @@ -144,7 +145,16 @@ class Milestone { displaySprint() { const { title, length, done, todo, inProgress, readyToReview, progress, points } = this; - return `${title} ・ 📉 ${(progress * 100).toFixed(2)}% ・ 📫 ${todo}pts ・ 🚧 ${inProgress}pts ・ 🔍 ${readyToReview}pts ・ ✅ ${done}pts ・ (${length} stories ・ ${points}pts)`; + return [ + title, + `${green(length)} stories`, + `${yellow(points)} points`, + `📉 ${(progress * 100).toFixed(2)}%`, + `📫 ${todo}pts`, + `🚧 ${inProgress}pts`, + `🔍 ${readyToReview}pts`, + `✅ ${done}pts`, + ].join(' ・ '); } /** @@ -155,7 +165,7 @@ class Milestone { displayBacklog() { const { title, length, points } = this; - return `${title} ・ 📇 ${length} stories ・ ${points} points`; + return `${title} ・ 📇 ${green(length)} stories ・ ${yellow(points)} points`; } /** @@ -178,10 +188,10 @@ class Milestone { const issues = all ? this.issues : this.getIssueByStatus('done'); return [`# ${this.title}`, '## Changelog '].concat( - issues - .sort(Issue.sortByPoint) - .map(issue => `- ${issue.title}`) - ); + issues + .sort(Issue.sortByPoint) + .map(issue => `- ${issue.title}`) + ).join('\r\n'); } } diff --git a/src/GithubAgileDashboard.js b/src/GithubAgileDashboard.js index 236bb4e..829e991 100644 --- a/src/GithubAgileDashboard.js +++ b/src/GithubAgileDashboard.js @@ -13,7 +13,7 @@ class GithubAgileDashboard { * @param {String} command */ constructor(owner, repo, username, password, cacheDir, command = 'status') { - this.cli = new CLI('gad> ', [command]); + this.cli = new CLI('gad> ', command.split('&&')); this.loader = new HttpLoader(this.setProject.bind(this), owner, repo, username.trim(), password, cacheDir); this.user = username.trim(); this.project = null; @@ -35,7 +35,6 @@ class GithubAgileDashboard { */ onInit() { if (!this.cli.ready) { - this.cli.on('help', this.helpCommand); this.cli.on('status', this.statusCommand); this.cli.on('sprint', this.sprintCommand, { sprint: 0 }); this.cli.on('sprints', this.sprintsCommand, { limit: null }); @@ -76,7 +75,7 @@ class GithubAgileDashboard { backlogCommand() { const milestones = this.project.getBacklogs(); - this.cli.result(milestones.map(milestone => milestone.display())); + this.cli.result(milestones.map(milestone => ' ' + milestone.display())); } /** @@ -116,8 +115,8 @@ class GithubAgileDashboard { } this.cli.result( - [`🔍 ${green(length)} pull requests awaiting your review:`] - .concat(pullRequests.map(pullRequest => pullRequest.display())) + [`🔍 ${green(length)} pull request(s) awaiting your review:`] + .concat(pullRequests.map(pullRequest => ' ' + pullRequest.display())) .join('\r\n') ); } @@ -126,14 +125,25 @@ class GithubAgileDashboard { * Show stories that are missing estimation */ estimateCommand() { - this.cli.result(this.project.getIssuesMissingEstimation().map(issue => issue.display())); + const issues = this.project.getIssuesMissingEstimation(); + const { length } = issues; + + if (length === 0) { + return this.cli.result('Nothing to estimate. Good job! 👍'); + } + + this.cli.result( + [`🔍 ${green(length)} issue(s) awaiting estimation:`] + .concat(issues.map(issue => ' ' + issue.display())) + .join('\r\n') + ); } /** * Display help */ helpCommand() { - this.cli.result(`Available commands: ${Array.from(this.cli.commands).join(', ')}`); + this.cli.result(`Available commands: ${Array.from(this.cli.commands.keys()).join(', ')}`); } } From bd2d9fd38a3e88a9b89b549201227e98fbd52e05 Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Tue, 27 Jun 2017 14:10:30 +0200 Subject: [PATCH 3/4] Updated doc --- README.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index df63063..bef5e41 100644 --- a/README.md +++ b/README.md @@ -35,19 +35,23 @@ As [recommended by GitHub](https://github.com/blog/180-local-github-config), Gad ### Commands -In your projet repository, just enter `gad`. - -| Command | Description | -|---|---| -| __sprint__ | Show the state of the current sprint | -| __sprints__ | Show the state of all sprints | -| __backlog__ | Show the state of the backlog | -| __review__ | Display PullRequest that are awaiting your review | -| __changelog__ | Generate a markdown changelog of the current sprint | -| __estimate__ | Show stories that are missing estimation | -| __status__ | Show the status of the repository | -| __help__ | Show list of commands | -| __exit__ | Quit the dashboard | +In your projet repository, just enter `gad [command] (options)`. + +| Command | Description | Options | +|---|---|---| +| __sprint__ | Show the state of the current sprint | __sprint__ `-s=-1` Show the previous sprint | +| __sprints__ | Show the state of all sprints | __limit__ `-l=2` limit the number of sprint to display | +| __backlog__ | Show the state of the backlog | | +| __review__ | Display PullRequest that are awaiting your review | | +| __changelog__ | Generate a markdown changelog of the current sprint | __all__ `--all` include open issues in the changelog. __sprint__ `-s=-2` Show the changelog from two sprints ago | +| __estimate__ | Show stories that are missing estimation | | +| __status__ | Show the status of the repository | | +| __help__ | Show list of commands | | +| __exit__ | Quit the dashboard | | + +You can run several commands with `&&`: + + gad "sprint && review" ### Options From d995bde43681642e9c5a0b3ec60fc2ba05f5d6db Mon Sep 17 00:00:00 2001 From: Thomas Jarrand Date: Wed, 28 Jun 2017 09:40:25 +0200 Subject: [PATCH 4/4] Disable multi-command support --- README.md | 4 ---- src/CLI.js | 2 +- src/GithubAgileDashboard.js | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bef5e41..9b0e182 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,6 @@ In your projet repository, just enter `gad [command] (options)`. | __help__ | Show list of commands | | | __exit__ | Quit the dashboard | | -You can run several commands with `&&`: - - gad "sprint && review" - ### Options You can manually specify any of the options on the fly: diff --git a/src/CLI.js b/src/CLI.js index 069edf1..b69d4be 100644 --- a/src/CLI.js +++ b/src/CLI.js @@ -10,7 +10,7 @@ class CLI extends EventEmitter { /** * @param {String} prompt - * @param {Array} commandStack command stack + * @param {Array} commandStack Command stack */ constructor(prompt = '', commandStack = []) { super(); diff --git a/src/GithubAgileDashboard.js b/src/GithubAgileDashboard.js index 829e991..0233e73 100644 --- a/src/GithubAgileDashboard.js +++ b/src/GithubAgileDashboard.js @@ -13,7 +13,7 @@ class GithubAgileDashboard { * @param {String} command */ constructor(owner, repo, username, password, cacheDir, command = 'status') { - this.cli = new CLI('gad> ', command.split('&&')); + this.cli = new CLI('gad> ', [command]); this.loader = new HttpLoader(this.setProject.bind(this), owner, repo, username.trim(), password, cacheDir); this.user = username.trim(); this.project = null;