Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
538c341823 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
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,17 +21,15 @@
|
||||
<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">
|
||||
<target name="package" depends="libpit,pit-cli,pit-swing">
|
||||
<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>
|
||||
|
@ -1,203 +0,0 @@
|
||||
<?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,8 +1,30 @@
|
||||
<project name="Personal Issue Tracker" default="release">
|
||||
|
||||
<property file="../version.properties"/>
|
||||
<property file="project.properties"/>
|
||||
<property environment="env"/>
|
||||
|
||||
<import file="../jdb-build-1.6.xml"/>
|
||||
<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"/>
|
||||
|
||||
<target name="init">
|
||||
<fail
|
||||
@ -11,13 +33,79 @@
|
||||
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
|
||||
</target>
|
||||
|
||||
<target name="release" depends="build">
|
||||
<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 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>
|
||||
|
||||
<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}"/>
|
||||
</target>
|
||||
</project>
|
||||
|
@ -1,7 +0,0 @@
|
||||
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.
@ -1,13 +1,11 @@
|
||||
#Thu, 08 Dec 2011 14:35:45 -0600
|
||||
#Wed, 26 Oct 2011 14:24:41 -0500
|
||||
#Sat Apr 24 17:08:00 CDT 2010
|
||||
build.dir=build
|
||||
src.dir=src
|
||||
lib.shared.dir=../shared-libs
|
||||
test.dir=test
|
||||
build.number=0
|
||||
version=3.2.1
|
||||
name=libpit
|
||||
build.number=1
|
||||
expected.application.version=2.6.1
|
||||
lib.dir=lib
|
||||
lib.local=true
|
||||
release.dir=release
|
||||
release.jar=pit-${application.version}.jar
|
||||
|
BIN
libpit/release/pit-2.6.1.jar
Normal file
BIN
libpit/release/pit-2.6.1.jar
Normal file
Binary file not shown.
@ -1,13 +0,0 @@
|
||||
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()
|
33
libpit/src/com/jdbernard/pit/Filter.groovy
Executable file
33
libpit/src/com/jdbernard/pit/Filter.groovy
Executable file
@ -0,0 +1,33 @@
|
||||
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)))
|
||||
}
|
||||
}
|
@ -2,30 +2,25 @@ package com.jdbernard.pit
|
||||
|
||||
import java.lang.IllegalArgumentException as IAE
|
||||
|
||||
public class Issue {
|
||||
public abstract class Issue {
|
||||
|
||||
protected String id
|
||||
protected Category category
|
||||
protected Status status
|
||||
protected int priority
|
||||
protected String text
|
||||
protected String title
|
||||
protected Date deliveryDate
|
||||
protected Date creationDate
|
||||
|
||||
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) })}
|
||||
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
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
|
||||
@ -53,18 +48,19 @@ public class Issue {
|
||||
priority = Math.min(9, Math.max(0, p))
|
||||
}
|
||||
|
||||
public String getTitle() { return title }
|
||||
|
||||
public void setTitle(String t) throws IOException { title = t }
|
||||
public String getTitle() { return text.readLines()[0] }
|
||||
|
||||
public String getText() { return text }
|
||||
|
||||
public void setText(String t) throws IOException { text = t }
|
||||
|
||||
public def propertyMissing(String name) { extendedProperties[name] }
|
||||
public boolean hasDelivery() { return deliveryDate == null }
|
||||
|
||||
public def propertyMissing(String name, def value) {
|
||||
extendedProperties[name] = value }
|
||||
public Date getCreationDate() { return creationDate }
|
||||
|
||||
public Date getDeliveryDate() { return deliveryDate }
|
||||
|
||||
public void setDeliveryDate(Date dd) { deliveryDate = dd }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
@ -10,32 +10,30 @@ public abstract class Project {
|
||||
|
||||
public void eachIssue(Filter filter = null, Closure c) {
|
||||
def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
|
||||
for (i in sort(issues.values(), sorter))
|
||||
for (i in issues.values().sort(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 sort(projects.values(), sorter))
|
||||
for (p in projects.values().sort(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) {
|
||||
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) }
|
||||
List result = this.issues.findAll { filter.accept(it) }
|
||||
this.eachProject(filter) { p -> result += p.getAllIssues(filter) }
|
||||
}
|
||||
|
||||
public void setName(String name) { this.name = name }
|
||||
|
||||
@ -51,10 +49,4 @@ 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/com/jdbernard/pit/Status.groovy
Executable file
41
libpit/src/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) {
|
||||
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]
|
||||
}
|
||||
}
|
110
libpit/src/com/jdbernard/pit/file/FileIssue.groovy
Executable file
110
libpit/src/com/jdbernard/pit/file/FileIssue.groovy
Executable file
@ -0,0 +1,110 @@
|
||||
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";
|
||||
}
|
||||
|
||||
}
|
@ -23,65 +23,56 @@ 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 = [:]
|
||||
|
||||
// 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'
|
||||
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'
|
||||
else {
|
||||
def lastId = (issues.values().max { it.id.toInteger() }).id
|
||||
options.id = (lastId.toInteger() + 1).toString().padLeft(
|
||||
lastId.length(), '0') }
|
||||
id = (issues.values().max { it.id.toInteger() }).id
|
||||
id = (id.toInteger() + 1).toString().padLeft(id.length(), '0')
|
||||
}
|
||||
|
||||
// Create an Issue object from the options (we will discard it later).
|
||||
issue = new Issue(options)
|
||||
def issueFile = new File(source, FileIssue.makeFilename(id,
|
||||
options.category, options.status, options.priority))
|
||||
|
||||
// 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)
|
||||
|
||||
// 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.
|
||||
def issue = new FileIssue(issueFile)
|
||||
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
|
||||
@ -90,7 +81,8 @@ 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
|
||||
@ -99,7 +91,8 @@ class FileProject extends Project {
|
||||
if (project instanceof FileProject)
|
||||
return project.source.delete()
|
||||
|
||||
return true }
|
||||
return true
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return name }
|
@ -18,8 +18,7 @@ public class XmlIssue extends Issue {
|
||||
}
|
||||
|
||||
XmlIssue(String id, Category c = Category.TASK, Status s = Status.NEW,
|
||||
int p = 9, String title, String text, XmlRepository repository,
|
||||
XmlProject project) {
|
||||
int p = 9, String text, XmlRepository repository, XmlProject project) {
|
||||
super(id, c, s, p)
|
||||
|
||||
this.project = project
|
||||
@ -27,10 +26,9 @@ 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, title: title])
|
||||
[id: id, category: c, status: s, priority: p])
|
||||
|
||||
super.@title = title
|
||||
super.@text = text
|
||||
this.text = text
|
||||
issueNode.value = text
|
||||
|
||||
repository.persist()
|
||||
@ -64,11 +62,4 @@ public class XmlIssue extends Issue {
|
||||
repository.persist()
|
||||
}
|
||||
|
||||
public void setTitle(String t) {
|
||||
super.setTitle(t)
|
||||
|
||||
issueNode.@title = t
|
||||
repository.persist()
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
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() }
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
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)))
|
||||
}
|
||||
}
|
@ -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) {
|
||||
// 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]
|
||||
}
|
||||
}
|
@ -1,183 +0,0 @@
|
||||
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 {
|
||||
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 setTitle(String title) throws IOException {
|
||||
super.setTitle(title)
|
||||
writeFile() }
|
||||
|
||||
public void setText(String text) throws IOException {
|
||||
super.setText(text)
|
||||
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() > maxKeyLen) { 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).") } }
|
||||
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
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()),
|
||||
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(); }
|
||||
}
|
@ -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: id, category: c, status: s, priority: p])
|
||||
super (id, c, s, p)
|
||||
}
|
||||
public boolean delete() { return true }
|
||||
}
|
@ -171,7 +171,9 @@ 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 , "Make our killer app shine!."
|
||||
assertEquals issue.text , "Add the killer feature to the killer app.\n" +
|
||||
"=========================================\n\n" +
|
||||
"Make our killer app shine!."
|
||||
assertEquals issue.source , issueFile
|
||||
}
|
||||
|
3
pit-cli/.gitignore
vendored
Normal file
3
pit-cli/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
release/
|
||||
build/
|
||||
*.sw?
|
@ -1,8 +1,40 @@
|
||||
<project name="Personal Issue Tracker CLI">
|
||||
|
||||
<property file="../version.properties"/>
|
||||
<property file="project.properties"/>
|
||||
<property environment="env" />
|
||||
|
||||
<import file="../jdb-build-1.6.xml"/>
|
||||
<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"/>
|
||||
|
||||
<target name="init">
|
||||
<fail
|
||||
@ -10,28 +42,77 @@
|
||||
message="GROOVY_HOME environment variable is not set."/>
|
||||
<echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
|
||||
|
||||
<fail message="Could not find PIT ${version} library.">
|
||||
<fail message="Could not find PIT ${application.version} library.">
|
||||
<condition>
|
||||
<not>
|
||||
<available
|
||||
file="${basedir}/../libpit/release/libpit-${version}.jar"/>
|
||||
file="${lib.dir}/pit-${application.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="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 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>
|
||||
|
||||
<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>
|
||||
|
||||
<target name="release" depends="build">
|
||||
<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"/>
|
||||
<delete dir="${release.dir}"/>
|
||||
<mkdir dir="${release.dir}"/>
|
||||
<copy file="${build.dir}/jar/${build.jar}"
|
||||
tofile="${release.dir}/${release.jar}"/>
|
||||
</target>
|
||||
|
||||
</project>
|
||||
|
Binary file not shown.
BIN
pit-cli/lib/pit-2.6.1.jar
Normal file
BIN
pit-cli/lib/pit-2.6.1.jar
Normal file
Binary file not shown.
Binary file not shown.
@ -1,12 +1,10 @@
|
||||
#Thu, 08 Dec 2011 14:59:30 -0600
|
||||
#Wed, 26 Oct 2011 15:01:22 -0500
|
||||
build.dir=build
|
||||
src.dir=src
|
||||
build.jar=pit-cli-${application.version}.${build.number}.jar
|
||||
build.number=0
|
||||
version=3.2.1
|
||||
name=pit-cli
|
||||
build.number=1
|
||||
expected.application.version=2.6.1
|
||||
lib.dir=lib
|
||||
lib.local=true
|
||||
release.dir=release
|
||||
release.jar=pit-cli-${application.version}.jar
|
||||
main.class=com.jdbernard.pit.PersonalIssueTrackerCLI
|
||||
|
253
pit-cli/src/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy
Normal file
253
pit-cli/src/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy
Normal file
@ -0,0 +1,253 @@
|
||||
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}"}
|
||||
}
|
@ -1,440 +0,0 @@
|
||||
package com.jdbernard.pit
|
||||
|
||||
import com.jdbernard.pit.file.*
|
||||
|
||||
import org.joda.time.DateMidnight
|
||||
import org.joda.time.DateTime
|
||||
|
||||
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.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.
|
||||
cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
|
||||
'Modify the priority of the selected issues.')
|
||||
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
|
||||
'Modify the category of the selected issues.')
|
||||
cli.S(argName: 'new-status', longOpt: 'set-status', args: 1,
|
||||
'Modify the status of the selected issues.')
|
||||
cli.E(argName: 'new-extended-property', args: 1, 'Modify the extended ' +
|
||||
'property of the selected issues. Format is "-E <propname>=<propvalue>"')
|
||||
cli.n(longOpt: 'new-issue', 'Create a new issue.')
|
||||
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.')
|
||||
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.')
|
||||
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.')
|
||||
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
|
||||
'Use <dir> as the base directory (defaults to current directory).')
|
||||
cli.D(longOpt: 'daily-list', 'Print a Daily Task list based on issue Due and' +
|
||||
' Reminder properties.')
|
||||
cli._(longOpt: 'dl-scheduled', 'Show scheduled 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).')
|
||||
cli._(longOpt: 'dl-reminder', 'Show upcoming 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).')
|
||||
cli._(longOpt: 'dl-hide-scheduled', 'Hide scheduled 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).')
|
||||
cli._(longOpt: 'dl-hide-reminder', 'Show upcoming 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).')
|
||||
cli._(longOpt: 'version', 'Display PIT version information.')
|
||||
|
||||
// =================================== //
|
||||
// ======== Parse CLI Options ======== //
|
||||
// =================================== //
|
||||
|
||||
def VERSION = "3.2.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: [],
|
||||
extendedProperties: [:],
|
||||
acceptProjects: true]
|
||||
|
||||
// options for changing properties of issue(s)
|
||||
def assignOpts = [:]
|
||||
|
||||
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}'."
|
||||
print "Valid options are: \n${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) }
|
||||
|
||||
// read and parse 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 extended property selection criteria
|
||||
if (opts.e) {
|
||||
opts.es.each { option ->
|
||||
def parts = option.split("=")
|
||||
selectOpts.extendedProperties[parts[0]] =
|
||||
ExtendedPropertyHelp.parse(parts[1]) }}
|
||||
|
||||
// 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) }
|
||||
|
||||
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')
|
||||
|
||||
|
||||
// ========================= //
|
||||
// ======== Actions ======== //
|
||||
// ========================= //
|
||||
|
||||
// list version information first
|
||||
if (opts.version) {
|
||||
|
||||
println "PIT CLI Version ${VERSION}"
|
||||
println "Written by Jonathan Bernard\n" }
|
||||
|
||||
else {
|
||||
|
||||
// build issue list
|
||||
issuedb = new FileProject(workingDir)
|
||||
|
||||
// build filter from options
|
||||
def filter = new Filter(selectOpts)
|
||||
|
||||
// list second
|
||||
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}" }
|
||||
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, "") }
|
||||
// print all projects
|
||||
issuedb.eachProject(filter) { printProject(it, "") } }
|
||||
|
||||
// daily list second
|
||||
else if (opts.D) {
|
||||
|
||||
// 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 filter for the default 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.
|
||||
if (!opts.o) { filter.issueSorter = [ {it.due}, {it.priority}, {it.id} ] }
|
||||
|
||||
// Get our issues
|
||||
def allIssues = issuedb.getAllIssues(filter)
|
||||
|
||||
// Set up our time interval.
|
||||
def today = new DateMidnight()
|
||||
def tomorrow = today.plusDays(1)
|
||||
|
||||
def scheduledToday = []
|
||||
def dueToday = []
|
||||
def reminderToday = []
|
||||
def notDueOrReminder = []
|
||||
|
||||
def printIssue = { issue ->
|
||||
if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}"
|
||||
else println " -- ${issue}" }
|
||||
|
||||
// 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.each { printIssue(it) }
|
||||
|
||||
println "" }
|
||||
|
||||
if (visibleSections.contains('due') && dueToday.size() > 0) {
|
||||
println "Tasks Due Today"
|
||||
println "---------------"
|
||||
|
||||
dueToday.each { printIssue(it) }
|
||||
|
||||
println ""}
|
||||
|
||||
if (visibleSections.contains('reminder') && reminderToday.size() > 0) {
|
||||
println "Upcoming Tasks"
|
||||
println "--------------"
|
||||
|
||||
reminderToday.each { printIssue(it) }
|
||||
|
||||
println ""}
|
||||
|
||||
if (visibleSections.contains('open') && notDueOrReminder.size() > 0) {
|
||||
println "Other Open Issues"
|
||||
println "-----------------"
|
||||
|
||||
notDueOrReminder.each { printIssue(it) }
|
||||
|
||||
println "" }}
|
||||
|
||||
// new issues fourth
|
||||
else if (opts.n) {
|
||||
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 as
|
||||
// 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) {} }
|
||||
|
||||
issue = issuedb.createNewIssue(assignOpts)
|
||||
|
||||
println "New issue created: "
|
||||
println issue }
|
||||
|
||||
// last, changes to existing issues
|
||||
else if (assignOpts.size() > 0) {
|
||||
|
||||
// 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 }}
|
||||
|
||||
issuedb.walkProject(filter) { issue ->
|
||||
println issue
|
||||
assignOpts.each { propName, value ->
|
||||
issue[propName] = value
|
||||
def formattedValue = ExtendedPropertyHelp.format(value)
|
||||
println " set ${propName} to ${formattedValue}" } }}
|
||||
|
||||
else { cli.usage(); return -1 }}
|
@ -1 +1 @@
|
||||
application.version=3.0.0
|
||||
application.version=2.6.1
|
||||
|
Reference in New Issue
Block a user