New jlp-based documentation for pit-cli.

This commit is contained in:
Jonathan Bernard 2012-02-13 12:12:26 -06:00
parent c0b02ca222
commit d0e968b2b7

View File

@ -1,3 +1,10 @@
/**
* # Personal Issue Tracker Command Line Interface
* @author Jonathan Bernard <jdbernard@gmail.com>
* @copyright 2009-2012 Jonathan Bernard
*
* This is a command-line interface to my personal issue tracker system.
*/
package com.jdbernard.pit package com.jdbernard.pit
import com.jdbernard.pit.file.* import com.jdbernard.pit.file.*
@ -11,47 +18,127 @@ import static java.lang.Math.min
def log = LoggerFactory.getLogger(getClass()) def log = LoggerFactory.getLogger(getClass())
// -------- command-line interface specification -------- // /// ## Command Line Options ##
/// --------------------------
def cli = new CliBuilder(usage: 'pit-cli [options]') def cli = new CliBuilder(usage: 'pit-cli [options]')
/// -h,--help
/// : Show help information
cli.h(longOpt: 'help', 'Show help information.') cli.h(longOpt: 'help', 'Show help information.')
/// -v,--verbose
/// : Show verbose task information.
cli.v(longOpt: 'verbose', 'Show verbose task information') cli.v(longOpt: 'verbose', 'Show verbose task information')
/// -l,--list
/// : List issues in the current project.
cli.l(longOpt: 'list', 'List issues in the current project.') cli.l(longOpt: 'list', 'List issues in the current project.')
/// -i,--id
/// : Filter issues by id. Accepts a comma-delimited list.
/// *Example:* `pit -l -i 0001,0002`
cli.i(argName: 'id', longOpt: 'id', args: 1, cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.') 'Filter issues by id. Accepts a comma-delimited list.')
/// -c,--category
/// : Filter issues by category (bug, feature, task). Accepts a
/// comma-delimited list. By default all categories are selected. The full
/// category name is not required, just enough to be uniquely identifiable.
/// *Example:* `pit -l -c bug,t # List bugs and tasks.`
cli.c(argName: 'category', longOpt: 'category', args: 1, cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task). Accepts a ' 'Filter issues by category (bug, feature, task). Accepts a '
+ 'comma-delimited list. By default all categories are selected.') + 'comma-delimited list. By default all categories are selected.')
/// -s,--status
/// : Filter issues by status (new, reassigned, rejected, resolved,
/// validation_required). The full status is not required, just enough to
/// uniquely identify the status.
/// *Example:* `pit -l -s reas,rej # List Reassigned and Rejected issues.`
cli.s(argName: 'status', longOpt: 'status', args: 1, cli.s(argName: 'status', longOpt: 'status', args: 1,
'Filter issues by status (new, reassigned, rejected, resolved, ' + 'Filter issues by status (new, reassigned, rejected, resolved, ' +
'validation_required)') 'validation_required)')
/// -p,--priority
/// : Filter issues by priority. This acts as a threshhold, listing all
/// issues greater than or equal to the given priority.
/// *Example:* `pit -l -p 5 # List all issues with priority >= 5`
cli.p(argName: 'priority', longOpt: 'priority', args: 1, cli.p(argName: 'priority', longOpt: 'priority', args: 1,
'Filter issues by priority. This acts as a threshhold, listing all issues ' 'Filter issues by priority. This acts as a threshhold, listing all issues '
+ 'greater than or equal to the given priority.') + 'greater than or equal to the given priority.')
/// -r,--project
/// : Filter issues by project (relative to the current directory). Accepts a
/// comma-delimited list. This option should be used in conjunction with the
/// `R,--recursive` option.
/// *Example:* `pit -l -R --project <project_name>`
cli.r(argName: 'project', longOpt: 'project', args: 1, cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a ' 'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.') + 'comma-delimited list.')
/// -R,--recursive
/// : Recursively include subprojects.
cli.R(longOpt: 'recursive', 'Include subprojects.') cli.R(longOpt: 'recursive', 'Include subprojects.')
/// -e,--extended-property
/// : Filter for issues by extended property. Format is
/// `-e <propname>=<propvalue>`.
cli.e(argName: 'extended-property', args: 1, 'Filter for issues by extended ' + cli.e(argName: 'extended-property', args: 1, 'Filter for issues by extended ' +
'property. Format is "-e <propname>=<propvalue>".') 'property. Format is "-e <propname>=<propvalue>".')
/*cli.s(longOpt: 'show-subprojects', /*cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)') 'Include sup projects in listing (default behaviour)')
cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')*/ // TODO: figure out better flags for these options. cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')*/ // TODO: figure out better flags for these options.
/// -P,--set-priority
/// : Modify the priority of the selected issues. Requires a value from 0-9.
cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1, cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
'Modify the priority of the selected issues.') 'Modify the priority of the selected issues.')
/// -C,--set-category
/// : Modify the category of the selected issues.
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1, cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
'Modify the category of the selected issues.') 'Modify the category of the selected issues.')
/// -S,--set-status
/// : Modify the status of the selected issues.
cli.S(argName: 'new-status', longOpt: 'set-status', args: 1, cli.S(argName: 'new-status', longOpt: 'set-status', args: 1,
'Modify the status of the selected issues.') 'Modify the status of the selected issues.')
cli.E(argName: 'new-extended-property', args: 1, 'Modify the extended ' +
'property of the selected issues. Format is "-E <propname>=<propvalue>"') /// -E,--new-issue
/// : Modify the extended property of the selected issues. Format is
/// `-E <propname>=<propvalue>`
cli.E(argName: 'new-extended-property', longOpt: 'set-extended-property',
args: 1, 'Modify the extended property of the selected issues. Format ' +
'is "-E <propname>=<propvalue>"')
/// -n,--new-issue
/// : Create a new issue
cli.n(longOpt: 'new-issue', 'Create a new issue.') cli.n(longOpt: 'new-issue', 'Create a new issue.')
/// --title
/// : Give the title for a new issue or modify the title for an existing
/// issue. By default the title for a new issue is expected on stanard
/// input.
cli._(longOpt: 'title', args: 1, argName: 'title', 'Give the title for a new' + cli._(longOpt: 'title', args: 1, argName: 'title', 'Give the title for a new' +
' issue or modify the title for an existing issue. By default the title' + ' issue or modify the title for an existing issue. By default the title' +
' for a new issue is expected on stanard input.') ' for a new issue is expected on stanard input.')
/// --text
/// : Give the text for a new issue or modify the text for an exising
/// issue. By default the text for a new issue is expected on standard
/// input.
cli._(longOpt: 'text', args: 1, argName: 'text', 'Give the text for a new' + cli._(longOpt: 'text', args: 1, argName: 'text', 'Give the text for a new' +
' issue or modify the text for an exising issue. By default the text for' + ' issue or modify the text for an exising issue. By default the text for' +
' a new issue is expected on standard input.') ' a new issue is expected on standard input.')
/** -o,--order
* : Order (sort) the results by the given properties. Provide a
* comma-seperated list of property names to sort by in order of
* importance. The basic properties (id, category, status, and priority)
* can be given using their one-letter forms (i,c,s,p) for brevity. For
* example: `-o Due,p,c` would sort first by the extended property `Due`,
* then for items that have the same `Due` value it would sort by
* priority, then by category. */
cli.o(longOpt: 'order', argName: 'order', args: 1, required: false, cli.o(longOpt: 'order', argName: 'order', args: 1, required: false,
'Order (sort) the results by the given properties. Provide a comma-' + 'Order (sort) the results by the given properties. Provide a comma-' +
'seperated list of property names to sort by in order of importance. The' + 'seperated list of property names to sort by in order of importance. The' +
@ -60,31 +147,64 @@ cli.o(longOpt: 'order', argName: 'order', args: 1, required: false,
' "-o Due,p,c" would sort first by the extended property "Due", then for' + ' "-o Due,p,c" would sort first by the extended property "Due", then for' +
' items that have the same "Due" value it would sort by priority, then' + ' items that have the same "Due" value it would sort by priority, then' +
' by category.') ' by category.')
/// -d,--dir
/// : Use `<dir>` as the base directory (defaults to current directory).
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false, cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
'Use <dir> as the base directory (defaults to current directory).') 'Use <dir> as the base directory (defaults to current directory).')
/// -D,--daily-list
/// : Print a Daily Task list based on issue Due, Scheduled, and Reminder
/// extended properties.
cli.D(longOpt: 'daily-list', 'Print a Daily Task list based on issue Due and' + cli.D(longOpt: 'daily-list', 'Print a Daily Task list based on issue Due and' +
' Reminder properties.') ' Reminder properties.')
/// --dl-scheduled
/// : Show scheduled tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-scheduled', 'Show scheduled tasks in the daily list (all' + cli._(longOpt: 'dl-scheduled', 'Show scheduled tasks in the daily list (all' +
' are shown by default).') ' are shown by default).')
/// --dl-due
/// : Show due tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-due', 'Show due tasks in the daily list (all are shown by' + cli._(longOpt: 'dl-due', 'Show due tasks in the daily list (all are shown by' +
' default).') ' default).')
/// --dl-reminder
/// : Show upcoming tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-reminder', 'Show upcoming tasks in the daily list (all ' + cli._(longOpt: 'dl-reminder', 'Show upcoming tasks in the daily list (all ' +
' are shown by default).') ' are shown by default).')
/// --dl-open
/// : Show open tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-open', 'Show open tasks in the daily list (all are shown ' + cli._(longOpt: 'dl-open', 'Show open tasks in the daily list (all are shown ' +
' by default).') ' by default).')
/// --dl-hide-scheduled
/// : Hide scheduled tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-hide-scheduled', 'Hide scheduled tasks in the daily list' + cli._(longOpt: 'dl-hide-scheduled', 'Hide scheduled tasks in the daily list' +
' (all are shown by default).') ' (all are shown by default).')
/// --dl-hide-due
/// : Show due tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-hide-due', 'Show due tasks in the daily list (all are' + cli._(longOpt: 'dl-hide-due', 'Show due tasks in the daily list (all are' +
' shown by default).') ' shown by default).')
/// --dl-hide-reminder
/// : Show upcoming tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-hide-reminder', 'Show upcoming tasks in the daily list' + cli._(longOpt: 'dl-hide-reminder', 'Show upcoming tasks in the daily list' +
' (all are shown by default).') ' (all are shown by default).')
/// --dl-hide-open
/// : Show open tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-hide-open', 'Show open tasks in the daily list (all are' + cli._(longOpt: 'dl-hide-open', 'Show open tasks in the daily list (all are' +
' shown by default).') ' shown by default).')
/// --version
/// : Display PIT version information.
cli._(longOpt: 'version', 'Display PIT version information.') cli._(longOpt: 'version', 'Display PIT version information.')
// =================================== // /// ## Parse CLI Options ##
// ======== Parse CLI Options ======== // /// -----------------------
// =================================== //
log.trace("Parsing options.") log.trace("Parsing options.")
@ -93,7 +213,7 @@ def opts = cli.parse(args)
def issuedb = [:] def issuedb = [:]
def workingDir = new File('.') def workingDir = new File('.')
// defaults for the issue filter/selector /// Defaults for the issue filter/selector.
def selectOpts = [ def selectOpts = [
categories: ['bug', 'feature', 'task'], categories: ['bug', 'feature', 'task'],
status: ['new', 'reassigned', 'rejected', status: ['new', 'reassigned', 'rejected',
@ -104,19 +224,19 @@ def selectOpts = [
extendedProperties: [:], extendedProperties: [:],
acceptProjects: true] acceptProjects: true]
// options for changing properties of issue(s) /// Defaults for changing properties of issue(s)
def assignOpts = [:] def assignOpts = [:]
if (!opts || opts.h) { if (!opts || opts.h) {
cli.usage() cli.usage()
System.exit(0) } System.exit(0) }
// read the category filter designation(s) ///Read the `-c` option: category filter designation(s).
if (opts.c) { if (opts.c) {
if (opts.c =~ /all/) {} // no-op, same as defaults if (opts.c =~ /all/) {} // no-op, same as defaults
else { selectOpts.categories = opts.c.split(/[,\s]/) } } else { selectOpts.categories = opts.c.split(/[,\s]/) } }
// parse the categories names into Category objects /// Parse the categories names into Category objects.
try { selectOpts.categories = try { selectOpts.categories =
selectOpts.categories.collect { Category.toCategory(it) } } selectOpts.categories.collect { Category.toCategory(it) } }
catch (Exception e) { catch (Exception e) {
@ -125,15 +245,15 @@ catch (Exception e) {
println " (abbreviations are accepted)." println " (abbreviations are accepted)."
System.exit(1) } System.exit(1) }
// read the status filter designation(s) /// Read the `-s` option: status filter designation(s).
if (opts.s) { if (opts.s) {
// -s all // -s all
if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected', if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected',
'resolved', 'validation_required'] 'resolved', 'validation_required']
// is <list> // <list>
else selectOpts.status = opts.s.split(/[,\s]/) } else selectOpts.status = opts.s.split(/[,\s]/) }
// parse the statuses into Status objects /// Parse the statuses into Status objects.
try { selectOpts.status = try { selectOpts.status =
selectOpts.status.collect { Status.toStatus(it) } } selectOpts.status.collect { Status.toStatus(it) } }
catch (Exception e) { catch (Exception e) {
@ -142,7 +262,7 @@ catch (Exception e) {
println " (abbreviations are accepted.)" println " (abbreviations are accepted.)"
System.exit(1) } System.exit(1) }
// read and parse the priority filter /// Read and parse the `-p` option: priority filter.
if (opts.p) try { if (opts.p) try {
selectOpts.priority = opts.p.toInteger() } selectOpts.priority = opts.p.toInteger() }
catch (NumberFormatException nfe) { catch (NumberFormatException nfe) {
@ -150,14 +270,14 @@ catch (NumberFormatException nfe) {
println "Valid values are: 0-9" println "Valid values are: 0-9"
System.exit(1) } System.exit(1) }
// read and parse the projects filter /// Read and parse the `-r` option: projects filter.
if (opts.r) { selectOpts.projects = if (opts.r) { selectOpts.projects =
opts.r.toLowerCase().split(/[,\s]/).asType(List.class) } opts.r.toLowerCase().split(/[,\s]/).asType(List.class) }
// read and parse the ids filter /// Read and parse the `-i` option: id filter.
if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) } if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) }
// read and parse sort criteria /// Read and parse the `-o` option: sort criteria.
if (opts.o) { if (opts.o) {
def sortProps = opts.o.split(',') def sortProps = opts.o.split(',')
selectOpts.issueSorter = sortProps.collect { prop -> selectOpts.issueSorter = sortProps.collect { prop ->
@ -168,16 +288,14 @@ if (opts.o) {
case ~/^c$/: return { issue -> issue.category } case ~/^c$/: return { issue -> issue.category }
default: return { issue -> issue[prop] } }}} default: return { issue -> issue[prop] } }}}
// read and parse extended property selection criteria /// Read and parse any extended property selection criteria.
if (opts.e) { if (opts.e) {
opts.es.each { option -> opts.es.each { option ->
def parts = option.split("=") def parts = option.split("=")
selectOpts.extendedProperties[parts[0]] = selectOpts.extendedProperties[parts[0]] =
ExtendedPropertyHelp.parse(parts[1]) }} ExtendedPropertyHelp.parse(parts[1]) }}
// TODO: accept projects value from input /// Read and parse the `-C` option: category to assign.
// read and parse the category to assign
if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) } if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) }
catch (Exception e) { catch (Exception e) {
println "Invalid category option: '-C ${e.localizedMessage}'." println "Invalid category option: '-C ${e.localizedMessage}'."
@ -185,7 +303,7 @@ catch (Exception e) {
println " (abbreviations are accepted)." println " (abbreviations are accepted)."
System.exit(1) } System.exit(1) }
// read and parse the status to assign /// Read and parse the `-S` option: status to assign.
if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) } if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) }
catch (Exception e) { catch (Exception e) {
println "Invalid status option: '-S ${e.localizedMessage}'." println "Invalid status option: '-S ${e.localizedMessage}'."
@ -193,25 +311,26 @@ catch (Exception e) {
println " (abbreviations are accepted)." println " (abbreviations are accepted)."
System.exit(1) } System.exit(1) }
// read and parse the priority to assign /// Read and parse the `-P` option: priority to assign.
if (opts.P) try {assignOpts.priority = opts.P.toInteger() } if (opts.P) try {assignOpts.priority = opts.P.toInteger() }
catch (NumberFormatException nfe) { catch (NumberFormatException nfe) {
println "Not a valid priority value: '-P ${opts.P}'." println "Not a valid priority value: '-P ${opts.P}'."
println "Valid values are: 0-9" println "Valid values are: 0-9"
System.exit(1) } System.exit(1) }
/// Read an parse any extended properties to be set.
if (opts.E) { if (opts.E) {
opts.Es.each { option -> opts.Es.each { option ->
def parts = option.split("=") def parts = option.split("=")
assignOpts[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }} assignOpts[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }}
// Read the title if given. /// Read the title if given.
if (opts.title) { assignOpts.title = opts.title } if (opts.title) { assignOpts.title = opts.title }
// Read the text if given /// Read the text if given.
if (opts.text) { assignOpts.text = opts.text } if (opts.text) { assignOpts.text = opts.text }
// set the project working directory /// Set the project working directory.
if (opts.d) { if (opts.d) {
workingDir = new File(opts.d.trim()) workingDir = new File(opts.d.trim())
if (!workingDir.exists()) { if (!workingDir.exists()) {
@ -223,32 +342,33 @@ def EOL = System.getProperty('line.separator')
log.debug("Finished parsing options:\nworkingDir: {}\nselectOpts: {}\nassignOpts: {}", log.debug("Finished parsing options:\nworkingDir: {}\nselectOpts: {}\nassignOpts: {}",
workingDir.canonicalPath, selectOpts, assignOpts) workingDir.canonicalPath, selectOpts, assignOpts)
// ========================= // /// ## Actions ##
// ======== Actions ======== // /// -------------
// ========================= //
// list version information first /// ### Version information.
if (opts.version) { if (opts.version) {
println "PIT CLI Version ${VERSION}" println "PIT CLI Version ${VERSION}"
println "Written by Jonathan Bernard\n" } println "Written by Jonathan Bernard\n" }
/// ----
else { else {
// build issue list /// Build issue list.
log.trace("Building issue database.") log.trace("Building issue database.")
issuedb = new FileProject(workingDir) issuedb = new FileProject(workingDir)
// build filter from options /// Build filter from options.
log.trace("Defining the filter.") log.trace("Defining the filter.")
def filter = new Filter(selectOpts) def filter = new Filter(selectOpts)
// list second /// ### List
if (opts.l) { if (opts.l) {
log.trace("Listing issues.") log.trace("Listing issues.")
// local function (closure) to print a single issue /// Local function (closure) to print a single issue.
def printIssue = { issue, offset -> def printIssue = { issue, offset ->
println "${offset}${issue}" println "${offset}${issue}"
if (opts.v) { if (opts.v) {
@ -260,7 +380,7 @@ if (opts.l) {
println "${offset} * ${name}: ${formattedValue}"} println "${offset} * ${name}: ${formattedValue}"}
println ""}} println ""}}
// local function (closure) to print a project and all visible subprojects /// Local function (closure) to print a project and all visible subprojects.
def printProject def printProject
printProject = { project, offset -> printProject = { project, offset ->
println "\n${offset}${project.name}" println "\n${offset}${project.name}"
@ -268,65 +388,70 @@ if (opts.l) {
project.eachIssue(filter) { printIssue(it, offset) } project.eachIssue(filter) { printIssue(it, offset) }
project.eachProject(filter) { printProject(it, offset + " ") } } project.eachProject(filter) { printProject(it, offset + " ") } }
// print all the issues in the root of this db /// Print all the issues in the root of this db.
issuedb.eachIssue(filter) { printIssue(it, "") } issuedb.eachIssue(filter) { printIssue(it, "") }
/// If the user set the recursive flag print all projects.
if (opts.R) { if (opts.R) {
// print all projects
issuedb.eachProject(filter) { printProject(it, "") }} } issuedb.eachProject(filter) { printProject(it, "") }} }
// daily list second /// ### Daily List
else if (opts.D) { else if (opts.D) {
log.trace("Showing a daily list.") log.trace("Showing a daily list.")
// Parse daily list specific display options /// #### Parse daily list specific display options.
def visibleSections = [] def visibleSections = []
def suppressedSections def suppressedSections
// Parse the additive options first. /// Parse the additive options first.
if (opts.'dl-scheduled') { visibleSections << 'scheduled' } if (opts.'dl-scheduled') { visibleSections << 'scheduled' }
if (opts.'dl-due') { visibleSections << 'due' } if (opts.'dl-due') { visibleSections << 'due' }
if (opts.'dl-reminder') { visibleSections << 'reminder' } if (opts.'dl-reminder') { visibleSections << 'reminder' }
if (opts.'dl-open') { visibleSections << 'open' } if (opts.'dl-open') { visibleSections << 'open' }
// If the user did not add any sections assume they want them all. /// If the user did not add any sections assume they want them all.
if (visibleSections.size() == 0) { if (visibleSections.size() == 0) {
visibleSections = ['scheduled', 'due', 'reminder', 'open'] } visibleSections = ['scheduled', 'due', 'reminder', 'open'] }
// Now go through the negative options. /// Now go through the negative options.
if (opts.'dl-hide-scheduled') { visibleSections -= 'scheduled' } if (opts.'dl-hide-scheduled') { visibleSections -= 'scheduled' }
if (opts.'dl-hide-due') { visibleSections -= 'due' } if (opts.'dl-hide-due') { visibleSections -= 'due' }
if (opts.'dl-hide-reminder') { visibleSections -= 'reminder' } if (opts.'dl-hide-reminder') { visibleSections -= 'reminder' }
if (opts.'dl-hide-open') { visibleSections -= 'open' } if (opts.'dl-hide-open') { visibleSections -= 'open' }
// If the user did not specifically ask for a status filter, we want a /// If the user did not specifically ask for a status filter, we want a
// different filter for the default when we are doing a daily list. /// different default filter when we are doing a daily list.
if (!opts.s) { filter.status = [Status.NEW, Status.VALIDATION_REQUIRED] } if (!opts.s) { filter.status = [Status.NEW, Status.VALIDATION_REQUIRED] }
// If the user did not give a specific sorting order, define our own. /// If the user did not give a specific sorting order, define our own: due
/// date, then priority, then id.
if (!opts.o) { filter.issueSorter = [ {it.due}, {it.priority}, {it.id} ] } if (!opts.o) { filter.issueSorter = [ {it.due}, {it.priority}, {it.id} ] }
// Get our issues /// #### Get all the issues involved.
def allIssues = opts.R ? def allIssues = opts.R ?
// If -R passed, get all issues, including subprojects. /// If `-R` passed, get all issues, including subprojects.
issuedb.getAllIssues(filter) : issuedb.getAllIssues(filter) :
// Otherwise, just use the issues for this project. /// Otherwise, just use the issues for this project.
issuedb.issues.values().findAll { filter ? filter.accept(it) : true } issuedb.issues.values().findAll { filter ? filter.accept(it) : true }
// Set up our time interval. /// Set up our time intervals.
def today = new DateMidnight() def today = new DateMidnight()
def tomorrow = today.plusDays(1) def tomorrow = today.plusDays(1)
/// We are going to sort the issues into these buckets based on when they are
/// scheduled, when they are duem and if they have a reminder set.
def scheduledToday = [] def scheduledToday = []
def dueToday = [] def dueToday = []
def reminderToday = [] def reminderToday = []
def notDueOrReminder = [] def notDueOrReminder = []
/// Helper closure to print an issue.
def printIssue = { issue -> def printIssue = { issue ->
if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}" if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}"
else println " -- ${issue}" } else println " -- ${issue}" }
/// A sorter which sorts by date first, then by priority.
def priorityDateSorter = { i1, i2 -> def priorityDateSorter = { i1, i2 ->
if (i1.priority == i2.priority) { if (i1.priority == i2.priority) {
def d1 = i1.due ?: new DateTime() def d1 = i1.due ?: new DateTime()
@ -335,25 +460,26 @@ else if (opts.D) {
return d1.compareTo(d2) } return d1.compareTo(d2) }
else { return i1.priority - i2.priority }} else { return i1.priority - i2.priority }}
// Sort the issues into seperate lists based on their due dates and /// #### Categorize and sort the issues.
// reminders. /// Sort the issues into seperate lists based on their due dates and
/// reminders.
allIssues.each { issue -> allIssues.each { issue ->
// Find the issues that are scheduled for today. /// * Find the issues that are scheduled for today.
if (issue.scheduled && issue.scheduled < tomorrow) { if (issue.scheduled && issue.scheduled < tomorrow) {
scheduledToday << issue } scheduledToday << issue }
// Find the issues that are due today or are past due. /// * Find the issues that are due today or are past due.
else if (issue.due && issue.due < tomorrow) { dueToday << issue } else if (issue.due && issue.due < tomorrow) { dueToday << issue }
// Find the issues that are not yet due but have a reminder for today or /// * Find the issues that are not yet due but have a reminder for today or
// days past. /// days past.
else if (issue.reminder && issue.reminder < tomorrow) { else if (issue.reminder && issue.reminder < tomorrow) {
reminderToday << issue } reminderToday << issue }
// All the others (not due and no reminder). /// * All the others (not due and no reminder).
else notDueOrReminder << issue } else notDueOrReminder << issue }
// Print the issues /// #### Print the issues
if (visibleSections.contains('scheduled') && scheduledToday.size() > 0) { if (visibleSections.contains('scheduled') && scheduledToday.size() > 0) {
println "Tasks Scheduled for Today" println "Tasks Scheduled for Today"
println "-------------------------" println "-------------------------"
@ -386,7 +512,7 @@ else if (opts.D) {
println "" }} println "" }}
// new issues fourth /// ### Create a New Issue.
else if (opts.n) { else if (opts.n) {
log.trace("Creating a new issue.") log.trace("Creating a new issue.")
@ -394,17 +520,17 @@ else if (opts.n) {
Issue issue Issue issue
def sin = System.in.newReader() def sin = System.in.newReader()
// Set the created extended property /// Set the created extended property.
assignOpts.created = new DateTime() assignOpts.created = new DateTime()
// Prompt for the different options if they were not given on the command /// Prompt for the different options if they were not given on the command
// line. We will loop until they have entered a valid value. How it works: /// line. We will loop until they have entered a valid value. How it works:
// In the body of the loop we will try to read the input, parse it and /// In the body of the loop we will try to read the input, parse it and
// assign it to a variable. If the input is invalid it will throw as /// assign it to a variable. If the input is invalid it will throw an
// exception before the assignment happens, the variable will still be /// exception before the assignment happens, the variable will still be
// null, and we will prompt the user again. /// null, and we will prompt the user again.
// Prompt for category. /// Prompt for category.
while(!assignOpts.category) { while(!assignOpts.category) {
try { try {
print "Category (bug, feature, task): " print "Category (bug, feature, task): "
@ -415,7 +541,7 @@ else if (opts.n) {
println "Valid options are: \n${Category.values().join(', ')}" println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)." } } println " (abbreviations are accepted)." } }
// Prompt for the priority. /// Prompt for the priority.
while (!assignOpts.priority) { while (!assignOpts.priority) {
try { try {
print "Priority (0-9): " print "Priority (0-9): "
@ -423,13 +549,13 @@ else if (opts.n) {
break } break }
catch (e) { println "Not a valid value." } } catch (e) { println "Not a valid value." } }
// Prompt for the issue title. No need to loop as the input does not need /// Prompt for the issue title. No need to loop as the input does not need
// to be validated. /// to be validated.
if (!assignOpts.title) { if (!assignOpts.title) {
println "Issue title: " println "Issue title: "
assignOpts.title = sin.readLine().trim() } assignOpts.title = sin.readLine().trim() }
// Prompt for the issue text. /// Prompt for the issue text.
if (!assignOpts.text) { if (!assignOpts.text) {
assignOpts.text = "" assignOpts.text = ""
println "Enter issue text (use EOF to stop): " println "Enter issue text (use EOF to stop): "
@ -444,35 +570,42 @@ else if (opts.n) {
assignOpts.text += line + EOL } } assignOpts.text += line + EOL } }
catch (e) {} } catch (e) {} }
/// Create the issue.
issue = issuedb.createNewIssue(assignOpts) issue = issuedb.createNewIssue(assignOpts)
println "New issue created: " println "New issue created: "
println issue } println issue }
// last, changes to existing issues /// ### Change Existing Issues.
else if (assignOpts.size() > 0) { else if (assignOpts.size() > 0) {
log.trace("Changing existing issues.") log.trace("Changing existing issues.")
// We are going to add some extra properties if the status is being changed, /// We are going to add some extra properties if the status is being changed,
// because we are nice like that. /// because we are nice like that.
if (assignOpts.status) { switch (assignOpts.status) { if (assignOpts.status) { switch (assignOpts.status) {
case Status.RESOLVED: assignOpts.resolved = new DateTime(); break case Status.RESOLVED: assignOpts.resolved = new DateTime(); break
case Status.REJECTED: assignOpts.rejected = new DateTime(); break case Status.REJECTED: assignOpts.rejected = new DateTime(); break
default: break }} default: break }}
/// #### processIssue
/// A local function to handle the changes for one issue.
def processIssue = { issue -> def processIssue = { issue ->
println issue println issue
/// Walk the assigned options map and set the properties on the issue.
assignOpts.each { propName, value -> assignOpts.each { propName, value ->
issue[propName] = value issue[propName] = value
def formattedValue = ExtendedPropertyHelp.format(value) def formattedValue = ExtendedPropertyHelp.format(value)
println " set ${propName} to ${formattedValue}" } } println " set ${propName} to ${formattedValue}" } }
/// If the user passed `-R`, walk the whole project, including subprojects.
if (opts.R) { issuedb.walkProject(filter, processIssue) } if (opts.R) { issuedb.walkProject(filter, processIssue) }
/// Otherwise, just process the issues in this project.
else { else {
issuedb.issues.values() issuedb.issues.values()
.findAll { filter ? filter.accept(it) : true } .findAll { filter ? filter.accept(it) : true }
.each(processIssue) }} .each(processIssue) }}
/// ### Invalid Input
else { else {
log.trace("Unknown request.") log.trace("Unknown request.")
cli.usage(); return -1 }} cli.usage(); return -1 }}