Compare commits

..

22 Commits
2.6.1 ... 3.3.3

Author SHA1 Message Date
76d1e48ebb Fixed a typo in an error message. 2012-08-30 06:26:39 -07:00
4fd297e03d CLI: Changed the daily list upcoming section behavior.
Upcoming now includes issues due within the next week by default. The number of
days to look ahead is configurable with the --dl-upcoming-days option.
2012-08-30 06:17:07 -07:00
d0e968b2b7 New jlp-based documentation for pit-cli. 2012-02-13 12:12:26 -06:00
c0b02ca222 Bugfixes on pit-cli:
* Fixed a bug in the option parsing. When no options (or unknown options) where
  presented it was not properly defaulting.
* Fixed a bug when incorrect options where given. Apache Commons CLI fails
  entirely when it is unable to parse an option. This means we cannot get the
  `--dir` option and we default to the program's working directory. When running
  on Nailgun this is not the desired behavior and can cause pit to look through
  a very deep file heirarchy to find issues.
2012-02-13 10:49:27 -06:00
85753de955 PIT CLI agenda sorting, FileIssue bug fixed.
* Fixed the bug in FileIssue where it would append a blank line to the body of
  an issue every time it wrote the issue to the file. Fixed by making the
  parser consume a line break before the property section.
* Fixed PIT CLI -D option sorting of issues so that it will sort issues by
  priority, then due date.
2011-12-19 16:25:25 -06:00
0441f3c510 Added -R option for PIT CLI. 2011-12-19 16:13:15 -06:00
c01eaa0255 Issue tracker: started issue tracking. 2011-12-18 22:57:19 -06:00
ec7c07f81f Bug fix in FileIssue.
* Property changes that changed the filename of the underlying file were
  neglecting to update the internal file pointer to the new file.
2011-12-12 15:56:10 -06:00
952064d903 Fixed FileIssue formatting and extended properties.
* The extended properties table was using the maxKeyLength for both keys and
  values.
* FileIssue was not persisting the file when extended properties were updated.
2011-12-08 16:01:54 -06:00
31b9802477 Fixed PIT CLI output when setting extended properties. 2011-12-08 15:37:06 -06:00
ae0d782a5b PIT CLI options -e, -E, --title, --text.
PIT CLI
-------

* Added an option, `-e`, to filter by extended properties and its complement,
  `-E`, to set extended properties. Format of the option argument is
  `<propName>=<propValue>`.
* Added the `--title` and `--text` options to specify the title and text of an
  issue on the command line.
* When a new issue is created or an issue is set to rejected or resolved status
  a timestamp is added as an extended property: `created`, `rejected`, and
  `resolved` are the property names respectively.
2011-12-08 15:02:33 -06:00
846d1edc74 Filter and FileProject support for extended properties.
* Added support for extended properties to `Filter`. Unlike the basic
  properties, which allow the caller to specify a list of acceptable values, the
  extended properties only supports a map of properties, all of which must be
  available on the issue and match the given value.
* Made `Issue` a concrete class. There was no reason it could not be a concrete
  object. The main difference is that the extending classes all add some sort of
  persistence to the issue, but having an in-memory `Issue` in its most basic
  form can be useful and should be allowed.
* Changed the default priority for new issues from 9 to 5.
* Bug fix on `FileIssue.formatIssue()`. If was not properly returning its
  result leading to failure if no extended properties were given.
* Reworked `FileIssue.formatIssue()` to only print the horizontal rule
  seperating the issue text from the extended properties if there are extended
  properties to print as well.
* Changed the `FileProject.createNewIssue()` to handle extended properties and
  handle the basic properties in a manner more consistent with `Issue`. It is
  now creating an `Issue` object and using the `FileIssue.formatIssue()`
  function to write that to a new issue file, then loading that file back in as
  a `FileIssue`. This cuts down on code duplication and makes it so that the
  options map that `FileProject.createNewIssue()` expects is the same as the
  options to the `Issue` constructor.
2011-12-08 14:38:24 -06:00
fd94f0e41a PIT CLI Options -o (order issues), -D (daily list).
* Added the order issues option (`-o`) to pit-cli. This option allows you to
  specific a sorting criteria for the issues returned. The form of the option is
  `-o <property>,<property>` where the properties can be any properties of the
  issues being sorted. The option supports short one-letter forms of the basic
  properties (id, priority, status, and category).
* Added the daily list mode (activated with the `-D` option). This mode prints
  out the tasks scheduled for today (based on the `scheduled` property, the
  tasks due today (based on he `due` property), the issues which have a reminder
  that is active (based on the `reminder` property), and the remaining open
  issues (those which do not fall in any of the other categories). This mode
  looks through all projects in the given repository and does not do any
  filtering or visual seperation by project.

  The individual sections can be suppressed or singled out by using the
  following options.

    Additive options (if none are given all sections are present).

    * `--dl-scheduled`
    * `--dl-due`
    * `--dl-reminder`
    * `--dl-open`

    Negative options (these suppress a specific section).

    * `--dl-hide-scheduled`
    * `--dl-hide-due`
    * `--dl-hide-reminder`
    * `--dl-hide-open`

  Additive options are useful if you only want one or two sections. Negative
  options are useful if you only want to exclude one or two sections.  It does
  not make sense to use both additive options and negative options, but it is
  allowed.
2011-12-07 18:15:01 -06:00
6f58a83ad4 Multiple sort criteria to filters, bugfix, and cleanup.
* Fixed a bug in the common build. This bug is fixed in version 1.9, but I am
  patching this bug locally in 1.6 until I have evaluated 1.9 with this project.
* Moved `ExtendedPropertyHelp` to `com.jdbernard.pit` from
  `com.jdbernard.pit.file`.
* Added a number of property types to `ExtendedPropertyHelp`. New additions are:

    * `java.util.Calendar` *object -> string value only*
    * `java.util.Date` *object -> string value only*
    * `java.lang.Long` *this replaces `java.util.Integer`*
    * `java.lang.Float` *object -> string value only*
    * `java.lang.Double`
* Cleaned up the `matches(String)` and `matches(Class)` functions of
  `ExtendedPropertyHelp`
* Modified `Filter` sorter behaviours. The `issueSorter` and `projectSorter`
  fields are now allowed to be either a closure or a list of closures. A single
  closure works as it did before. The list of closures allows the caller to
  specify multiple sort criteria. The individual criteria closures are applied
  in reverse order, so that the first item in the sorter list is the most
  significant criteria. For example, if the caller set the sorter to
  `[{it.category},{it.priority}]` then the issues would be sorted first by
  priority and then sorted again by category, meaning that the resulting data
  would be ordered first by the category of the issue and then by the priority
  for issues that share the same category.
* Modified the methods in `Project` that use `Filter` objects to conform to the
  above behavior regarding sorting. It may be a better idea though to move the
  sort code all into `Filter` so that it is in one place.
* Cleaned up the code in `Status` for matching status based on given symbols or
  partial status names.
2011-12-07 18:01:18 -06:00
47cf3cf0a4 Updated build to include pit-cli runtime libs. 2011-11-22 14:44:48 -06:00
e00e2e296e Updated and optimized --version option in pit-clit. 2011-11-22 14:39:55 -06:00
c26ba17dbb Refactored pit-cli build process to work with JDB common build. 2011-11-21 03:03:52 -06:00
447e74f956 Implemented extended attributes on File issues.
* Created a PEG parser for issue files.
* Added parsing and formatting code to `FileIssue` to handle extended
  properties.
2011-11-21 00:46:24 -06:00
66b68160e5 Moved to JDB common build structure. 2011-11-20 21:00:53 -06:00
f86316c68f Continued implementation of extended attributes.
* Changed the `Issue` constructor to use an attribute map instead of an
  increasingly long parameter list. Of course we lose some control over required
  parameters.
* Added the Joda Time and SLF4J logging libraries.
* Implemented the `FileIssue` constructor for the new `Issue` refactor.
2011-11-05 08:44:47 -05:00
5ff4665a07 Starting work on extended attributes.
* `Issue` implementation is tentatively complete.
* Started ona direction for implementation with `FileIssue`.
* Revisiting the code for `XmlIssue` to polish it up. The XML code was never
  really finished, but it should be for 3.0.x
2011-11-03 02:39:36 -05:00
faacfd859a Starting version 3.0.x.
* Major differences will be with `Issues`. The structure of issues will not be
  as tied to the original `FileIssue` implementation and will support arbitrary
  attributes.
* Removed 2.6.x files and updated versioning properties.
* Fixed a typo in pit-cli
* Temporarily removed the invocation of pit-swing's build file until I have that
  sorted. It was not working.
2011-11-03 02:36:35 -05:00
64 changed files with 1426 additions and 707 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
release/
*.sw* *.sw*
*/build/ */build/

View File

@ -9,7 +9,7 @@
<target name="clean"> <target name="clean">
<ant dir="libpit" target="clean" inheritAll="false"/> <ant dir="libpit" target="clean" inheritAll="false"/>
<ant dir="pit-cli" target="clean" inheritAll="false"/> <ant dir="pit-cli" target="clean" inheritAll="false"/>
<ant dir="pit-swing" target="clean" inheritAll="false"/> <!-- <ant dir="pit-swing" target="clean" inheritAll="false"/> -->
</target> </target>
<target name="libpit"> <target name="libpit">
@ -21,15 +21,17 @@
<ant dir="pit-cli" target="release" inheritAll="false"/> <ant dir="pit-cli" target="release" inheritAll="false"/>
</target> </target>
<target name="pit-swing" depends="libpit"> <!-- <target name="pit-swing" depends="libpit">
<copy file="${libpit.jar}" todir="pit-swing/lib"/> <copy file="${libpit.jar}" todir="pit-swing/lib"/>
<ant dir="pit-swing" fork="true" target="package" inheritAll="false"/> <ant dir="pit-swing" fork="true" target="package" inheritAll="false"/>
</target> </target> -->
<target name="package" depends="libpit,pit-cli,pit-swing"> <!-- <target name="package" depends="libpit,pit-cli,pit-swing"> -->
<target name="package" depends="libpit,pit-cli">
<mkdir dir="release/lib"/> <mkdir dir="release/lib"/>
<copy file="pit-cli/release/pit-clii-${application.version}.jar" todir="release"/> <copy file="pit-cli/release/pit-clii-${application.version}.jar" todir="release"/>
<copy file="pit-swing/dist/jar/pit-swing.jar" tofile="release/pit-swing-${application.version}.jar"/> <!-- <copy file="pit-swing/dist/jar/pit-swing.jar"
tofile="release/pit-swing-${application.version}.jar"/> -->
<copy file="libpit/release/pit-${application.version}.jar" todir="release/lib"/> <copy file="libpit/release/pit-${application.version}.jar" todir="release/lib"/>
</target> </target>
</project> </project>

13
issues/libpit/0000bs5.rst Normal file
View File

@ -0,0 +1,13 @@
FileIssue is not formatting output in the same way it parses input.
===================================================================
`FileIssue.formatIssue(Issue)` introduces at least one extra line to the end
of the issue body compared to what is parsed in using the PegParser.
----
========= ===================
Created : 2011-12-18T22:53:45
Resolved: 2011-12-19T16:09:50
========= ===================

203
jdb-build-1.6.xml Normal file
View File

@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<project name="Jonathan Bernard Build Common">
<property environment="env"/>
<!--======== INIT TARGETS ========-->
<target name="-init" depends="-common-init,init"/>
<target name="-common-init">
<!-- Set default values for some key properties. Since properties are
write once, any value set before this point takes precedence. -->
<property name="versioning.file" value="project.properties"/>
<property name="src.dir" value="${basedir}/src"/>
<property name="build.dir" value="${basedir}/build"/>
<property name="lib.dir" value="${basedir}/lib"/>
<property name="resources.dir" value="${basedir}/resources"/>
<!--======== PATHS ========-->
<path id="groovy.classpath">
<fileset dir="${env.GROOVY_HOME}/lib">
<include name="*.jar"/>
</fileset>
</path>
<path id="groovy.embeddable">
<fileset dir="${env.GROOVY_HOME}/embeddable">
<include name="*.jar"/>
</fileset>
</path>
<path id="compile-libs">
<fileset dir="${build.dir}/lib/compile/jar">
<include name="*.jar"/>
</fileset>
</path>
<path id="runtime-libs">
<fileset dir="${build.dir}/lib/runtime/jar">
<include name="*.jar"/>
</fileset>
</path>
</target>
<target name="-init-groovy">
<taskdef name="groovyc" classpathref="groovy.classpath"
classname="org.codehaus.groovy.ant.Groovyc"/>
<taskdef name="groovy" classpathref="groovy.classpath"
classname="org.codehaus.groovy.ant.Groovy"/>
</target>
<target name="init"/>
<target name="clean" depends="-init">
<delete dir="${build.dir}"/>
</target>
<!--======== LIBRARY TARGETS ========-->
<target name="-lib" depends="-lib-local,-lib-ivy,lib"/>
<target name="lib"/>
<target name="-lib-ivy" unless="${lib.local}"/>
<target name="-lib-local" if="${lib.local}">
<echo message="Resolving libraries locally."/>
<mkdir dir="${build.dir}/lib/compile/jar"/>
<mkdir dir="${build.dir}/lib/runtime/jar"/>
<copy todir="${build.dir}/lib/compile/jar" failonerror="false">
<fileset dir="${lib.dir}/compile/jar"/>
</copy>
<copy todir="${build.dir}/lib/runtime/jar" failonerror="false">
<fileset dir="${lib.dir}/runtime/jar"/>
</copy>
</target>
<!--======== VERSIONING TARGETS ========-->
<target name="increment-build-number" depends="-init">
<propertyfile file="${versioning.file}">
<entry key="build.number" default="0" type="int" value="1"
operation="+"/>
</propertyfile>
</target>
<target name="set-version" depends="-common-init">
<input
message="The current version is ${version}. Enter a new version: "
addproperty="new-version"/>
<propertyfile file="${versioning.file}">
<entry key="version" value="${new-version}" operation="="
type="string"/>
<entry key="build.number" value="0" type="int" operation="="/>
</propertyfile>
</target>
<!--======== COMPILATION TARGETS ========-->
<target name="-compile-groovy" depends="-init,-init-groovy,-lib">
<mkdir dir="${build.dir}/main/classes"/>
<groovyc srcdir="${src.dir}/main" destdir="${build.dir}/main/classes"
includeAntRuntime="false" fork="yes">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
</classpath>
<javac/>
</groovyc>
</target>
<target name="-compile-java" depends="-init,-lib">
<mkdir dir="${build.dir}/main/classes"/>
<javac srcdir="${src.dir}/main" destdir="${build.dir}/main/classes"
includeAntRuntime="false" classpathref="compile-libs"/>
</target>
<target name="compile" depends="-compile-groovy"/>
<!--======== JUNIT TARGETS ========-->
<target name="-compile-tests-groovy" depends="-init,compile">
<mkdir dir="${build.dir}/test/classes"/>
<groovyc srcdir="${src.dir}/test" destdir="${build.dir}/test/classes"
includeAntRuntime="false" fork="true">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
</classpath>
</groovyc>
</target>
<target name="-compile-tests-java" depends="-init,compile">
<mkdir dir="${build.dir}/test/classes"/>
<javac srcdir="${src.dir}/test" destdir="${build.dir}/test/classes"
includeAntRuntime="false">
<classpath>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
</classpath>
</javac>
</target>
<target name="compile-tests" depends="-compile-tests-groovy"/>
<target name="run-tests" depends="compile-tests,resources-test">
<junit printsummary="true">
<classpath>
<path refid="groovy.classpath"/>
<path refid="compile-libs"/>
<path location="${build.dir}/main/classes"/>
<path location="${build.dir}/test/classes"/>
</classpath>
<formatter type="plain" usefile="false"/>
<batchtest>
<fileset dir="${build.dir}/test/classes">
<include name="**/*"/>
</fileset>
</batchtest>
</junit>
</target>
<!--======== RESOURCES TARGETS ========-->
<target name="resources" depends="-init">
<mkdir dir="${build.dir}/main/classes"/>
<copy todir="${build.dir}/main/classes" failonerror="false">
<fileset dir="${resources.dir}/main/"/>
</copy>
</target>
<target name="resources-test" depends="-init">
<mkdir dir="${build.dir}/test/classes"/>
<copy todir="${build.dir}/test/classes" failonerror="false">
<fileset dir="${resources.dir}/test/"/>
</copy>
</target>
<!--======== BUILD TARGETS ========-->
<target name="-build-modular"
depends="compile,increment-build-number,resources">
<jar destfile="${build.dir}/${name}-${version}.${build.number}.jar"
basedir="${build.dir}/main/classes"/>
</target>
<target name="-build-packed-libs"
depends="compile,increment-build-number,resources">
<unjar destdir="${build.dir}/main/classes">
<fileset dir="${build.dir}/lib/runtime/jar"/>
</unjar>
<jar destfile="${build.dir}/${name}-${version}.${build.number}.jar"
basedir="${build.dir}/main/classes"/>
</target>
<target name="build" depends="-build-modular"/>
</project>

View File

@ -1,30 +1,8 @@
<project name="Personal Issue Tracker" default="release"> <project name="Personal Issue Tracker" default="release">
<property file="../version.properties"/>
<property file="project.properties"/> <property file="project.properties"/>
<property environment="env"/>
<path id="groovy.libs"> <import file="../jdb-build-1.6.xml"/>
<fileset dir="${env.GROOVY_HOME}/lib">
<include name="**/*.jar"/>
</fileset>
</path>
<path id="groovyc.classpath">
<path refid="groovy.libs"/>
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
<pathelement path="${build.dir}/classes"/>
</path>
<path id="test.classpath">
<path refid="groovyc.classpath"/>
<pathelement path="${build.dir}/tests"/>
</path>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc"
classpathref="groovy.libs"/>
<target name="init"> <target name="init">
<fail <fail
@ -33,79 +11,17 @@
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/> <echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
</target> </target>
<target name="increment-build-number" depends="init"> <target name="clean-all" depends="clean">
<!-- Check to see if the application version has changed. <delete dir="${release.dir}"/>
If it has, reset the build number to 0 -->
<condition property="build.number.final"
value="${build.number}"
else="0" >
<equals
arg1="${application.version}"
arg2="${expected.application.version}"/>
</condition>
<echo message="Version: ${application.version}"/>
<echo message="Build number: ${build.number.final}"/>
<!-- Write the actual application version and build number -->
<propertyfile file="project.properties">
<entry key="build.number" value="${build.number.final}"/>
<entry
key="expected.application.version"
value="${application.version}"/>
</propertyfile>
<!-- increment build number -->
<propertyfile file="project.properties">
<entry key="build.number" operation="+" type="int" default="0"/>
</propertyfile>
<property file="project.properties"/>
</target>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
<target name="compile" depends="init,increment-build-number">
<mkdir dir="${build.dir}/classes"/>
<groovyc
srcdir="${src.dir}"
destdir="${build.dir}/classes"
classpathref="groovyc.classpath"/>
</target>
<target name="compile-tests" depends="init,compile">
<mkdir dir="${build.dir}/tests"/>
<groovyc
srcdir="${test.dir}"
destdir="${build.dir}/tests"
classpathref="groovyc.classpath"/>
</target>
<target name="test" depends="compile-tests">
<junit fork="yes" haltonfailure="yes">
<classpath refid="test.classpath"/>
<formatter type="brief" usefile="false" />
<batchtest>
<fileset dir="${build.dir}/tests">
<include name="**/*Test.class"/>
</fileset>
</batchtest>
</junit>
</target>
<target name="build" depends="compile,test">
<mkdir dir="${build.dir}/jar"/>
<jar
destfile="${build.dir}/jar/pit-${application.version}.${build.number.final}.jar"
basedir="${build.dir}/classes"
compress="on"/>
</target> </target>
<target name="release" depends="build"> <target name="release" depends="build">
<delete dir="${release.dir}"/> <mkdir dir="${release.dir}/lib"/>
<mkdir dir="${release.dir}"/> <copy file="${build.dir}/${name}-${version}.${build.number}.jar"
<copy file="${build.dir}/jar/pit-${application.version}.${build.number.final}.jar" tofile="${release.dir}/${name}-${version}.jar"/>
tofile="${release.dir}/${release.jar}"/> <copy todir="${release.dir}/lib">
<fileset dir="${build.dir}/lib/runtime/jar"/>
</copy>
</target> </target>
</project> </project>

7
libpit/doc/grammar.txt Normal file
View File

@ -0,0 +1,7 @@
IssueFile - Title Body PropertyBlock?
Title - ONE_LINE TITLE_SEPARATOR
Body - ANY_LINE+
Separator - DASH{4} NEW_LINE
PropertyBlock - HorizontalRule TableSeparator PropertyDefinition+ TableSeparator
TableSeparator -
PropertyDefinition - PropertyKey COLON PropertyValue

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,11 +1,13 @@
#Wed, 26 Oct 2011 14:24:41 -0500 #Thu, 30 Aug 2012 05:57:48 -0700
#Sat Apr 24 17:08:00 CDT 2010 #Sat Apr 24 17:08:00 CDT 2010
build.dir=build build.dir=build
src.dir=src src.dir=src
lib.shared.dir=../shared-libs lib.shared.dir=../shared-libs
test.dir=test test.dir=test
build.number=1 build.number=2
expected.application.version=2.6.1 version=3.3.3
name=libpit
lib.dir=lib lib.dir=lib
lib.local=true
release.dir=release release.dir=release
release.jar=pit-${application.version}.jar release.jar=pit-${application.version}.jar

Binary file not shown.

View File

@ -0,0 +1,13 @@
import com.jdbernard.pit.*
import com.jdbernard.pit.file.*
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.parboiled.parserunners.RecoveringParseRunner
parser = Parboiled.createParser(IssuePegParser.class)
parseRunner = new ReportingParseRunner(parser.IssueFile())
issueFile = new File('/Volumes/NO NAME/Dropbox/tasks/0015tn3.rst')
issueText = issueFile.text
result = parseRunner.run(issueText)
issueMap = result.valueStack.pop()

View File

@ -1,33 +0,0 @@
package com.jdbernard.pit
class Filter {
List<Category> categories = null
List<Status> status = null
List<String> projects = null
List<String> ids = null
int priority = 9
boolean acceptProjects = true
Closure issueSorter = defaultIssueSorter
Closure projectSorter = defaultProjectSorter
public static Closure defaultIssueSorter = { it.id.toInteger() }
public static Closure defaultProjectSorter = { it.name }
public boolean accept(Issue i) {
return (i.priority <= priority &&
(!categories || categories.contains(i.category)) &&
(!status || status.contains(i.status)) &&
(!ids || ids.contains(i.id)))
}
public boolean accept(Project p) {
return (acceptProjects &&
(!projects || projects.contains(p.name)))
}
public boolean accept(String name) {
return (acceptProjects &&
(!projects || projects.contains(name)))
}
}

View File

@ -1,41 +0,0 @@
package com.jdbernard.pit
public enum Status {
REASSIGNED('a'),
REJECTED('j'),
NEW('n'),
RESOLVED('s'),
VALIDATION_REQUIRED('v')
String symbol
protected Status(String s) { symbol = s }
public static Status toStatus(String str) {
Status retVal = null
for(status in Status.values()) {
if (status.symbol.equalsIgnoreCase(str) ||
status.name().startsWith(str.toUpperCase())) {
if (retVal != null)
throw new IllegalArgumentException("Request string is" +
" ambigous, '${str}' could represent ${retVal} or " +
"${status}, possibly others.")
retVal = status
}
}
if (retVal == null)
throw new IllegalArgumentException("No status matches '${str}'")
return retVal
}
public String toString() {
def words = name().split("_")
String result = ""
words.each { result += "${it[0]}${it[1..-1].toLowerCase()} " }
return result[0..-2]
}
}

View File

@ -1,110 +0,0 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
import java.lang.IllegalArgumentException as IAE
public class FileIssue extends Issue {
protected File source
public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/
public FileIssue(File file) {
super('REPLACE_ME')
def matcher = file.name =~ fileExp
if (!matcher)
throw new IllegalArgumentException("${file} " +
"is not a valid Issue file.")
super.@id = matcher[0][1]
super.@category = Category.toCategory(matcher[0][2])
super.@status = Status.toStatus(matcher[0][3])
super.@priority = matcher[0][4].toInteger()
this.source = file
super.@text = file.text
}
public void setCategory(Category c) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, c, status, priority)))
if (!renamed)
throw new IOException("I was unable to set the category. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setCategory(c)
}
public void setStatus(Status s) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, category, s, priority)))
if (!renamed)
throw new IOException("I was unable to set the status. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setStatus(s)
}
public void setPriority(int p) throws IOException {
boolean renamed
renamed = source.renameTo(new File(source.canonicalFile.parentFile,
makeFilename(id, category, status, p)))
if (!renamed)
throw new IOException("I was unable to set the priority. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).")
else super.setPriority(p)
}
public String getFilename() {
return makeFilename(id, category, status, priority)
}
public void setText(String text) throws IOException {
try { source.write(text) }
catch (IOException ioe) {
throw new IOException("I could not save the new text for this "
+ "issue. I can not write to the file for this issue. I do not"
+ " know why, I am sorry (maybe the file can not be reached).")
}
super.setText(text)
}
boolean deleteFile() { return source.deleteDir() }
public static boolean isValidFilename(String name) {
return name ==~ fileExp
}
public static String makeFilename(String id, Category category,
Status status, int priority) {
// bounds check priority
priority = Math.min(9, Math.max(0, priority))
//check for valid values of cateogry and id
if (category == null)
throw new IAE("Category must be non-null.")
if (status == null)
throw new IAE("Status must be non-null.")
if (!(id ==~ /\d+/))
throw new IAE( "'${id}' is not a legal value for id.")
return id + category.symbol + status.symbol + priority + ".rst";
}
}

View File

@ -0,0 +1,81 @@
package com.jdbernard.pit
import org.joda.time.DateMidnight
import org.joda.time.DateTime
import java.text.SimpleDateFormat
public enum ExtendedPropertyHelp {
// Property types should be ordered here in order of decreasing specificity.
// That is, subclasses should come before the more general class so that
// objects are converted using the most specific class that
// ExtendedPropertyHelp knows how to work with.
DATE_MIDNIGHT(/^\d{4}-\d{2}-\d{2}$/, DateMidnight,
{ v -> DateMidnight.parse(v) },
{ d -> d.toString("YYYY-MM-dd") }),
DATETIME(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, DateTime,
{ v -> DateTime.parse(v) },
{ d -> d.toString("YYYY-MM-dd'T'HH:mm:ss") }),
// We never want to parse a value into a java.util.Date or
// java.util.Calendar object (we are using Joda Time instead of the
// standard Java Date and Calendar objects) but we do want to be able to
// handle if someone gives us a Date or Calendar object.
DATE(NEVER_MATCH, Date,
{ v -> v }, // never called
{ d -> dateFormat.format(d) }),
CALENDAR(NEVER_MATCH, Calendar,
{ v -> v }, // never called
{ c ->
def df = dateFormat.clone()
df.calendar = c
df.format(c.time) }),
INTEGER(NEVER_MATCH, Integer,
{ v -> v as Integer }, // never called
{ i -> i as String }),
LONG(/^\d+$/, Long,
{ v -> v as Long },
{ l -> l as String }),
FLOAT(NEVER_MATCH, Float,
{ v -> v as Float}, // never called
{ f -> f as String}),
DOUBLE(/^\d+\.\d+$/, Double,
{ v -> v as Double },
{ d -> d as String });
String pattern;
Class klass;
def parseFun, formatFun;
private static SimpleDateFormat dateFormat =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
// This pattern for can never match (is uses negative lookahead to
// contradict itself).
private static String NEVER_MATCH = /(?!x)x/;
public ExtendedPropertyHelp(String pattern, Class klass, def parseFun,
def formatFun) {
this.pattern = pattern
this.klass = klass
this.parseFun = parseFun
this.formatFun = formatFun }
public boolean matches(String prop) { return prop ==~ pattern }
public boolean matches(Class klass) { return this.klass == klass }
public static Object parse(String value) {
def propertyType = ExtendedPropertyHelp.values().find {
it.matches(value) }
return propertyType ? propertyType.parseFun(value) : value }
public static String format(def object) {
def propertyType = ExtendedPropertyHelp.values().find {
it.klass.isInstance(object) }
return propertyType ? propertyType.formatFun(object) : object.toString() }
}

View File

@ -0,0 +1,42 @@
package com.jdbernard.pit
class Filter {
List<Category> categories = null
List<Status> status = null
List<String> projects = null
List<String> ids = null
Map<String, Object> extendedProperties = null
int priority = 9
boolean acceptProjects = true
def issueSorter = defaultIssueSorter
def projectSorter = defaultProjectSorter
public static Closure defaultIssueSorter = { it.id.toInteger() }
public static Closure defaultProjectSorter = { it.name }
public boolean accept(Issue i) {
return (
// Needs to meet the priority threshold.
i.priority <= priority &&
// Needs to be in one of the filtered categories (if given)
(!categories || categories.contains(i.category)) &&
// Needs to have one of the filtered statuses (if given)
(!status || status.contains(i.status)) &&
// Needs to be one of the filtered ids (if given)
(!ids || ids.contains(i.id)) &&
// Needs to have all of the extended properties (if given)
(!extendedProperties ||
extendedProperties.every { name, value -> i[name] == value }))
}
public boolean accept(Project p) {
return (acceptProjects &&
(!projects || projects.contains(p.name)))
}
public boolean accept(String name) {
return (acceptProjects &&
(!projects || projects.contains(name)))
}
}

View File

@ -2,25 +2,30 @@ package com.jdbernard.pit
import java.lang.IllegalArgumentException as IAE import java.lang.IllegalArgumentException as IAE
public abstract class Issue { public class Issue {
protected String id protected String id
protected Category category protected Category category
protected Status status protected Status status
protected int priority protected int priority
protected String text protected String text
protected Date deliveryDate protected String title
protected Date creationDate
Issue(String id, Category c = Category.TASK, Status s = Status.NEW, Map extendedProperties = [:]
int p = 9) {
this.id = id Issue(Map props) {
this.category = c this.id = props.id
this.status = s this.category = props.category ?: Category.TASK
this.priority = p this.status = props.status ?: Status.NEW
this.creationDate = new Date() this.priority = props.priority ?: 5
this.deliveryDate = null this.title = props.title ?: ''
} this.text = props.text ?: ''
// Put all the non-native properties into our extendedProperties map.
def nativeProps =
["id", "category", "status", "priority", "title", "text"]
extendedProperties.putAll(props.findAll {
!nativeProps.contains(it.key) })}
public String getId() { return id; } public String getId() { return id; }
@ -48,19 +53,18 @@ public abstract class Issue {
priority = Math.min(9, Math.max(0, p)) priority = Math.min(9, Math.max(0, p))
} }
public String getTitle() { return text.readLines()[0] } public String getTitle() { return title }
public void setTitle(String t) throws IOException { title = t }
public String getText() { return text } public String getText() { return text }
public void setText(String t) throws IOException { text = t } public void setText(String t) throws IOException { text = t }
public boolean hasDelivery() { return deliveryDate == null } public def propertyMissing(String name) { extendedProperties[name] }
public Date getCreationDate() { return creationDate } public def propertyMissing(String name, def value) {
extendedProperties[name] = value }
public Date getDeliveryDate() { return deliveryDate }
public void setDeliveryDate(Date dd) { deliveryDate = dd }
@Override @Override
public String toString() { public String toString() {

View File

@ -10,30 +10,32 @@ public abstract class Project {
public void eachIssue(Filter filter = null, Closure c) { public void eachIssue(Filter filter = null, Closure c) {
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
for (i in issues.values().sort(sorter)) for (i in sort(issues.values(), sorter))
if (!filter || filter.accept(i)) if (!filter || filter.accept(i))
c.call(i) c.call(i) }
}
public void eachProject(Filter filter = null, Closure c) { public void eachProject(Filter filter = null, Closure c) {
def sorter = filter?.projectSorter ?: Filter.defaultProjectSorter def sorter = filter?.projectSorter ?: Filter.defaultProjectSorter
for (p in projects.values().sort(sorter)) for (p in sort(projects.values(), sorter))
if (!filter || filter.accept(p)) if (!filter || filter.accept(p))
c.call(p) c.call(p) }
}
// walk every issue and project in this project recursively and execute the // walk every issue and project in this project recursively and execute the
// given closure on each issue that meets the filter criteria // given closure on each issue that meets the filter criteria
public void walkProject(Filter filter, Closure c) { public void walkProject(Filter filter, Closure c) {
this.eachIssue(filter, c) this.eachIssue(filter, c)
this.eachProject(filter) { p -> p.walkProject(filter, c) } this.eachProject(filter) { p -> p.walkProject(filter, c) } }
}
// This get all issues, including subissues // This get all issues, including subissues
public List getAllIssues(Filter filter = null) { public List getAllIssues(Filter filter = null) {
List result = this.issues.findAll { filter.accept(it) } def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
this.eachProject(filter) { p -> result += p.getAllIssues(filter) }
} List allIssues = this.issues.values().findAll {
filter ? filter.accept(it) : true }
this.eachProject(filter) { p -> allIssues += p.getAllIssues(filter) }
return sort(allIssues, sorter) }
public void setName(String name) { this.name = name } public void setName(String name) { this.name = name }
@ -49,4 +51,10 @@ public abstract class Project {
public abstract boolean deleteIssue(Issue issue) public abstract boolean deleteIssue(Issue issue)
public abstract boolean deleteProject(Project project) public abstract boolean deleteProject(Project project)
protected List sort(def collection, def sorter) {
if (sorter instanceof Closure) {
return collection.sort(sorter) }
else if (sorter instanceof List) {
return sorter.reverse().inject(collection) { c, s -> c.sort(s) }}}
} }

View File

@ -0,0 +1,41 @@
package com.jdbernard.pit
public enum Status {
REASSIGNED('a'),
REJECTED('j'),
NEW('n'),
RESOLVED('s'),
VALIDATION_REQUIRED('v')
String symbol
protected Status(String s) { symbol = s }
public static Status toStatus(String str) {
// Try to match based on symbol
def match = Status.values().find {it.symbol.equalsIgnoreCase(str)}
if (match) { return match }
// No match on the symbol, look for the status name (or abbreviations)
match = Status.values().findAll {
it.name().startsWith(str.toUpperCase()) }
// No matching status, oops.
if (match.size() == 0) {
throw new IllegalArgumentException("No status matches '${str}'") }
// More than one matching status, oops.
else if (match.size() > 1) {
throw new IllegalArgumentException("Request string is" +
" ambigous, '${str}' could represent any of ${match}.")}
// Only one matching status, yay!
else { return match[0] }}
public String toString() {
def words = name().split("_")
String result = ""
words.each { result += "${it[0]}${it[1..-1].toLowerCase()} " }
return result[0..-2]
}
}

View File

@ -0,0 +1,189 @@
package com.jdbernard.pit.file
import com.jdbernard.pit.*
import java.lang.IllegalArgumentException as IAE
import org.parboiled.Parboiled
import org.parboiled.parserunners.ReportingParseRunner
import org.slf4j.Logger
import org.slf4j.LoggerFactory
public class FileIssue extends Issue {
protected File source
private Logger log = LoggerFactory.getLogger(getClass())
public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/
protected static parseRunner
static {
def parser = Parboiled.createParser(IssuePegParser)
parseRunner = new ReportingParseRunner(parser.IssueFile()) }
public FileIssue(File file) {
super(id: -1, title: 'REPLACE_ME')
if (log.isDebugEnabled()) {
log.debug("Loading a FileIssue from '{}'", file.canonicalPath) }
def matcher = file.name =~ fileExp
if (!matcher)
throw new IllegalArgumentException("${file} " +
"is not a valid Issue file.")
// Read issue attributes from the filename.
super.id = matcher[0][1]
super.category = Category.toCategory(matcher[0][2])
super.status = Status.toStatus(matcher[0][3])
super.priority = matcher[0][4].toInteger()
log.debug("id: {}\tcategory: {}\tstatus: {}\tpriority: {}",
super.id, super.category, super.status, super.priority)
this.source = file
// Parse the file and extract the title, text, and extended properties
// TODO: guard against parsing problems (null/empty value stack, etc.)
def parsedIssue = parseRunner.run(file.text).valueStack.pop()
super.text = parsedIssue.body
super.title = parsedIssue.title
// Add the extended properties
parsedIssue.extProperties.each { key, value ->
key = key.toLowerCase().replaceAll(/\s/, '_')
super.extendedProperties[key] =
ExtendedPropertyHelp.parse(value) }
}
public void setCategory(Category c) throws IOException {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, c, status, priority))
if (source.renameTo(newSource)) {
source = newSource
super.setCategory(c) }
else { throw new IOException("I was unable to set the category. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public void setStatus(Status s) throws IOException {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, category, s, priority))
if (source.renameTo(newSource)) {
source = newSource
super.setStatus(s) }
else { throw new IOException("I was unable to set the status. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public void setPriority(int p) throws IOException {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, category, status, p))
if (source.renameTo(newSource)) {
source = newSource
super.setPriority(p) }
else { throw new IOException("I was unable to set the priority. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public String getFilename() {
return makeFilename(id, category, status, priority) }
public void setTitle(String title) throws IOException {
super.setTitle(title)
writeFile() }
public void setText(String text) throws IOException {
super.setText(text)
writeFile() }
public def propertyMissing(String name, def value) {
super.propertyMissing(name, value)
writeFile() }
boolean deleteFile() { return source.deleteDir() }
public static boolean isValidFilename(String name) {
return name ==~ fileExp }
public static String makeFilename(String id, Category category,
Status status, int priority) {
// bounds check priority
priority = Math.min(9, Math.max(0, priority))
//check for valid values of cateogry and id
if (category == null)
throw new IAE("Category must be non-null.")
if (status == null)
throw new IAE("Status must be non-null.")
if (!(id ==~ /\d+/))
throw new IAE( "'${id}' is not a legal value for id.")
return id + category.symbol + status.symbol + priority + ".rst" }
public static String formatIssue(Issue issue) {
def result = new StringBuilder()
result.append(issue.title)
result.append("\n")
result.append("=".multiply(issue.title.length()))
result.append("\n\n")
result.append(issue.text)
// If there are any extended properties, let's write those.
if (issue.extendedProperties.size() > 0) {
result.append("\n----\n\n")
def extOutput = [:]
def maxKeyLen = 0
def maxValLen = 0
// Find the longest key and value, convert all to strings.
issue.extendedProperties.each { key, val ->
def ks = key.toString().split('_').collect({it.capitalize()}).join(' ')
def vs = ExtendedPropertyHelp.format(val)
extOutput[ks] = vs
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
if (vs.length() > maxValLen) { maxValLen = vs.length() } }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n")
extOutput.sort().each { key, val ->
result.append(key.padRight(maxKeyLen))
result.append(": ")
result.append(val.padRight(maxValLen))
result.append("\n") }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n") }
return result.toString()}
protected void writeFile() {
try { source.write(formatIssue(this)) }
catch (IOException ioe) {
throw new IOException("I could not save the new text for this "
+ "issue. I can not write to the file for this issue. I do not"
+ " know why, I am sorry (maybe the file can not be reached).") } }
}

View File

@ -23,56 +23,65 @@ class FileProject extends Project {
child.isHidden()) return // just an issue folder child.isHidden()) return // just an issue folder
// otherwise build and add to list // otherwise build and add to list
projects[(child.name)] = new FileProject(child) projects[(child.name)] = new FileProject(child) }
} else if (child.isFile() && else if (child.isFile() &&
FileIssue.isValidFilename(child.name)) { FileIssue.isValidFilename(child.name)) {
def issue def issue
// if exception, then not an issue // if exception, then not an issue
try { issue = new FileIssue(child) } catch (all) { return } try { issue = new FileIssue(child) } catch (all) { return }
issues[(issue.id)] = issue issues[(issue.id)] = issue } }}
}
}
}
public void setName(String name) { public void setName(String name) {
super.setName(name) super.setName(name)
source.renameTo(new File(source.canonicalFile.parentFile, name)) source.renameTo(new File(source.canonicalFile.parentFile, name)) }
}
public FileIssue createNewIssue(Map options) { public FileIssue createNewIssue(Map options) {
Issue issue
File issueFile
if (!options) options = [:] if (!options) options = [:]
if (!options.category) options.category = Category.TASK
if (!options.status) options.status = Status.NEW // We want some different defaults for issues due to the parser being
if (!options.priority) options.priority = 5 // unable to handle empty title or text.
if (!options.text) options.text = "Default issue title.\n" + if (!options.title) options.title = "Default issue title."
"====================\n" if (!options.text) options.text = "Describe the issue here."
String id
if (issues.size() == 0) id = '0000' // We are also going to find the next id based on the issues already in the
// project.
if (issues.size() == 0) options.id = '0000'
else { else {
id = (issues.values().max { it.id.toInteger() }).id def lastId = (issues.values().max { it.id.toInteger() }).id
id = (id.toInteger() + 1).toString().padLeft(id.length(), '0') options.id = (lastId.toInteger() + 1).toString().padLeft(
} lastId.length(), '0') }
def issueFile = new File(source, FileIssue.makeFilename(id, // Create an Issue object from the options (we will discard it later).
options.category, options.status, options.priority)) issue = new Issue(options)
// Create the filename and File object based on the options given.
issueFile = new File(source, FileIssue.makeFilename(
issue.id, issue.category, issue.status, issue.priority))
// Create the actual file on the system
issueFile.createNewFile() issueFile.createNewFile()
issueFile.write(options.text)
def issue = new FileIssue(issueFile) // Write the issue to the file created.
issueFile.write(FileIssue.formatIssue(issue))
// Read that new file back in as a FileIssue
issue = new FileIssue(issueFile)
// Add the issue to our collection.
issues[(issue.id)] = issue issues[(issue.id)] = issue
return issue return issue }
}
public FileProject createNewProject(String name) { public FileProject createNewProject(String name) {
def newDir = new File(source, name) def newDir = new File(source, name)
newDir.mkdirs() newDir.mkdirs()
return new FileProject(newDir) return new FileProject(newDir) }
}
public boolean deleteIssue(Issue issue) { public boolean deleteIssue(Issue issue) {
if (!issues[(issue.id)]) return false if (!issues[(issue.id)]) return false
@ -81,8 +90,7 @@ class FileProject extends Project {
if (issue instanceof FileIssue) if (issue instanceof FileIssue)
return issue.deleteFile() return issue.deleteFile()
else return true else return true }
}
public boolean deleteProject(Project project) { public boolean deleteProject(Project project) {
if (!projects[(project.name)]) return false if (!projects[(project.name)]) return false
@ -91,8 +99,7 @@ class FileProject extends Project {
if (project instanceof FileProject) if (project instanceof FileProject)
return project.source.delete() return project.source.delete()
return true return true }
}
@Override @Override
public String toString() { return name } public String toString() { return name }

View File

@ -0,0 +1,67 @@
package com.jdbernard.pit.file;
import java.util.HashMap;
import java.util.Map;
import org.parboiled.Action;
import org.parboiled.BaseParser;
import org.parboiled.Context;
import org.parboiled.Rule;
import org.parboiled.annotations.*;
@BuildParseTree
public class IssuePegParser extends BaseParser<Object> {
public Rule IssueFile() {
return Sequence(push(makeNode()),
Title(), Body(), Optional(PropertyBlock())); }
Rule Title() {
return Sequence(
OneOrMore(NOT_EOL), addToNode("title", match()), EOL,
HorizontalRule(), EOL,
ZeroOrMore(SPACE), EOL); }
Rule Body() { return Sequence(OneOrMore(Sequence(
TestNot(PropertyBlock()), ANY)), addToNode("body", match())); }
Rule PropertyBlock() {
return Sequence(push(makeNode()),
EOL, HorizontalRule(), OneOrMore(EOL), TableSeparator(),
EOL, OneOrMore(PropertyDefinition()), TableSeparator(),
addToNode("extProperties", pop())); }
Rule PropertyDefinition() {
return Sequence(
PropertyKey(), push(match()), COLON,
PropertyValue(), push(match()), EOL,
swap(), addToNode(popAsString().trim(), popAsString().trim())); }
Rule PropertyKey() { return OneOrMore(Sequence(TestNot(COLON), NOT_EOL)); }
Rule PropertyValue() { return OneOrMore(NOT_EOL); }
Rule TableSeparator() {
return Sequence(OneOrMore(SEPARATOR_CHAR), OneOrMore(SPACE),
OneOrMore(SEPARATOR_CHAR)); }
Rule HorizontalRule() {
return Sequence(SEPARATOR_CHAR, SEPARATOR_CHAR, SEPARATOR_CHAR,
OneOrMore(SEPARATOR_CHAR)); }
Rule EOL = Ch('\n');
Rule NOT_EOL = Sequence(TestNot(EOL), ANY);
Rule SEPARATOR_CHAR = AnyOf("\"'`~:-_=+*^#<>");
Rule SPACE = AnyOf(" \t");
Rule COLON = Ch(':');
Map makeNode() { return new HashMap(); }
boolean addToNode(Object key, Object val) {
Map node = (Map) pop();
node.put(key, val);
push(node);
return true; }
String popAsString() { return (String) pop(); }
}

View File

@ -18,7 +18,8 @@ public class XmlIssue extends Issue {
} }
XmlIssue(String id, Category c = Category.TASK, Status s = Status.NEW, XmlIssue(String id, Category c = Category.TASK, Status s = Status.NEW,
int p = 9, String text, XmlRepository repository, XmlProject project) { int p = 9, String title, String text, XmlRepository repository,
XmlProject project) {
super(id, c, s, p) super(id, c, s, p)
this.project = project this.project = project
@ -26,9 +27,10 @@ public class XmlIssue extends Issue {
// Node constructor adds the node to the parent node // Node constructor adds the node to the parent node
issueNode = new Node(project.projectNode, "Issue", issueNode = new Node(project.projectNode, "Issue",
[id: id, category: c, status: s, priority: p]) [id: id, category: c, status: s, priority: p, title: title])
this.text = text super.@title = title
super.@text = text
issueNode.value = text issueNode.value = text
repository.persist() repository.persist()
@ -62,4 +64,11 @@ public class XmlIssue extends Issue {
repository.persist() repository.persist()
} }
public void setTitle(String t) {
super.setTitle(t)
issueNode.@title = t
repository.persist()
}
} }

View File

@ -2,7 +2,7 @@ package com.jdbernard.pit
public class MockIssue extends Issue { public class MockIssue extends Issue {
public MockIssue(String id, Category c, Status s, int p) { public MockIssue(String id, Category c, Status s, int p) {
super (id, c, s, p) super ([id: id, category: c, status: s, priority: p])
} }
public boolean delete() { return true } public boolean delete() { return true }
} }

View File

@ -171,9 +171,7 @@ class FileIssueTest {
assertEquals issue.status , Status.NEW assertEquals issue.status , Status.NEW
assertEquals issue.priority , 1 assertEquals issue.priority , 1
assertEquals issue.title , "Add the killer feature to the killer app." assertEquals issue.title , "Add the killer feature to the killer app."
assertEquals issue.text , "Add the killer feature to the killer app.\n" + assertEquals issue.text , "Make our killer app shine!."
"=========================================\n\n" +
"Make our killer app shine!."
assertEquals issue.source , issueFile assertEquals issue.source , issueFile
} }

3
pit-cli/.gitignore vendored
View File

@ -1,3 +0,0 @@
release/
build/
*.sw?

View File

@ -1,40 +1,8 @@
<project name="Personal Issue Tracker CLI"> <project name="Personal Issue Tracker CLI">
<property file="../version.properties"/>
<property file="project.properties"/> <property file="project.properties"/>
<property environment="env" />
<path id="groovy.libs"> <import file="../jdb-build-1.6.xml"/>
<fileset dir="${env.GROOVY_HOME}/lib">
<include name="**/*.jar"/>
</fileset>
</path>
<path id="groovy.embeddable">
<fileset dir="${env.GROOVY_HOME}/embeddable">
<include name="**/*.jar"/>
</fileset>
</path>
<path id="project.libs">
<fileset dir="${lib.dir}">
<include name="**/*.jar"/>
</fileset>
</path>
<path id="groovyc.classpath">
<path refid="groovy.libs"/>
<path refid="project.libs"/>
</path>
<path id="package.jars">
<path refid="groovy.embeddable"/>
<path refid="project.libs"/>
</path>
<taskdef name="groovyc"
classname="org.codehaus.groovy.ant.Groovyc"
classpathref="groovy.libs"/>
<target name="init"> <target name="init">
<fail <fail
@ -42,77 +10,32 @@
message="GROOVY_HOME environment variable is not set."/> message="GROOVY_HOME environment variable is not set."/>
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/> <echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
<fail message="Could not find PIT ${application.version} library."> <fail message="Could not find PIT ${version} library.">
<condition> <condition>
<not> <not>
<available <available
file="${lib.dir}/pit-${application.version}.jar"/> file="${basedir}/../libpit/release/libpit-${version}.jar"/>
</not> </not>
</condition> </condition>
</fail> </fail>
<echo message="PIT library found at ${lib.dir}/pit-${application.version}.jar"/>
<fail message="The PIT project is at version ${application.version} but pit-cli is versioned as ${expected.application.version}. Ensure that pit-cli is updated tp reflect the changes in libpit and then run the 'upgrade-version' task to sync the pit-vli subproject with the PIT project.">
<condition>
<not>
<equals
arg1="${application.version}"
arg2="${expected.application.version}"/>
</not>
</condition>
</fail>
<echo message="Application version: ${application.version}"/>
</target> </target>
<target name="upgrade-version"> <target name="clean-all" depends="clean">
<propertyfile file="project.properties"> <delete dir="${release.dir}"/>
<entry
key="expected.application.version"
value="${application.version}"/>
<entry key="build.number" value="0"/>
</propertyfile>
<echo message="pit-cli version upgraded to ${application.version}"/>
</target> </target>
<target name="increment-build-number" depends="init"> <target name="lib">
<propertyfile file="project.properties"> <copy todir="${build.dir}/lib/compile/jar"
<entry key="build.number" operation="+" type="int" default="0"/> file="${basedir}/../libpit/release/libpit-${version}.jar"/>
</propertyfile> <copy todir="${build.dir}/lib/runtime/jar"
</target> file="${basedir}/../libpit/release/libpit-${version}.jar"/>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
<target name="compile" depends="init,increment-build-number">
<mkdir dir="${build.dir}/classes"/>
<groovyc
srcdir="${src.dir}"
destdir="${build.dir}/classes"
classpathref="groovyc.classpath"/>
</target>
<target name="build" depends="compile">
<mkdir dir="${build.dir}/jar"/>
<unjar dest="${build.dir}/classes">
<path refid="package.jars"/>
</unjar>
<jar
destfile="${build.dir}/jar/${build.jar}"
basedir="${build.dir}/classes"
compress="on">
<manifest>
<attribute name="Main-Class" value="${main.class}"/>
</manifest>
</jar>
</target> </target>
<target name="release" depends="build"> <target name="release" depends="build">
<delete dir="${release.dir}"/> <mkdir dir="${release.dir}/lib"/>
<mkdir dir="${release.dir}"/> <copy todir="${release.dir}/lib">
<copy file="${build.dir}/jar/${build.jar}" <fileset dir="${build.dir}/lib/runtime/jar"/></copy>
tofile="${release.dir}/${release.jar}"/> <copy tofile="${release.dir}/${name}-${version}.jar"
file="${build.dir}/${name}-${version}.${build.number}.jar"/>
</target> </target>
</project> </project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,10 +1,12 @@
#Wed, 26 Oct 2011 15:01:22 -0500 #Thu, 30 Aug 2012 06:25:30 -0700
build.dir=build build.dir=build
src.dir=src src.dir=src
build.jar=pit-cli-${application.version}.${build.number}.jar build.jar=pit-cli-${application.version}.${build.number}.jar
build.number=1 build.number=5
expected.application.version=2.6.1 version=3.3.3
name=pit-cli
lib.dir=lib lib.dir=lib
lib.local=true
release.dir=release release.dir=release
release.jar=pit-cli-${application.version}.jar release.jar=pit-cli-${application.version}.jar
main.class=com.jdbernard.pit.PersonalIssueTrackerCLI main.class=com.jdbernard.pit.PersonalIssueTrackerCLI

View File

@ -1,253 +0,0 @@
package com.jdbernard.pit
import com.jdbernard.pit.file.*
import static java.lang.Math.max
import static java.lang.Math.min
// -------- command-line interface specification -------- //
def cli = new CliBuilder(usage: 'pit-cli [options]')
cli.h(longOpt: 'help', 'Show help information.')
cli.v(longOpt: 'verbose', 'Show verbose task information')
cli.l(longOpt: 'list', 'List issues. Unless otherwise specified it lists all '
+ 'sub projects and all unclosed issue categories.')
cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.')
cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task). Accepts a '
+ 'comma-delimited list. By default all categories are selected.')
cli.s(argName: 'status', longOpt: 'status', args: 1,
'Filter issues by status (new, reassigned, rejected, resolved, ' +
'validation_required)')
cli.p(argName: 'priority', longOpt: 'priority', args: 1,
'Filter issues by priority. This acts as a threshhold, listing all issues '
+ 'greater than or equal to the given priority.')
cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.')
/*cli.s(longOpt: 'show-subprojects',
'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.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
required: false, 'Modify the priority of the selected issues.')
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
required: false, 'Modify the category of the selected issues.')
cli.S(argName: 'new-status', longOpt: 'set-status', args: 1,
required: false, 'Modify the status of the selected issues.')
cli.n(longOpt: 'new-issue', 'Create a new issue.')
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
'Use <dir> as the base directory (defaults to current directory).')
cli._(longOpt: 'version', 'Display PIT version information.')
// -------- parse CLI options -------- //
def VERSION = "2.6.1"
def opts = cli.parse(args)
def issuedb = [:]
def workingDir = new File('.')
// defaults for the issue filter/selector
def selectOpts = [
categories: ['bug', 'feature', 'task'],
status: ['new', 'reassigned', 'rejected',
'resolved', 'validation_required'],
priority: 9,
projects: [],
ids: [],
acceptProjects: true]
// defaults for changing properties of issue(s)
def assignOpts = [
category: Category.TASK,
status: Status.NEW,
priority: 5,
text: "New issue."]
if (!opts) opts.l = true; // default to 'list'
if (opts.h) {
cli.usage()
System.exit(0) }
// read the category filter designation(s)
if (opts.c) {
if (opts.c =~ /all/) {} // no-op, same as defaults
else { selectOpts.categories = opts.c.split(/[,\s]/) } }
// parse the categories names into Category objects
try { selectOpts.categories =
selectOpts.categories.collect { Category.toCategory(it) } }
catch (Exception e) {
println "Invalid category option: '-c ${e.localizedMessage}'."
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read the status filter designation(s)
if (opts.s) {
// -s all
if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected',
'resolved', 'validation_required']
// is <list>
else selectOpts.status = opts.s.split(/[,\s]/) }
// parse the statuses into Status objects
try { selectOpts.status =
selectOpts.status.collect { Status.toStatus(it) } }
catch (Exception e) {
println "Invalid status option: '-s ${e.localizedMessage}'."
println "Valid options are: \b${Status.values().join(', ')}"
println " (abbreviations are accepted.)"
System.exit(1) }
// read and parse the priority filter
if (opts.p) try {
selectOpts.priority = opts.p.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-p ${opts.p}'."
println "Valid values are: 0-9"
System.exit(1) }
// read and parse the projects filter
if (opts.r) { selectOpts.projects =
opts.r.toLowerCase().split(/[,\s]/).asType(List.class) }
// read and parse the ids filter
if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) }
// TODO: accept projects value from input
// read and parse the category to assign
if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) }
catch (Exception e) {
println "Invalid category option: '-C ${e.localizedMessage}'."
println "Valid categories are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the status to assign
if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) }
catch (Exception e) {
println "Invalid status option: '-S ${e.localizedMessage}'."
println "Valid stasus options are: \n{Status.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the priority to assign
if (opts.P) try {assignOpts.priority = opts.P.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-P ${opts.P}'."
println "Valid values are: 0-9"
System.exit(1) }
// look for assignment text
if (opts.getArgs().length > 0) {
assignOpts.text = opts.getArgs()[0] }
// set the project working directory
if (opts.d) {
workingDir = new File(opts.d.trim())
if (!workingDir.exists()) {
println "Directory '${workingDir}' does not exist."
return -1 } }
def EOL = System.getProperty('line.separator')
// build issue list
issuedb = new FileProject(workingDir)
// build filter from options
def filter = new Filter(selectOpts)
// -------- Actions -------- //
// list version information first
if (opts.version) {
println "PIT CLI Version ${VERSION}"
println "Written by Jonathan Bernard\n" }
// list second
else if (opts.l) {
// local function (closure) to print a single issue
def printIssue = { issue, offset ->
println "${offset}${issue}"
if (opts.v) {
println ""
issue.text.eachLine { println "${offset} ${it}" }
println "" } }
// local function (closure) to print a project and all visible subprojects
def printProject
printProject = { project, offset ->
println "\n${offset}${project.name}"
println "${offset}${'-'.multiply(project.name.length())}"
project.eachIssue(filter) { printIssue(it, offset) }
project.eachProject(filter) { printProject(it, offset + " ") } }
// print all the issues in the root of this db
issuedb.eachIssue(filter) { printIssue(it, "") }
// print all projects
issuedb.eachProject(filter) { printProject(it, "") } }
// new issues third
else if (opts.n) {
def cat, priority
String text = ""
Issue issue
def sin = System.in.newReader()
if (opts.C) { cat = assignOpts.category }
else while(true) {
try {
print "Category (bug, feature, task): "
cat = Category.toCategory(sin.readLine())
break }
catch (e) {
println "Invalid category: " + e.getLocalizedMessage()
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)." } }
if (opts.P) { priority = assignOpts.priority }
else while (true) {
try {
print "Priority (0-9): "
priority = max(0, min(9, sin.readLine().toInteger()))
break }
catch (e) { println "Not a valid value." } }
if (opts.getArgs().length > 0) { text = assignOpts.text }
else {
println "Enter issue (use EOF): "
try {
def line = ""
while(true) {
line = sin.readLine()
if (line =~ /EOF/) break
text += line + EOL
} }
catch (e) {} }
issue = issuedb.createNewIssue(category: cat, priority: priority, text: text)
println "New issue created: "
println issue }
// last, changes to existing issues
else {
// change priority
if (opts.P) issuedb.walkProject(filter) {
it.priority = assignOpts.priority
println "[${it}] -- set priority to ${assignOpts.priority}"}
// change third
else if (opts.C) issuedb.walkProject(filter) {
it.category = assignOpts.cat
println "[${it}] -- set category to ${assignOpts.category}"}
// change status
else if (opts.S) issuedb.walkProject(filter) {
it.status = assignOpts.status
println "[${it}] -- set status to ${assignOpts.status}"}
}

View File

@ -0,0 +1,631 @@
/**
* # 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
import com.jdbernard.pit.file.*
import org.joda.time.DateMidnight
import org.joda.time.DateTime
import org.slf4j.LoggerFactory
import static java.lang.Math.max
import static java.lang.Math.min
def log = LoggerFactory.getLogger(getClass())
/// ## Command Line Options ##
/// --------------------------
/// @org cli-options
def cli = new CliBuilder(usage: 'pit-cli [options]')
/// -h,--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')
/// -l,--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,
'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,
'Filter issues by category (bug, feature, task). Accepts a '
+ '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,
'Filter issues by status (new, reassigned, rejected, resolved, ' +
'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,
'Filter issues by priority. This acts as a threshhold, listing all issues '
+ '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,
'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.')
/// -R,--recursive
/// : Recursively 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 ' +
'property. Format is "-e <propname>=<propvalue>".')
/*cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)')
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,
'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,
'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,
'Modify the status of the selected issues.')
/// -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.')
/// --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' +
' issue or modify the title for an existing issue. By default the title' +
' 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' +
' issue or modify the text for an exising issue. By default the text for' +
' 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,
'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.')
/// -d,--dir
/// : Use `<dir>` as the base directory (defaults to current directory).
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
'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' +
' 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' +
' 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' +
' default).')
/// --dl-upcoming
/// : Show upcoming tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-upcoming', 'Show upcoming tasks in the daily list (all ' +
' 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 ' +
' 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' +
' (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' +
' shown by default).')
/// --dl-hide-upcoming
/// : Show upcoming tasks in the daily list (all are shown by default).
cli._(longOpt: 'dl-hide-upcoming', 'Show upcoming tasks in the daily list' +
' (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' +
' shown by default).')
/// --dl-upcoming-days
/// : The upcoming tasks section in the daily list includes any tasks due
/// within the next seven days by default. This option overrides that
/// default and allows you to specify the number of days ahead the upcoming
/// section looks.
cli._(longOpt: 'dl-upcoming-days', argName: 'num-days', args:1, required: false,
'The upcoming tasks section in the daily list includes any tasks due ' +
'within the next seven days by default. This option overrides that ' +
'default and allows you to specify the number of days ahead the upcoming ' +
'section looks.')
/// --version
/// : Display PIT version information.
cli._(longOpt: 'version', 'Display PIT version information.')
/// ## Parse CLI Options ##
/// -----------------------
log.trace("Parsing options.")
def VERSION = "3.3.3"
def opts = cli.parse(args)
def issuedb = [:]
def workingDir = new File('.')
/// Defaults for the issue filter/selector.
def selectOpts = [
categories: ['bug', 'feature', 'task'],
status: ['new', 'reassigned', 'rejected',
'resolved', 'validation_required'],
priority: 9,
projects: [],
ids: [],
extendedProperties: [:],
acceptProjects: true]
/// Defaults for changing properties of issue(s)
def assignOpts = [:]
if (!opts || opts.h) {
cli.usage()
System.exit(0) }
///Read the `-c` option: category filter designation(s).
if (opts.c) {
if (opts.c =~ /all/) {} // no-op, same as defaults
else { selectOpts.categories = opts.c.split(/[,\s]/) } }
/// Parse the categories names into Category objects.
try { selectOpts.categories =
selectOpts.categories.collect { Category.toCategory(it) } }
catch (Exception e) {
println "Invalid category option: '-c ${e.localizedMessage}'."
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
/// Read the `-s` option: status filter designation(s).
if (opts.s) {
// -s all
if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected',
'resolved', 'validation_required']
// <list>
else selectOpts.status = opts.s.split(/[,\s]/) }
/// Parse the statuses into Status objects.
try { selectOpts.status =
selectOpts.status.collect { Status.toStatus(it) } }
catch (Exception e) {
println "Invalid status option: '-s ${e.localizedMessage}'."
print "Valid options are: \n${Status.values().join(', ')}"
println " (abbreviations are accepted.)"
System.exit(1) }
/// Read and parse the `-p` option: priority filter.
if (opts.p) try {
selectOpts.priority = opts.p.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-p ${opts.p}'."
println "Valid values are: 0-9"
System.exit(1) }
/// Read and parse the `-r` option: projects filter.
if (opts.r) { selectOpts.projects =
opts.r.toLowerCase().split(/[,\s]/).asType(List.class) }
/// Read and parse the `-i` option: id filter.
if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) }
/// Read and parse the `-o` option: sort criteria.
if (opts.o) {
def sortProps = opts.o.split(',')
selectOpts.issueSorter = sortProps.collect { prop ->
switch (prop) {
case ~/^i$/: return { issue -> issue.id }
case ~/^p$/: return { issue -> issue.priority }
case ~/^s$/: return { issue -> issue.status }
case ~/^c$/: return { issue -> issue.category }
default: return { issue -> issue[prop] } }}}
/// Read and parse any extended property selection criteria.
if (opts.e) {
opts.es.each { option ->
def parts = option.split("=")
selectOpts.extendedProperties[parts[0]] =
ExtendedPropertyHelp.parse(parts[1]) }}
/// Read and parse the `-C` option: category to assign.
if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) }
catch (Exception e) {
println "Invalid category option: '-C ${e.localizedMessage}'."
println "Valid categories are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
/// Read and parse the `-S` option: status to assign.
if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) }
catch (Exception e) {
println "Invalid status option: '-S ${e.localizedMessage}'."
println "Valid stasus options are: \n${Status.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
/// Read and parse the `-P` option: priority to assign.
if (opts.P) try {assignOpts.priority = opts.P.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-P ${opts.P}'."
println "Valid values are: 0-9"
System.exit(1) }
/// Read an parse any extended properties to be set.
if (opts.E) {
opts.Es.each { option ->
def parts = option.split("=")
assignOpts[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }}
/// Read the title if given.
if (opts.title) { assignOpts.title = opts.title }
/// Read the text if given.
if (opts.text) { assignOpts.text = opts.text }
/// Set the project working directory.
if (opts.d) {
workingDir = new File(opts.d.trim())
if (!workingDir.exists()) {
println "Directory '${workingDir}' does not exist."
return -1 } }
def EOL = System.getProperty('line.separator')
log.debug("Finished parsing options:\nworkingDir: {}\nselectOpts: {}\nassignOpts: {}",
workingDir.canonicalPath, selectOpts, assignOpts)
/// ## Actions ##
/// -------------
/// ### Version information.
if (opts.version) {
println "PIT CLI Version ${VERSION}"
println "Written by Jonathan Bernard\n" }
/// ----
else {
/// Build issue list.
log.trace("Building issue database.")
issuedb = new FileProject(workingDir)
/// Build filter from options.
log.trace("Defining the filter.")
def filter = new Filter(selectOpts)
/// ### List
if (opts.l) {
log.trace("Listing issues.")
/// Local function (closure) to print a single issue.
def printIssue = { issue, offset ->
println "${offset}${issue}"
if (opts.v) {
println ""
issue.text.eachLine { println "${offset} ${it}" }
println ""
issue.extendedProperties.each { name, value ->
def formattedValue = ExtendedPropertyHelp.format(value)
println "${offset} * ${name}: ${formattedValue}"}
println ""}}
/// Local function (closure) to print a project and all visible subprojects.
def printProject
printProject = { project, offset ->
println "\n${offset}${project.name}"
println "${offset}${'-'.multiply(project.name.length())}"
project.eachIssue(filter) { printIssue(it, offset) }
project.eachProject(filter) { printProject(it, offset + " ") } }
/// Print all the issues in the root of this db.
issuedb.eachIssue(filter) { printIssue(it, "") }
/// If the user set the recursive flag print all projects.
if (opts.R) {
issuedb.eachProject(filter) { printProject(it, "") }} }
/// ### Daily List
else if (opts.D) {
log.trace("Showing a daily list.")
/// Set up our time intervals.
def today = new DateMidnight()
def tomorrow = today.plusDays(1)
/// #### Parse daily list specific display options.
def visibleSections = []
def suppressedSections
def upcomingCutoff = today.plusDays(7)
/// Check for a custom upcoming section cutoff date.
if (opts.'dl-upcoming-days') {
int numDays = opts.'dl-upcoming-days' as int
upcomingCutoff = today.plusDays(numDays) }
/// Parse the additive options first.
if (opts.'dl-scheduled') { visibleSections << 'scheduled' }
if (opts.'dl-due') { visibleSections << 'due' }
if (opts.'dl-upcoming') { visibleSections << 'upcoming' }
if (opts.'dl-open') { visibleSections << 'open' }
/// If the user did not add any sections assume they want them all.
if (visibleSections.size() == 0) {
visibleSections = ['scheduled', 'due', 'upcoming', 'open'] }
/// Now go through the negative options.
if (opts.'dl-hide-scheduled') { visibleSections -= 'scheduled' }
if (opts.'dl-hide-due') { visibleSections -= 'due' }
if (opts.'dl-hide-upcoming') { visibleSections -= 'upcoming' }
if (opts.'dl-hide-open') { visibleSections -= 'open' }
/// If the user did not specifically ask for a status filter, we want a
/// different default filter when we are doing a daily list.
if (!opts.s) { filter.status = [Status.NEW, Status.VALIDATION_REQUIRED] }
/// 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} ] }
/// #### Get all the issues involved.
def allIssues = opts.R ?
/// If `-R` passed, get all issues, including subprojects.
issuedb.getAllIssues(filter) :
/// Otherwise, just use the issues for this project.
issuedb.issues.values().findAll { filter ? filter.accept(it) : true }
/// We are going to sort the issues into these buckets based on when they are
/// scheduled, when they are due and if they have a reminder set.
def scheduledToday = []
def dueToday = []
def upcoming = []
def notDueOrReminder = []
/// Helper closure to print an issue.
def printIssue = { issue ->
if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}"
else println " -- ${issue}" }
/// A sorter which sorts by date first, then by priority.
def priorityDateSorter = { i1, i2 ->
if (i1.priority == i2.priority) {
def d1 = i1.due ?: new DateTime()
def d2 = i2.due ?: new DateTime()
return d1.compareTo(d2) }
else { return i1.priority - i2.priority }}
/// #### Categorize and sort the issues.
/// Sort the issues into seperate lists based on their due dates and
/// reminders.
allIssues.each { issue ->
/// * Find the issues that are scheduled for today.
if (issue.scheduled && issue.scheduled < tomorrow) {
scheduledToday << issue }
/// * Find the issues that are due today or are past due.
else if (issue.due && issue.due < tomorrow) { dueToday << issue }
/// * Find the issues that are not yet due but have a reminder for
/// today or days past as well as issues that are due before the
/// `upcomingCutoff` date.
else if ((issue.reminder && issue.reminder < tomorrow) ||
(issue.due && issue.due < upcomingCutoff)) {
upcoming << issue }
/// * All the others (not due and no reminder).
else notDueOrReminder << issue }
/// #### Print the issues
if (visibleSections.contains('scheduled') && scheduledToday.size() > 0) {
println "Tasks Scheduled for Today"
println "-------------------------"
scheduledToday.sort(priorityDateSorter).each { printIssue(it) }
println "" }
if (visibleSections.contains('due') && dueToday.size() > 0) {
println "Tasks Due Today"
println "---------------"
dueToday.sort(priorityDateSorter).each { printIssue(it) }
println ""}
if (visibleSections.contains('upcoming') && upcoming.size() > 0) {
println "Upcoming Tasks"
println "--------------"
upcoming.sort(priorityDateSorter).each { printIssue(it) }
println ""}
if (visibleSections.contains('open') && notDueOrReminder.size() > 0) {
println "Other Open Issues"
println "-----------------"
notDueOrReminder.sort(priorityDateSorter).each { printIssue(it) }
println "" }}
/// ### Create a New Issue.
else if (opts.n) {
log.trace("Creating a new issue.")
Issue issue
def sin = System.in.newReader()
/// Set the created extended property.
assignOpts.created = new DateTime()
/// 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:
/// 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 an
/// exception before the assignment happens, the variable will still be
/// null, and we will prompt the user again.
/// Prompt for category.
while(!assignOpts.category) {
try {
print "Category (bug, feature, task): "
assignOpts.category = Category.toCategory(sin.readLine())
break }
catch (e) {
println "Invalid category: " + e.getLocalizedMessage()
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)." } }
/// Prompt for the priority.
while (!assignOpts.priority) {
try {
print "Priority (0-9): "
assignOpts.priority = max(0, min(9, sin.readLine().toInteger()))
break }
catch (e) { println "Not a valid value." } }
/// Prompt for the issue title. No need to loop as the input does not need
/// to be validated.
if (!assignOpts.title) {
println "Issue title: "
assignOpts.title = sin.readLine().trim() }
/// Prompt for the issue text.
if (!assignOpts.text) {
assignOpts.text = ""
println "Enter issue text (use EOF to stop): "
try {
def line = ""
while(true) {
line = sin.readLine()
// Stop when they enter EOF
if (line ==~ /^EOF$/) break
assignOpts.text += line + EOL } }
catch (e) {} }
/// Create the issue.
issue = issuedb.createNewIssue(assignOpts)
println "New issue created: "
println issue }
/// ### Change Existing Issues.
else if (assignOpts.size() > 0) {
log.trace("Changing existing issues.")
/// We are going to add some extra properties if the status is being changed,
/// because we are nice like that.
if (assignOpts.status) { switch (assignOpts.status) {
case Status.RESOLVED: assignOpts.resolved = new DateTime(); break
case Status.REJECTED: assignOpts.rejected = new DateTime(); break
default: break }}
/// #### processIssue
/// A local function to handle the changes for one issue.
def processIssue = { issue ->
println issue
/// Walk the assigned options map and set the properties on the issue.
assignOpts.each { propName, value ->
issue[propName] = value
def formattedValue = ExtendedPropertyHelp.format(value)
println " set ${propName} to ${formattedValue}" } }
/// If the user passed `-R`, walk the whole project, including subprojects.
if (opts.R) { issuedb.walkProject(filter, processIssue) }
/// Otherwise, just process the issues in this project.
else {
issuedb.issues.values()
.findAll { filter ? filter.accept(it) : true }
.each(processIssue) }}
/// ### Invalid Input
else {
log.trace("Unknown request.")
cli.usage(); return -1 }}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
application.version=2.6.1 application.version=3.3.2