Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
d0e968b2b7 | |||
c0b02ca222 | |||
85753de955 | |||
0441f3c510 | |||
c01eaa0255 | |||
ec7c07f81f | |||
952064d903 | |||
31b9802477 | |||
ae0d782a5b | |||
846d1edc74 | |||
fd94f0e41a | |||
6f58a83ad4 | |||
47cf3cf0a4 | |||
e00e2e296e | |||
c26ba17dbb | |||
447e74f956 | |||
66b68160e5 | |||
f86316c68f | |||
5ff4665a07 | |||
faacfd859a |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
release/
|
||||
*.sw*
|
||||
*/build/
|
||||
|
12
build.xml
12
build.xml
@ -9,7 +9,7 @@
|
||||
<target name="clean">
|
||||
<ant dir="libpit" 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 name="libpit">
|
||||
@ -21,15 +21,17 @@
|
||||
<ant dir="pit-cli" target="release" inheritAll="false"/>
|
||||
</target>
|
||||
|
||||
<target name="pit-swing" depends="libpit">
|
||||
<!-- <target name="pit-swing" depends="libpit">
|
||||
<copy file="${libpit.jar}" todir="pit-swing/lib"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
</target>
|
||||
</project>
|
||||
|
13
issues/libpit/0000bs5.rst
Normal file
13
issues/libpit/0000bs5.rst
Normal 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
203
jdb-build-1.6.xml
Normal 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="-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>
|
106
libpit/build.xml
106
libpit/build.xml
@ -1,30 +1,8 @@
|
||||
<project name="Personal Issue Tracker" default="release">
|
||||
<property file="../version.properties"/>
|
||||
|
||||
<property file="project.properties"/>
|
||||
<property environment="env"/>
|
||||
|
||||
<path id="groovy.libs">
|
||||
<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"/>
|
||||
<import file="../jdb-build-1.6.xml"/>
|
||||
|
||||
<target name="init">
|
||||
<fail
|
||||
@ -33,79 +11,17 @@
|
||||
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
|
||||
</target>
|
||||
|
||||
<target name="increment-build-number" depends="init">
|
||||
<!-- Check to see if the application version has changed.
|
||||
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 name="clean-all" depends="clean">
|
||||
<delete dir="${release.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="release" depends="build">
|
||||
<delete dir="${release.dir}"/>
|
||||
<mkdir dir="${release.dir}"/>
|
||||
<copy file="${build.dir}/jar/pit-${application.version}.${build.number.final}.jar"
|
||||
tofile="${release.dir}/${release.jar}"/>
|
||||
<mkdir dir="${release.dir}/lib"/>
|
||||
<copy file="${build.dir}/${name}-${version}.${build.number}.jar"
|
||||
tofile="${release.dir}/${name}-${version}.jar"/>
|
||||
<copy todir="${release.dir}/lib">
|
||||
<fileset dir="${build.dir}/lib/runtime/jar"/>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
|
7
libpit/doc/grammar.txt
Normal file
7
libpit/doc/grammar.txt
Normal 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
|
BIN
libpit/lib/compile/jar/joda-time-2.0.jar
Normal file
BIN
libpit/lib/compile/jar/joda-time-2.0.jar
Normal file
Binary file not shown.
BIN
libpit/lib/compile/jar/parboiled-core-1.0.2.jar
Normal file
BIN
libpit/lib/compile/jar/parboiled-core-1.0.2.jar
Normal file
Binary file not shown.
BIN
libpit/lib/compile/jar/parboiled-java-1.0.2.jar
Normal file
BIN
libpit/lib/compile/jar/parboiled-java-1.0.2.jar
Normal file
Binary file not shown.
BIN
libpit/lib/compile/jar/slf4j-api-1.6.1.jar
Normal file
BIN
libpit/lib/compile/jar/slf4j-api-1.6.1.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/joda-time-2.0.jar
Normal file
BIN
libpit/lib/runtime/jar/joda-time-2.0.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/logback-classic-0.9.26.jar
Normal file
BIN
libpit/lib/runtime/jar/logback-classic-0.9.26.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/logback-core-0.9.26.jar
Normal file
BIN
libpit/lib/runtime/jar/logback-core-0.9.26.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/parboiled-core-1.0.2.jar
Normal file
BIN
libpit/lib/runtime/jar/parboiled-core-1.0.2.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/parboiled-java-1.0.2.jar
Normal file
BIN
libpit/lib/runtime/jar/parboiled-java-1.0.2.jar
Normal file
Binary file not shown.
BIN
libpit/lib/runtime/jar/slf4j-api-1.6.1.jar
Normal file
BIN
libpit/lib/runtime/jar/slf4j-api-1.6.1.jar
Normal file
Binary file not shown.
@ -1,11 +1,13 @@
|
||||
#Fri, 21 Oct 2011 16:18:33 -0500
|
||||
#Mon, 13 Feb 2012 10:13:41 -0600
|
||||
#Sat Apr 24 17:08:00 CDT 2010
|
||||
build.dir=build
|
||||
src.dir=src
|
||||
lib.shared.dir=../shared-libs
|
||||
test.dir=test
|
||||
build.number=11
|
||||
expected.application.version=2.6.0
|
||||
build.number=1
|
||||
version=3.3.2
|
||||
name=libpit
|
||||
lib.dir=lib
|
||||
lib.local=true
|
||||
release.dir=release
|
||||
release.jar=pit-${application.version}.jar
|
||||
|
Binary file not shown.
13
libpit/resources/main/test.groovy
Normal file
13
libpit/resources/main/test.groovy
Normal 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()
|
@ -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)))
|
||||
}
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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() }
|
||||
}
|
42
libpit/src/main/com/jdbernard/pit/Filter.groovy
Executable file
42
libpit/src/main/com/jdbernard/pit/Filter.groovy
Executable 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)))
|
||||
}
|
||||
}
|
@ -2,25 +2,30 @@ package com.jdbernard.pit
|
||||
|
||||
import java.lang.IllegalArgumentException as IAE
|
||||
|
||||
public abstract class Issue {
|
||||
public class Issue {
|
||||
|
||||
protected String id
|
||||
protected Category category
|
||||
protected Status status
|
||||
protected int priority
|
||||
protected String text
|
||||
protected Date deliveryDate
|
||||
protected Date creationDate
|
||||
protected String title
|
||||
|
||||
Issue(String id, Category c = Category.TASK, Status s = Status.NEW,
|
||||
int p = 9) {
|
||||
this.id = id
|
||||
this.category = c
|
||||
this.status = s
|
||||
this.priority = p
|
||||
this.creationDate = new Date()
|
||||
this.deliveryDate = null
|
||||
}
|
||||
Map extendedProperties = [:]
|
||||
|
||||
Issue(Map props) {
|
||||
this.id = props.id
|
||||
this.category = props.category ?: Category.TASK
|
||||
this.status = props.status ?: Status.NEW
|
||||
this.priority = props.priority ?: 5
|
||||
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; }
|
||||
|
||||
@ -48,19 +53,18 @@ public abstract class Issue {
|
||||
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 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 Date getDeliveryDate() { return deliveryDate }
|
||||
|
||||
public void setDeliveryDate(Date dd) { deliveryDate = dd }
|
||||
public def propertyMissing(String name, def value) {
|
||||
extendedProperties[name] = value }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
@ -10,30 +10,32 @@ public abstract class Project {
|
||||
|
||||
public void eachIssue(Filter filter = null, Closure c) {
|
||||
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))
|
||||
c.call(i)
|
||||
}
|
||||
c.call(i) }
|
||||
|
||||
public void eachProject(Filter filter = null, Closure c) {
|
||||
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))
|
||||
c.call(p)
|
||||
}
|
||||
c.call(p) }
|
||||
|
||||
// walk every issue and project in this project recursively and execute the
|
||||
// given closure on each issue that meets the filter criteria
|
||||
public void walkProject(Filter filter, Closure 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
|
||||
public List getAllIssues(Filter filter = null) {
|
||||
List result = this.issues.findAll { filter.accept(it) }
|
||||
this.eachProject(filter) { p -> result += p.getAllIssues(filter) }
|
||||
}
|
||||
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
|
||||
|
||||
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 }
|
||||
|
||||
@ -49,4 +51,10 @@ public abstract class Project {
|
||||
public abstract boolean deleteIssue(Issue issue)
|
||||
|
||||
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) }}}
|
||||
}
|
41
libpit/src/main/com/jdbernard/pit/Status.groovy
Executable file
41
libpit/src/main/com/jdbernard/pit/Status.groovy
Executable 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]
|
||||
}
|
||||
}
|
189
libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy
Executable file
189
libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy
Executable 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).") } }
|
||||
|
||||
}
|
@ -23,56 +23,65 @@ class FileProject extends Project {
|
||||
child.isHidden()) return // just an issue folder
|
||||
|
||||
// otherwise build and add to list
|
||||
projects[(child.name)] = new FileProject(child)
|
||||
} else if (child.isFile() &&
|
||||
projects[(child.name)] = new FileProject(child) }
|
||||
else if (child.isFile() &&
|
||||
FileIssue.isValidFilename(child.name)) {
|
||||
def issue
|
||||
|
||||
// if exception, then not an issue
|
||||
try { issue = new FileIssue(child) } catch (all) { return }
|
||||
|
||||
issues[(issue.id)] = issue
|
||||
}
|
||||
}
|
||||
}
|
||||
issues[(issue.id)] = issue } }}
|
||||
|
||||
public void setName(String 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) {
|
||||
Issue issue
|
||||
File issueFile
|
||||
|
||||
if (!options) options = [:]
|
||||
if (!options.category) options.category = Category.TASK
|
||||
if (!options.status) options.status = Status.NEW
|
||||
if (!options.priority) options.priority = 5
|
||||
if (!options.text) options.text = "Default issue title.\n" +
|
||||
"====================\n"
|
||||
String id
|
||||
if (issues.size() == 0) id = '0000'
|
||||
|
||||
// We want some different defaults for issues due to the parser being
|
||||
// unable to handle empty title or text.
|
||||
if (!options.title) options.title = "Default issue title."
|
||||
if (!options.text) options.text = "Describe the issue here."
|
||||
|
||||
// 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 {
|
||||
id = (issues.values().max { it.id.toInteger() }).id
|
||||
id = (id.toInteger() + 1).toString().padLeft(id.length(), '0')
|
||||
}
|
||||
def lastId = (issues.values().max { it.id.toInteger() }).id
|
||||
options.id = (lastId.toInteger() + 1).toString().padLeft(
|
||||
lastId.length(), '0') }
|
||||
|
||||
def issueFile = new File(source, FileIssue.makeFilename(id,
|
||||
options.category, options.status, options.priority))
|
||||
// Create an Issue object from the options (we will discard it later).
|
||||
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.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
|
||||
|
||||
return issue
|
||||
}
|
||||
return issue }
|
||||
|
||||
public FileProject createNewProject(String name) {
|
||||
def newDir = new File(source, name)
|
||||
newDir.mkdirs()
|
||||
|
||||
return new FileProject(newDir)
|
||||
}
|
||||
return new FileProject(newDir) }
|
||||
|
||||
public boolean deleteIssue(Issue issue) {
|
||||
if (!issues[(issue.id)]) return false
|
||||
@ -81,8 +90,7 @@ class FileProject extends Project {
|
||||
if (issue instanceof FileIssue)
|
||||
return issue.deleteFile()
|
||||
|
||||
else return true
|
||||
}
|
||||
else return true }
|
||||
|
||||
public boolean deleteProject(Project project) {
|
||||
if (!projects[(project.name)]) return false
|
||||
@ -91,8 +99,7 @@ class FileProject extends Project {
|
||||
if (project instanceof FileProject)
|
||||
return project.source.delete()
|
||||
|
||||
return true
|
||||
}
|
||||
return true }
|
||||
|
||||
@Override
|
||||
public String toString() { return name }
|
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal file
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal 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(); }
|
||||
}
|
@ -18,7 +18,8 @@ public class XmlIssue extends Issue {
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
this.project = project
|
||||
@ -26,9 +27,10 @@ public class XmlIssue extends Issue {
|
||||
|
||||
// Node constructor adds the node to the parent node
|
||||
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
|
||||
|
||||
repository.persist()
|
||||
@ -62,4 +64,11 @@ public class XmlIssue extends Issue {
|
||||
repository.persist()
|
||||
}
|
||||
|
||||
public void setTitle(String t) {
|
||||
super.setTitle(t)
|
||||
|
||||
issueNode.@title = t
|
||||
repository.persist()
|
||||
}
|
||||
|
||||
}
|
@ -2,7 +2,7 @@ package com.jdbernard.pit
|
||||
|
||||
public class MockIssue extends Issue {
|
||||
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 }
|
||||
}
|
@ -171,9 +171,7 @@ class FileIssueTest {
|
||||
assertEquals issue.status , Status.NEW
|
||||
assertEquals issue.priority , 1
|
||||
assertEquals issue.title , "Add the killer feature to the killer app."
|
||||
assertEquals issue.text , "Add the killer feature to the killer app.\n" +
|
||||
"=========================================\n\n" +
|
||||
"Make our killer app shine!."
|
||||
assertEquals issue.text , "Make our killer app shine!."
|
||||
assertEquals issue.source , issueFile
|
||||
}
|
||||
|
3
pit-cli/.gitignore
vendored
3
pit-cli/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
release/
|
||||
build/
|
||||
*.sw?
|
@ -1,40 +1,8 @@
|
||||
<project name="Personal Issue Tracker CLI">
|
||||
<property file="../version.properties"/>
|
||||
|
||||
<property file="project.properties"/>
|
||||
<property environment="env" />
|
||||
|
||||
<path id="groovy.libs">
|
||||
<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"/>
|
||||
<import file="../jdb-build-1.6.xml"/>
|
||||
|
||||
<target name="init">
|
||||
<fail
|
||||
@ -42,77 +10,32 @@
|
||||
message="GROOVY_HOME environment variable is not set."/>
|
||||
<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>
|
||||
<not>
|
||||
<available
|
||||
file="${lib.dir}/pit-${application.version}.jar"/>
|
||||
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
|
||||
</not>
|
||||
</condition>
|
||||
</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 name="upgrade-version">
|
||||
<propertyfile file="project.properties">
|
||||
<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 name="clean-all">
|
||||
<delete dir="${release.dir}"/>
|
||||
</target>
|
||||
|
||||
<target name="increment-build-number" depends="init">
|
||||
<propertyfile file="project.properties">
|
||||
<entry key="build.number" operation="+" type="int" default="0"/>
|
||||
</propertyfile>
|
||||
</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="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 name="lib">
|
||||
<copy todir="${build.dir}/lib/compile/jar"
|
||||
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
|
||||
<copy todir="${build.dir}/lib/runtime/jar"
|
||||
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
|
||||
</target>
|
||||
|
||||
<target name="release" depends="build">
|
||||
<delete dir="${release.dir}"/>
|
||||
<mkdir dir="${release.dir}"/>
|
||||
<copy file="${build.dir}/jar/${build.jar}"
|
||||
tofile="${release.dir}/${release.jar}"/>
|
||||
<mkdir dir="${release.dir}/lib"/>
|
||||
<copy todir="${release.dir}/lib">
|
||||
<fileset dir="${build.dir}/lib/runtime/jar"/></copy>
|
||||
<copy tofile="${release.dir}/${name}-${version}.jar"
|
||||
file="${build.dir}/${name}-${version}.${build.number}.jar"/>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
|
BIN
pit-cli/lib/compile/jar/joda-time-2.0.jar
Normal file
BIN
pit-cli/lib/compile/jar/joda-time-2.0.jar
Normal file
Binary file not shown.
BIN
pit-cli/lib/compile/jar/slf4j-api-1.6.1.jar
Normal file
BIN
pit-cli/lib/compile/jar/slf4j-api-1.6.1.jar
Normal file
Binary file not shown.
Binary file not shown.
BIN
pit-cli/lib/runtime/jar/joda-time-2.0.jar
Normal file
BIN
pit-cli/lib/runtime/jar/joda-time-2.0.jar
Normal file
Binary file not shown.
BIN
pit-cli/lib/runtime/jar/logback-classic-0.9.26.jar
Normal file
BIN
pit-cli/lib/runtime/jar/logback-classic-0.9.26.jar
Normal file
Binary file not shown.
BIN
pit-cli/lib/runtime/jar/logback-core-0.9.26.jar
Normal file
BIN
pit-cli/lib/runtime/jar/logback-core-0.9.26.jar
Normal file
Binary file not shown.
BIN
pit-cli/lib/runtime/jar/slf4j-api-1.6.1.jar
Normal file
BIN
pit-cli/lib/runtime/jar/slf4j-api-1.6.1.jar
Normal file
Binary file not shown.
@ -1,10 +1,12 @@
|
||||
#Tue, 25 Oct 2011 11:32:31 -0500
|
||||
#Mon, 13 Feb 2012 10:46:15 -0600
|
||||
build.dir=build
|
||||
src.dir=src
|
||||
build.jar=pit-cli-${application.version}.${build.number}.jar
|
||||
build.number=13
|
||||
expected.application.version=2.6.0
|
||||
build.number=5
|
||||
version=3.3.2
|
||||
name=pit-cli
|
||||
lib.dir=lib
|
||||
lib.local=true
|
||||
release.dir=release
|
||||
release.jar=pit-cli-${application.version}.jar
|
||||
main.class=com.jdbernard.pit.PersonalIssueTrackerCLI
|
||||
|
@ -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.0"
|
||||
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, closed): "
|
||||
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}"}
|
||||
}
|
@ -0,0 +1,611 @@
|
||||
/**
|
||||
* # 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 ##
|
||||
/// --------------------------
|
||||
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-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 ' +
|
||||
' 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-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' +
|
||||
' (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).')
|
||||
|
||||
/// --version
|
||||
/// : Display PIT version information.
|
||||
cli._(longOpt: 'version', 'Display PIT version information.')
|
||||
|
||||
/// ## Parse CLI Options ##
|
||||
/// -----------------------
|
||||
|
||||
log.trace("Parsing options.")
|
||||
|
||||
def VERSION = "3.3.2"
|
||||
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.")
|
||||
|
||||
/// #### Parse daily list specific display options.
|
||||
def visibleSections = []
|
||||
def suppressedSections
|
||||
|
||||
/// Parse the additive options first.
|
||||
if (opts.'dl-scheduled') { visibleSections << 'scheduled' }
|
||||
if (opts.'dl-due') { visibleSections << 'due' }
|
||||
if (opts.'dl-reminder') { visibleSections << 'reminder' }
|
||||
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', 'reminder', '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-reminder') { visibleSections -= 'reminder' }
|
||||
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 }
|
||||
|
||||
/// Set up our time intervals.
|
||||
def today = new DateMidnight()
|
||||
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 dueToday = []
|
||||
def reminderToday = []
|
||||
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.
|
||||
else if (issue.reminder && issue.reminder < tomorrow) {
|
||||
reminderToday << 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('reminder') && reminderToday.size() > 0) {
|
||||
println "Upcoming Tasks"
|
||||
println "--------------"
|
||||
|
||||
reminderToday.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.
@ -1 +1 @@
|
||||
application.version=2.6.0
|
||||
application.version=3.3.2
|
||||
|
Reference in New Issue
Block a user