Compare commits

..

1 Commits
3.2.3 ... 2.6.1

Author SHA1 Message Date
538c341823 Bugfix: fixed typo in pit-cli. 2011-10-26 15:03:31 -05:00
57 changed files with 711 additions and 1218 deletions

1
.gitignore vendored
View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -1,13 +1,11 @@
#Sun, 11 Dec 2011 21:03:38 -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=3
version=3.2.3
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

Binary file not shown.

View File

@ -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()

View 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)))
}
}

View File

@ -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() {

View File

@ -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) }}}
}

View File

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

View File

@ -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";
}
}

View File

@ -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 }

View File

@ -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()
}
}

View File

@ -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() }
}

View File

@ -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)))
}
}

View File

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

View File

@ -1,189 +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 {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, c, status, priority))
if (source.renameTo(newSource)) {
source = newSource
super.setCategory(c) }
else { throw new IOException("I was unable to set the category. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public void setStatus(Status s) throws IOException {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, category, s, priority))
if (source.renameTo(newSource)) {
source = newSource
super.setStatus(s) }
else { throw new IOException("I was unable to set the status. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public void setPriority(int p) throws IOException {
File newSource = new File(source.canonicalFile.parentFile,
makeFilename(id, category, status, p))
if (source.renameTo(newSource)) {
source = newSource
super.setPriority(p) }
else { throw new IOException("I was unable to set the priority. "
+ "I need to rename the file for this issue, but something is "
+ "preventing me from doing so (maybe the path to the file is "
+ "no longer valid, or maybe the file is currently open in "
+ "some other program).") }}
public String getFilename() {
return makeFilename(id, category, status, priority) }
public void setTitle(String title) throws IOException {
super.setTitle(title)
writeFile() }
public void setText(String text) throws IOException {
super.setText(text)
writeFile() }
public def propertyMissing(String name, def value) {
super.propertyMissing(name, value)
writeFile() }
boolean deleteFile() { return source.deleteDir() }
public static boolean isValidFilename(String name) {
return name ==~ fileExp }
public static String makeFilename(String id, Category category,
Status status, int priority) {
// bounds check priority
priority = Math.min(9, Math.max(0, priority))
//check for valid values of cateogry and id
if (category == null)
throw new IAE("Category must be non-null.")
if (status == null)
throw new IAE("Status must be non-null.")
if (!(id ==~ /\d+/))
throw new IAE( "'${id}' is not a legal value for id.")
return id + category.symbol + status.symbol + priority + ".rst" }
public static String formatIssue(Issue issue) {
def result = new StringBuilder()
result.append(issue.title)
result.append("\n")
result.append("=".multiply(issue.title.length()))
result.append("\n\n")
result.append(issue.text)
// If there are any extended properties, let's write those.
if (issue.extendedProperties.size() > 0) {
result.append("\n----\n\n")
def extOutput = [:]
def maxKeyLen = 0
def maxValLen = 0
// Find the longest key and value, convert all to strings.
issue.extendedProperties.each { key, val ->
def ks = key.toString().split('_').collect({it.capitalize()}).join(' ')
def vs = ExtendedPropertyHelp.format(val)
extOutput[ks] = vs
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
if (vs.length() > maxValLen) { maxValLen = vs.length() } }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n")
extOutput.sort().each { key, val ->
result.append(key.padRight(maxKeyLen))
result.append(": ")
result.append(val.padRight(maxValLen))
result.append("\n") }
result.append("=".multiply(maxKeyLen + 1))
result.append(" ")
result.append("=".multiply(maxValLen))
result.append("\n") }
return result.toString()}
protected void writeFile() {
try { source.write(formatIssue(this)) }
catch (IOException ioe) {
throw new IOException("I could not save the new text for this "
+ "issue. I can not write to the file for this issue. I do not"
+ " know why, I am sorry (maybe the file can not be reached).") } }
}

View File

@ -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(); }
}

View File

@ -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 }
}

View File

@ -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
View File

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

View File

@ -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>

BIN
pit-cli/lib/pit-2.6.1.jar Normal file

Binary file not shown.

View File

@ -1,12 +1,10 @@
#Sun, 11 Dec 2011 21:04:03 -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=2
version=3.2.3
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

View 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}"}
}

View File

@ -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.3"
def opts = cli.parse(args)
def issuedb = [:]
def workingDir = new File('.')
// defaults for the issue filter/selector
def selectOpts = [
categories: ['bug', 'feature', 'task'],
status: ['new', 'reassigned', 'rejected',
'resolved', 'validation_required'],
priority: 9,
projects: [],
ids: [],
extendedProperties: [:],
acceptProjects: true]
// 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 }}

View File

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