Compare commits
	
		
			92 Commits
		
	
	
		
			3.2.3
			...
			8b0c751344
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8b0c751344 | |||
| 0f7e257f76 | |||
| 76225d1c50 | |||
| 0361d1b869 | |||
| 40cb602362 | |||
| e2a306c1d6 | |||
| e955cd5b24 | |||
| 4176dfea3a | |||
| 587e3c4509 | |||
| f6a97c384a | |||
| 0c3d73dc2b | |||
| 9a0bf35882 | |||
| be7c099b7b | |||
| d04797460c | |||
| 8cf0bf5d98 | |||
| ddad90ddef | |||
| 34ce2b61b9 | |||
| 661d5959c6 | |||
| 6665f09b7b | |||
| bcb1c7c17c | |||
| b0e3f5a9d8 | |||
| fee4ba70a6 | |||
| 171adbb59d | |||
| d01d6e37f4 | |||
| b98596574d | |||
| ea9f8ea7ac | |||
| ae4a943e82 | |||
| 58a5321d95 | |||
| 7215b4969b | |||
| c7891de310 | |||
| a373af0658 | |||
| de3ee05680 | |||
| 59440d2c9d | |||
| 6226ff21c5 | |||
| 71e035fdbe | |||
| df854f864c | |||
| 7bccd83e23 | |||
| b25d2be164 | |||
| e0ab3cb401 | |||
| d93c0cf348 | |||
| 9606e71cec | |||
| 98f4dda1ad | |||
| 393be347c9 | |||
| f8fed9d937 | |||
| ef16eafd48 | |||
| 4af0d09356 | |||
| 071c4b66e5 | |||
| 57a3af4f2f | |||
| 08b9df2086 | |||
| 339e88cddd | |||
| 0a2249018b | |||
| ec3008937d | |||
| 10fcc34ea2 | |||
| 4127fbe41c | |||
| 0671d7728e | |||
| 7b5f26f24a | |||
| 
						 | 
					db3e648d47 | ||
| 476a94c679 | |||
| 65edc56e08 | |||
| d4db66a71e | |||
| f8ccc831ef | |||
| 
						 | 
					93a0a15f12 | ||
| 
						 | 
					dc31d590a0 | ||
| 
						 | 
					8b46cc19d8 | ||
| 
						 | 
					567c2d2178 | ||
| 
						 | 
					08dfbde57f | ||
| 
						 | 
					a924d7b649 | ||
| 
						 | 
					2404f6a3d1 | ||
| 
						 | 
					2b5f82203c | ||
| 
						 | 
					29959a6a8d | ||
| 
						 | 
					6f247032a3 | ||
| 
						 | 
					efd5f6adff | ||
| 
						 | 
					49c5753ef1 | ||
| 
						 | 
					3bdb2ecb1f | ||
| 
						 | 
					28569a643e | ||
| 
						 | 
					97eb286e32 | ||
| 
						 | 
					fcab7a4cc6 | ||
| 
						 | 
					20c82ea9ba | ||
| 
						 | 
					11b18317bd | ||
| 
						 | 
					d86da67284 | ||
| 
						 | 
					063a869b51 | ||
| 
						 | 
					46d4db0d6a | ||
| 
						 | 
					34e01119a9 | ||
| 
						 | 
					d6880d9cc1 | ||
| 
						 | 
					582bf819f5 | ||
| 
						 | 
					76d1e48ebb | ||
| 
						 | 
					4fd297e03d | ||
| 
						 | 
					d0e968b2b7 | ||
| 
						 | 
					c0b02ca222 | ||
| 
						 | 
					85753de955 | ||
| 
						 | 
					0441f3c510 | ||
| 
						 | 
					c01eaa0255 | 
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,3 +1,4 @@
 | 
			
		||||
release/
 | 
			
		||||
*.sw*
 | 
			
		||||
*/build/
 | 
			
		||||
nimcache/
 | 
			
		||||
/pit
 | 
			
		||||
/pit_api
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.mise.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
[tools]
 | 
			
		||||
nim = "2.2.0"
 | 
			
		||||
							
								
								
									
										138
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,138 @@
 | 
			
		||||
# Personal Issue Tracker
 | 
			
		||||
 | 
			
		||||
This is [Jonathan Bernard's](mailto:jonathan@jdbernard.com) personal issue
 | 
			
		||||
tracker. In it's current form it is essentially a way to keep an curated list of
 | 
			
		||||
TODO's, organizing them by workflow category (todo, todo-today, dormant, etc.)
 | 
			
		||||
and context (Personal, Work, etc.).
 | 
			
		||||
 | 
			
		||||
## Categories
 | 
			
		||||
 | 
			
		||||
`pit` organizes issues into the following workflow categories:
 | 
			
		||||
 | 
			
		||||
- `current` - actively in progress
 | 
			
		||||
- `todo` - to be addressed in the future
 | 
			
		||||
- `todo-today` - chosen to be addressed today
 | 
			
		||||
- `pending` - blocked by some third party
 | 
			
		||||
- `dormant` - long-term things I don't want to forget but don't need in front
 | 
			
		||||
  of me every day.
 | 
			
		||||
- `done`
 | 
			
		||||
 | 
			
		||||
In my typical workflow the `todo` category serves as a collection point for
 | 
			
		||||
things I want to keep track of. Then on a a daily basis I review issues in the
 | 
			
		||||
`todo` category and move a selection to the `todo-today` category. I also try
 | 
			
		||||
to keep the total number of issues in the `todo` below about a dozen. If there
 | 
			
		||||
are more than a dozen things in my `todo` category I will identify the lowest
 | 
			
		||||
priority items and move them to the `dormant` category.
 | 
			
		||||
 | 
			
		||||
## Issue Properties
 | 
			
		||||
 | 
			
		||||
`pit` allows arbitrary properties to be attached to issues in the form of
 | 
			
		||||
key-value pairs. On the command line these can be provided via the `-p` or
 | 
			
		||||
`--properties` parameter in the form
 | 
			
		||||
`-p <prop1Name>:<prop1Value>;<prop2Name>:<prop2Value>[;...]`
 | 
			
		||||
 | 
			
		||||
There are a couple of properties that pit will recognize automatically:
 | 
			
		||||
 | 
			
		||||
- `context`: the context organization feature is implemented using issue
 | 
			
		||||
  properties.
 | 
			
		||||
- `created`: `pit` uses this property to timestamp an issue when it is created.
 | 
			
		||||
- `completed`: `pit` uses this property to timestamp an issue when it is moved
 | 
			
		||||
  to the `done` category.
 | 
			
		||||
- `pending`: `pit` looks to this property to provide extra information about
 | 
			
		||||
  issues in the `pending` category. Typically I use this to note who or what is
 | 
			
		||||
  blocking the issue and why.
 | 
			
		||||
 | 
			
		||||
Some other common properties I use are:
 | 
			
		||||
 | 
			
		||||
- `resolution`: for short notes about why an issue was moved to `done`,
 | 
			
		||||
  especially if it the action wasn't taken or if it is not completely clear
 | 
			
		||||
  that this issue was completed.
 | 
			
		||||
 | 
			
		||||
## Configuration Options
 | 
			
		||||
 | 
			
		||||
`pit` allows configuration via command-line options and via a configuration
 | 
			
		||||
file. There is some overlap between the two methods of configuring `pit`, but
 | 
			
		||||
it is not a complete mapping.
 | 
			
		||||
 | 
			
		||||
### Config File
 | 
			
		||||
 | 
			
		||||
`pit` looks for a JSON configuration file in the following places (in order):
 | 
			
		||||
 | 
			
		||||
1. From a file path passed on the command line via the `--config <cfgFile>` parameter,
 | 
			
		||||
2. `./.pitrc`, in the current working directory,
 | 
			
		||||
3. From a file path set in the `PITRC` environment variable.
 | 
			
		||||
4. `$HOME/.pitrc`, in the user's home directory.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#### Sample Config File
 | 
			
		||||
 | 
			
		||||
This example illustrates all of the possible configuration options.
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  "api": {
 | 
			
		||||
    "apiKeys": [
 | 
			
		||||
      "50cdcb660554e2d50fd88bd40b6579717bf00643f6ff57f108baf16c8c083f77",
 | 
			
		||||
      "e4fc1aac49fc1f2f7f4ca6b1f04d41a4ccdd58e13bb53b41da97703d47267ceb",
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "cli": {
 | 
			
		||||
    "defaultContext": "personal",
 | 
			
		||||
    "verbose": false,
 | 
			
		||||
    "termWidth": 120,
 | 
			
		||||
    "triggerPtk": true
 | 
			
		||||
  },
 | 
			
		||||
  "contexts": {
 | 
			
		||||
    "nla-music": "New Life Music",
 | 
			
		||||
    "nla-youth-band": "New Life Youth Band",
 | 
			
		||||
    "acn": "Accenture",
 | 
			
		||||
    "hff": "Hope Family Fellowship"
 | 
			
		||||
  },
 | 
			
		||||
  "tasksDir": "/mnt/c/Users/Jonathan Bernard/synced/tasks"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Explanation of configurable options.
 | 
			
		||||
 | 
			
		||||
In general, options supplied on the CLI directly will override options supplied
 | 
			
		||||
in the configuration file. All options are optional unless stated otherwise.
 | 
			
		||||
 | 
			
		||||
* `api`: configuration options specific to the API service.
 | 
			
		||||
 | 
			
		||||
  - `apiKeys`: a list of Bearer tokens accepted by the API for the purpose of
 | 
			
		||||
    authenticating API requests.
 | 
			
		||||
 | 
			
		||||
* `cli`: configuration options specific to the CLI.
 | 
			
		||||
 | 
			
		||||
  - `defaultContext`: if present all invokations to the CLI will
 | 
			
		||||
    be in this context. This is like adding a `--context <defaultContext>`
 | 
			
		||||
    parameter to every CLI invocation. Any actual `--context` parameter will
 | 
			
		||||
    override this value.
 | 
			
		||||
 | 
			
		||||
  - `verbose`: Show issue details when listing issues (same as
 | 
			
		||||
    `--verbose` flag).
 | 
			
		||||
 | 
			
		||||
  - `termWidth`: Set the expected width of the terminal (for wrapping text).
 | 
			
		||||
 | 
			
		||||
  - `triggerPtk`: If set to `true`, invoke the `ptk` command to start and stop
 | 
			
		||||
    timers when issues move to the `current` and `done` categories
 | 
			
		||||
    respectively.
 | 
			
		||||
 | 
			
		||||
* `contexts`: `pit` allows issues to be organized into different contexts via
 | 
			
		||||
  a `context` property on the issue. The CLI groups issues according to
 | 
			
		||||
  context. When printing contexts the CLI will take the value from the issues'
 | 
			
		||||
  `context` properties and capatalize it. In some cases you may wish to have a
 | 
			
		||||
  different display value for a context. I like to use abbreviations for long
 | 
			
		||||
  context names to reduce the need to type, `hff` for "Hope Family Fellowship",
 | 
			
		||||
  for example. The `contexts` config option allows you to provide a map of
 | 
			
		||||
  context values to context display names See the sample file below for an
 | 
			
		||||
  example.
 | 
			
		||||
 | 
			
		||||
  Note that this mapping does not have to have entries for all contexts, only
 | 
			
		||||
  those you wish to provide with an alternate display form. For example, in the
 | 
			
		||||
  configuration sample above the default context is `personal`, a value not
 | 
			
		||||
  present in the `contexts` configuration. `personal` will be displayed as
 | 
			
		||||
  "Personal"; it does not need an alternate display name.
 | 
			
		||||
 | 
			
		||||
* `tasksDir` **required**: a file path to the root directory for the issue
 | 
			
		||||
  repository (same as `--tasks-dir` CLI parameter).
 | 
			
		||||
							
								
								
									
										37
									
								
								build.xml
									
									
									
									
									
								
							
							
						
						@@ -1,37 +0,0 @@
 | 
			
		||||
<project name="Personal Issue Tracker" default="package">
 | 
			
		||||
    <property file="version.properties"/>
 | 
			
		||||
    <property environment="env"/>
 | 
			
		||||
 | 
			
		||||
    <property
 | 
			
		||||
        name="libpit.jar"
 | 
			
		||||
        value="libpit/release/pit-${application.version}.jar"/>
 | 
			
		||||
 | 
			
		||||
    <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"/> -->
 | 
			
		||||
    </target>
 | 
			
		||||
 | 
			
		||||
    <target name="libpit">
 | 
			
		||||
        <ant dir="libpit" target="release" inheritAll="false"/>
 | 
			
		||||
    </target>
 | 
			
		||||
 | 
			
		||||
    <target name="pit-cli" depends="libpit">
 | 
			
		||||
        <copy file="${libpit.jar}" todir="pit-cli/lib"/>
 | 
			
		||||
        <ant dir="pit-cli" target="release" inheritAll="false"/>
 | 
			
		||||
    </target>
 | 
			
		||||
 | 
			
		||||
    <!-- <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 name="package" depends="libpit,pit-cli,pit-swing"> -->
 | 
			
		||||
    <target name="package" depends="libpit,pit-cli">
 | 
			
		||||
        <mkdir dir="release/lib"/>
 | 
			
		||||
        <copy file="pit-cli/release/pit-clii-${application.version}.jar" todir="release"/>
 | 
			
		||||
        <!-- <copy file="pit-swing/dist/jar/pit-swing.jar"
 | 
			
		||||
                   tofile="release/pit-swing-${application.version}.jar"/> -->
 | 
			
		||||
        <copy file="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>
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
<project name="Personal Issue Tracker" default="release">
 | 
			
		||||
 | 
			
		||||
    <property file="project.properties"/>
 | 
			
		||||
 | 
			
		||||
    <import file="../jdb-build-1.6.xml"/>
 | 
			
		||||
 | 
			
		||||
    <target name="init">
 | 
			
		||||
        <fail
 | 
			
		||||
            unless="env.GROOVY_HOME"
 | 
			
		||||
            message="GROOVY_HOME environment variable is not set."/>
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
</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
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
#Sun, 11 Dec 2011 21:03:38 -0600
 | 
			
		||||
#Sat Apr 24 17:08:00 CDT 2010
 | 
			
		||||
build.dir=build
 | 
			
		||||
src.dir=src
 | 
			
		||||
lib.shared.dir=../shared-libs
 | 
			
		||||
test.dir=test
 | 
			
		||||
build.number=3
 | 
			
		||||
version=3.2.3
 | 
			
		||||
name=libpit
 | 
			
		||||
lib.dir=lib
 | 
			
		||||
lib.local=true
 | 
			
		||||
release.dir=release
 | 
			
		||||
release.jar=pit-${application.version}.jar
 | 
			
		||||
@@ -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()
 | 
			
		||||
							
								
								
									
										1835
									
								
								libpit/session.vim
									
									
									
									
									
								
							
							
						
						@@ -1,17 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
public enum Category {
 | 
			
		||||
    BUG,
 | 
			
		||||
    FEATURE,
 | 
			
		||||
    TASK
 | 
			
		||||
 | 
			
		||||
    public static Category toCategory(String s) {
 | 
			
		||||
        for(c in Category.values())
 | 
			
		||||
            if (c.name().startsWith(s.toUpperCase())) return c
 | 
			
		||||
        throw new IllegalArgumentException("No category matches ${s}.")
 | 
			
		||||
    }   
 | 
			
		||||
 | 
			
		||||
    public String getSymbol() { toString()[0].toLowerCase() }
 | 
			
		||||
 | 
			
		||||
    public String toString() { return "${name()[0]}${name()[1..-1].toLowerCase()}" }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,37 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
public class FlatProjectView extends Project {
 | 
			
		||||
 | 
			
		||||
    public FlatProjectView(String name) { super(name) }
 | 
			
		||||
 | 
			
		||||
    public Issue createNewIssue(Map options) {
 | 
			
		||||
        throw new UnsupportedOperationException("The FlatProjectView is " +
 | 
			
		||||
            "read-only.")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Project createNewProject(String name) {
 | 
			
		||||
        throw new UnsupportedOperationException("The FlatProjectView is " +
 | 
			
		||||
            "read-only.")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteIssue(Issue issue) { return false }
 | 
			
		||||
    public boolean deleteProject(Project project) { return false }
 | 
			
		||||
 | 
			
		||||
    public boolean delete() { return true }
 | 
			
		||||
 | 
			
		||||
    public void eachIssue(Filter filter = null, Closure closure) {
 | 
			
		||||
        def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
 | 
			
		||||
        def gatherIssues
 | 
			
		||||
        def gatheredIssues = []
 | 
			
		||||
 | 
			
		||||
        gatherIssues = { project, f -> 
 | 
			
		||||
            project.eachIssue(f) { gatheredIssues << it }
 | 
			
		||||
            project.eachProject(f) { gatherIssues(it, f) }
 | 
			
		||||
        }
 | 
			
		||||
        for (p in projects.values()) 
 | 
			
		||||
            if (!filter || filter.accept(p))
 | 
			
		||||
                gatherIssues(p, filter)
 | 
			
		||||
 | 
			
		||||
        gatheredIssues.sort(sorter).each(closure)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
import java.lang.IllegalArgumentException as IAE
 | 
			
		||||
 | 
			
		||||
public class Issue {
 | 
			
		||||
 | 
			
		||||
    protected String id
 | 
			
		||||
    protected Category category
 | 
			
		||||
    protected Status status
 | 
			
		||||
    protected int priority
 | 
			
		||||
    protected String text
 | 
			
		||||
    protected String title
 | 
			
		||||
 | 
			
		||||
    Map extendedProperties = [:]
 | 
			
		||||
 | 
			
		||||
    Issue(Map props) {
 | 
			
		||||
        this.id = props.id
 | 
			
		||||
        this.category = props.category ?: Category.TASK
 | 
			
		||||
        this.status = props.status ?: Status.NEW
 | 
			
		||||
        this.priority = props.priority ?: 5
 | 
			
		||||
        this.title = props.title ?: ''
 | 
			
		||||
        this.text = props.text ?: ''
 | 
			
		||||
 | 
			
		||||
        // Put all the non-native properties into our extendedProperties map.
 | 
			
		||||
        def nativeProps =
 | 
			
		||||
            ["id", "category", "status", "priority", "title", "text"]
 | 
			
		||||
        extendedProperties.putAll(props.findAll {
 | 
			
		||||
            !nativeProps.contains(it.key) })}
 | 
			
		||||
 | 
			
		||||
    public String getId() { return id; }
 | 
			
		||||
 | 
			
		||||
    public Category getCategory() { return category }
 | 
			
		||||
 | 
			
		||||
    public void setCategory(Category c) throws IOException {
 | 
			
		||||
        if (c == null)
 | 
			
		||||
            throw new IAE("Category cannot be null.")
 | 
			
		||||
 | 
			
		||||
        this.category = c
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Status getStatus() { return status }
 | 
			
		||||
 | 
			
		||||
    public void setStatus(Status s) throws IOException {
 | 
			
		||||
        if (s == null)
 | 
			
		||||
            throw new IAE("Status cannot be null.")
 | 
			
		||||
 | 
			
		||||
        this.status = s
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public int getPriority() { return priority }
 | 
			
		||||
 | 
			
		||||
    public void setPriority(int p) throws IOException {
 | 
			
		||||
        priority = Math.min(9, Math.max(0, p))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public String getTitle() { return title }
 | 
			
		||||
 | 
			
		||||
    public void setTitle(String t) throws IOException { title = t }
 | 
			
		||||
 | 
			
		||||
    public String getText() { return text }
 | 
			
		||||
 | 
			
		||||
    public void setText(String t) throws IOException { text = t }
 | 
			
		||||
 | 
			
		||||
    public def propertyMissing(String name) { extendedProperties[name] }
 | 
			
		||||
 | 
			
		||||
    public def propertyMissing(String name, def value) {
 | 
			
		||||
        extendedProperties[name] = value }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String toString() {
 | 
			
		||||
        return "${id}(${priority}-${status}): ${category} ${title}"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,60 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
public abstract class Project {
 | 
			
		||||
 | 
			
		||||
    protected String name
 | 
			
		||||
    Map<String, Issue> issues = [:]
 | 
			
		||||
    Map<String, Project> projects = [:]
 | 
			
		||||
 | 
			
		||||
    Project(String name) { this.name = name }
 | 
			
		||||
 | 
			
		||||
    public void eachIssue(Filter filter = null, Closure c) {
 | 
			
		||||
        def sorter = filter?.issueSorter ?: Filter.defaultIssueSorter
 | 
			
		||||
        for (i in sort(issues.values(), sorter)) 
 | 
			
		||||
            if (!filter || filter.accept(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))
 | 
			
		||||
            if (!filter || filter.accept(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 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) }
 | 
			
		||||
 | 
			
		||||
    public void setName(String name) { this.name = name }
 | 
			
		||||
 | 
			
		||||
    public String getName() { return name }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    String toString() { return name }
 | 
			
		||||
 | 
			
		||||
    public abstract Issue createNewIssue(Map options)
 | 
			
		||||
 | 
			
		||||
    public abstract Project createNewProject(String name)
 | 
			
		||||
 | 
			
		||||
    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) }}}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
public abstract class Repository {
 | 
			
		||||
 | 
			
		||||
    public abstract void persist()
 | 
			
		||||
    public abstract Project[] getRootProjects()
 | 
			
		||||
    public abstract Project createNewProject(String 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,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).") } }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,107 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.file
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
 | 
			
		||||
class FileProject extends Project {
 | 
			
		||||
 | 
			
		||||
    protected File source
 | 
			
		||||
 | 
			
		||||
    public FileProject(File dir) {
 | 
			
		||||
        super(dir.canonicalFile.name)
 | 
			
		||||
 | 
			
		||||
        if (!dir.isDirectory())
 | 
			
		||||
            throw new IllegalArgumentException(
 | 
			
		||||
                "${dir.name} is not a directory.")
 | 
			
		||||
 | 
			
		||||
        this.source = dir
 | 
			
		||||
 | 
			
		||||
        dir.eachFile { child ->
 | 
			
		||||
 | 
			
		||||
            // add sub projects
 | 
			
		||||
            if (child.isDirectory())  {
 | 
			
		||||
                if (child.name ==~ /\d+/ ||
 | 
			
		||||
                    child.isHidden())  return // just an issue folder
 | 
			
		||||
 | 
			
		||||
                // otherwise build and add to list
 | 
			
		||||
                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 } }}
 | 
			
		||||
 | 
			
		||||
    public void setName(String name) {
 | 
			
		||||
        super.setName(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'
 | 
			
		||||
        else {
 | 
			
		||||
            def lastId = (issues.values().max { it.id.toInteger() }).id
 | 
			
		||||
            options.id = (lastId.toInteger() + 1).toString().padLeft(
 | 
			
		||||
                lastId.length(), '0') }
 | 
			
		||||
 | 
			
		||||
        // Create an Issue object from the options (we will discard it later).
 | 
			
		||||
        issue = new Issue(options)
 | 
			
		||||
 | 
			
		||||
        // Create the filename and File object based on the options given.
 | 
			
		||||
        issueFile = new File(source, FileIssue.makeFilename(
 | 
			
		||||
            issue.id, issue.category, issue.status, issue.priority))
 | 
			
		||||
 | 
			
		||||
        // Create the actual file on the system
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
 | 
			
		||||
        // Write the issue to the file created.
 | 
			
		||||
        issueFile.write(FileIssue.formatIssue(issue))
 | 
			
		||||
 | 
			
		||||
        // Read that new file back in as a FileIssue
 | 
			
		||||
        issue = new FileIssue(issueFile)
 | 
			
		||||
 | 
			
		||||
        // Add the issue to our collection.
 | 
			
		||||
        issues[(issue.id)] = issue
 | 
			
		||||
 | 
			
		||||
        return issue }
 | 
			
		||||
 | 
			
		||||
    public FileProject createNewProject(String name) {
 | 
			
		||||
        def newDir = new File(source, name)
 | 
			
		||||
        newDir.mkdirs()
 | 
			
		||||
 | 
			
		||||
        return new FileProject(newDir) }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteIssue(Issue issue) {
 | 
			
		||||
        if (!issues[(issue.id)]) return false
 | 
			
		||||
 | 
			
		||||
        issues.remove(issue.id)
 | 
			
		||||
        if (issue instanceof FileIssue)
 | 
			
		||||
            return issue.deleteFile()
 | 
			
		||||
 | 
			
		||||
        else return true }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteProject(Project project) {
 | 
			
		||||
        if (!projects[(project.name)]) return false
 | 
			
		||||
 | 
			
		||||
        projects.remove(project.name)
 | 
			
		||||
        if (project instanceof FileProject)
 | 
			
		||||
            return project.source.delete()
 | 
			
		||||
 | 
			
		||||
        return true }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public String toString() { return name }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.file
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
 | 
			
		||||
public class FileRepository extends Repository {
 | 
			
		||||
 | 
			
		||||
    @Delegate FileProject fileProject
 | 
			
		||||
 | 
			
		||||
    public FileRepository(File dir) {
 | 
			
		||||
        assert dir.isDirectory()
 | 
			
		||||
        fileProject = new FileProject(dir)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public void persist() {} // nothing to do
 | 
			
		||||
    public Project[] getRootProjects() {
 | 
			
		||||
        return [fileProject] as Project[]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public FileProject createNewProject(String name) {
 | 
			
		||||
        return fileProject.createNewProject()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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(); }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.util
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
 | 
			
		||||
if (args.size() != 1) {
 | 
			
		||||
    println "Usage: Convert1_2 [dir]"
 | 
			
		||||
    System.exit(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
File rootDir = new File(args[0])
 | 
			
		||||
Scanner scan = new Scanner(System.in)
 | 
			
		||||
 | 
			
		||||
rootDir.eachFileRecurse { file ->
 | 
			
		||||
    def m = file.name =~ /(\d+)([bcft])(\d).*/
 | 
			
		||||
    if (m && file.isFile()) {
 | 
			
		||||
        println m[0][0]
 | 
			
		||||
        def parentFile = file.canonicalFile.parentFile
 | 
			
		||||
        def c
 | 
			
		||||
        def s
 | 
			
		||||
        switch(m[0][2]) {
 | 
			
		||||
            case "c":
 | 
			
		||||
                println file.readLines()[0]
 | 
			
		||||
                print "Issue was closed, was category does it belong in?"
 | 
			
		||||
                c = Category.toCategory(scan.nextLine())
 | 
			
		||||
                s = Status.RESOLVED
 | 
			
		||||
                break
 | 
			
		||||
            default:
 | 
			
		||||
                c = Category.toCategory(m[0][2])
 | 
			
		||||
                s = Status.NEW
 | 
			
		||||
                break
 | 
			
		||||
        }
 | 
			
		||||
        println "${m[0][2]}: ${c}"
 | 
			
		||||
        file.renameTo(new File(parentFile,
 | 
			
		||||
            FileIssue.makeFilename(m[0][1], c, s, m[0][3].toInteger())))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.xml
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
 | 
			
		||||
public class XmlIssue extends Issue {
 | 
			
		||||
 | 
			
		||||
    def issueNode
 | 
			
		||||
    XmlProject project
 | 
			
		||||
    XmlRepository repository
 | 
			
		||||
 | 
			
		||||
    XmlIssue(def issueNode, XmlRepository repository, XmlProject project) {
 | 
			
		||||
        super(issueNode.@id, issueNode.@category ?: Category.TASK,
 | 
			
		||||
            issueNode.@status ?: Status.NEW, issueNode.@priority ?: 9)
 | 
			
		||||
 | 
			
		||||
        this.issueNode = issueNode
 | 
			
		||||
        this.project = project
 | 
			
		||||
        this.repository = repository
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    XmlIssue(String id, Category c = Category.TASK, Status s = Status.NEW,
 | 
			
		||||
    int p = 9, String title, String text, XmlRepository repository,
 | 
			
		||||
    XmlProject project) {
 | 
			
		||||
        super(id, c, s, p)
 | 
			
		||||
 | 
			
		||||
        this.project = project
 | 
			
		||||
        this.repository = repository
 | 
			
		||||
 | 
			
		||||
        // 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])
 | 
			
		||||
 | 
			
		||||
        super.@title = title
 | 
			
		||||
        super.@text = text
 | 
			
		||||
        issueNode.value = text
 | 
			
		||||
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setCategory(Category c) {
 | 
			
		||||
        super.setCategory(c)
 | 
			
		||||
 | 
			
		||||
        issueNode.@category = c.name()
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setStatus(Status s) {
 | 
			
		||||
        super.setStatus(s)
 | 
			
		||||
 | 
			
		||||
        issueNode.@status = s.name()
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setPriority(int p) {
 | 
			
		||||
        super.setPriority(p)
 | 
			
		||||
 | 
			
		||||
        issueNode.@priority = p
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setText(String t) {
 | 
			
		||||
        super.setText(t)
 | 
			
		||||
 | 
			
		||||
        issueNode.value = t
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setTitle(String t) {
 | 
			
		||||
        super.setTitle(t)
 | 
			
		||||
 | 
			
		||||
        issueNode.@title = t
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.xml
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
 | 
			
		||||
public class XmlProject extends Project {
 | 
			
		||||
 | 
			
		||||
    def projectNode
 | 
			
		||||
    XmlRepository repository
 | 
			
		||||
 | 
			
		||||
    XmlProject(def projectNode, XmlRepository repository) {
 | 
			
		||||
        super(projectNode.@name)
 | 
			
		||||
        
 | 
			
		||||
        this.projectNode = projectNode
 | 
			
		||||
        this.repository = repository
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    XmlProject(String name, def parentProject, XmlRepository repository) {
 | 
			
		||||
        super(name)
 | 
			
		||||
 | 
			
		||||
        // Node constructor adds the node to the parent node
 | 
			
		||||
        projectNode = new Node(parentProject.projectNode, "Project",
 | 
			
		||||
            [name: name])
 | 
			
		||||
    
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void setName(String name) {
 | 
			
		||||
        super.setName(name)
 | 
			
		||||
 | 
			
		||||
        projectNode.@name = name
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public XmlIssue createNewIssue(Map options) {
 | 
			
		||||
        if (!options) options = [:]
 | 
			
		||||
        if (!options.category) options.category = Category.TASK
 | 
			
		||||
        if (!options.status) options.status = Status.NEW
 | 
			
		||||
        if (!options.priority) options.priority = 5
 | 
			
		||||
        if (!options.text) options.text = "Default issue title.\n" +
 | 
			
		||||
                                          "====================\n"
 | 
			
		||||
 | 
			
		||||
        String id
 | 
			
		||||
        if (issues.size() == 0) id = "0000"
 | 
			
		||||
        else {
 | 
			
		||||
            id = (issues.values().max { it.id.toInteger() }).id
 | 
			
		||||
            id = (id.toInteger() + 1).toString().padLeft(id.length(), '0')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // XmlIssue constructor will persist XML data
 | 
			
		||||
        issues[(id)] = new XmlIssue(id, options.category, options.status,
 | 
			
		||||
            options.priority, options.text, repository, this)
 | 
			
		||||
 | 
			
		||||
        return issues[(id)]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public XmlProject createNewProject(String name) {
 | 
			
		||||
        // XmlProject constructor persists the XML data
 | 
			
		||||
        projects[(name)] = new XmlProject(name, this, repository)
 | 
			
		||||
        return projects[(name)]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteIssue(Issue issue) {
 | 
			
		||||
        if (!issues[(issue.id)]) return false
 | 
			
		||||
 | 
			
		||||
        issues.remove(issue.id)
 | 
			
		||||
        if (issue instanceof XmlIssue)
 | 
			
		||||
            projectNode.remove(issue.issueNode)
 | 
			
		||||
 | 
			
		||||
        repository.persist()
 | 
			
		||||
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteProject(Project project) {
 | 
			
		||||
        if (!projects[(project.name)]) return false
 | 
			
		||||
 | 
			
		||||
        projects.remove(project.name)
 | 
			
		||||
        if (project instanceof XmlProject)
 | 
			
		||||
            projectNode.remove(project.projectNode)
 | 
			
		||||
 | 
			
		||||
        repository.persist()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,47 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.xml
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
import groovy.xml.XmlUtil
 | 
			
		||||
 | 
			
		||||
public class XmlRepository extends Repository {
 | 
			
		||||
 | 
			
		||||
    def repository
 | 
			
		||||
    def projects = []
 | 
			
		||||
    File repoFile
 | 
			
		||||
 | 
			
		||||
    public XmlRepository(File repoFile) {
 | 
			
		||||
 | 
			
		||||
        this.repoFile = repoFile
 | 
			
		||||
        repository = new XmlParser().parse(repoFile)
 | 
			
		||||
 | 
			
		||||
        repository.Project.each { projectNode ->
 | 
			
		||||
            projects << new XmlProject(projectNode)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public synchronized void persist() {
 | 
			
		||||
        repoFile.withOutputStream { XmlUtil.serialize(repository, it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Project[] getRootProjects() {
 | 
			
		||||
        return projects as XmlProject[]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public XmlProject createNewProject(String name) {
 | 
			
		||||
        def newProject = new XmlProject(name, this, null)
 | 
			
		||||
        repository << newProject.projectNode
 | 
			
		||||
 | 
			
		||||
        persist()
 | 
			
		||||
        return newProject
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean deleteProject(Project p) {
 | 
			
		||||
        if (!projects.contains(p)) return false
 | 
			
		||||
 | 
			
		||||
        projects.remove(p)
 | 
			
		||||
        repository.remove(p.projectNode)
 | 
			
		||||
        
 | 
			
		||||
        return true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import static org.junit.Assert.assertEquals
 | 
			
		||||
 | 
			
		||||
import static com.jdbernard.pit.Category.toCategory
 | 
			
		||||
 | 
			
		||||
class CategoryTest {
 | 
			
		||||
 | 
			
		||||
    @Test void testToCategory() {
 | 
			
		||||
 | 
			
		||||
        assertEquals toCategory("BUG"), Category.BUG
 | 
			
		||||
        assertEquals toCategory("FEATURE"), Category.FEATURE
 | 
			
		||||
        assertEquals toCategory("TASK"), Category.TASK
 | 
			
		||||
 | 
			
		||||
        assertEquals toCategory("bug"), Category.BUG
 | 
			
		||||
        assertEquals toCategory("feature"), Category.FEATURE
 | 
			
		||||
        assertEquals toCategory("task"), Category.TASK
 | 
			
		||||
 | 
			
		||||
        assertEquals toCategory("b"), Category.BUG
 | 
			
		||||
        assertEquals toCategory("f"), Category.FEATURE
 | 
			
		||||
        assertEquals toCategory("t"), Category.TASK
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testGetSymbol() {
 | 
			
		||||
 | 
			
		||||
        assertEquals Category.BUG.symbol,       "b"
 | 
			
		||||
        assertEquals Category.FEATURE.symbol,   "f"
 | 
			
		||||
        assertEquals Category.TASK.symbol,      "t"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import org.junit.Before
 | 
			
		||||
import org.junit.After
 | 
			
		||||
 | 
			
		||||
import static org.junit.Assert.assertTrue
 | 
			
		||||
import static org.junit.Assert.assertFalse
 | 
			
		||||
 | 
			
		||||
class FilterTest {
 | 
			
		||||
 | 
			
		||||
    Project proj
 | 
			
		||||
 | 
			
		||||
    @Before void setUpIssues() {
 | 
			
		||||
 | 
			
		||||
        proj = new MockProject('proj1')
 | 
			
		||||
 | 
			
		||||
        def issue = new MockIssue( '0000', Category.TASK, Status.NEW, 5)
 | 
			
		||||
        proj.issues['0000'] = issue
 | 
			
		||||
 | 
			
		||||
        issue = new MockIssue('0001', Category.BUG, Status.REJECTED, 3)
 | 
			
		||||
        proj.issues['0001'] = issue
 | 
			
		||||
 | 
			
		||||
        issue = new MockIssue('0002', Category.BUG, Status.RESOLVED, 9)
 | 
			
		||||
        proj.issues['0002'] = issue
 | 
			
		||||
 | 
			
		||||
        issue = new MockIssue('0003', Category.FEATURE, Status.REASSIGNED, 0)
 | 
			
		||||
        proj.issues['0003'] = issue
 | 
			
		||||
 | 
			
		||||
        def subProj = new MockProject('subproj1')
 | 
			
		||||
        proj.projects['subproj1'] = subProj
 | 
			
		||||
 | 
			
		||||
        subProj = new MockProject('subproj2')
 | 
			
		||||
        proj.projects['subproj2'] = subProj
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testDefaultFilter() {
 | 
			
		||||
        Filter f = new Filter()
 | 
			
		||||
 | 
			
		||||
        proj.issues.values().each { assertTrue f.accept(it) }
 | 
			
		||||
        proj.projects.values().each { assertTrue f.accept(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testPriorityIssueFilter() {
 | 
			
		||||
        Filter f = new Filter(priority: 9)
 | 
			
		||||
        
 | 
			
		||||
        proj.eachIssue { assertTrue f.accept(it) }
 | 
			
		||||
 | 
			
		||||
        f.priority = 6
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0001'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0002'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.priority = 5
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0001'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0002'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.priority = 0
 | 
			
		||||
        assertFalse f.accept(proj.issues['0000'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0001'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0002'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testCategoryFilter() {
 | 
			
		||||
        Filter f = new Filter(categories: 
 | 
			
		||||
            [Category.BUG, Category.FEATURE])
 | 
			
		||||
 | 
			
		||||
        assertFalse f.accept(proj.issues['0000'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0001'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0002'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.categories = [ Category.TASK ]
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0001'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0002'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.categories = [ Category.BUG, Category.TASK ]
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0001'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0002'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testStatusFilter() {
 | 
			
		||||
        Filter f = new Filter(status:
 | 
			
		||||
            [Status.NEW, Status.REASSIGNED, Status.REJECTED])
 | 
			
		||||
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0001'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0002'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.status = [ Status.RESOLVED ]
 | 
			
		||||
        assertFalse f.accept(proj.issues['0000'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0001'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0002'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0003'])
 | 
			
		||||
 | 
			
		||||
        f.status = [ Status.NEW, Status.RESOLVED ]
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0000'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0001'])
 | 
			
		||||
        assertTrue  f.accept(proj.issues['0002'])
 | 
			
		||||
        assertFalse f.accept(proj.issues['0003'])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testProjectFilter() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testAcceptsProjectsFilter() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testCompositeFilter() {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
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])
 | 
			
		||||
    }
 | 
			
		||||
    public boolean delete() { return true }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
class MockProject extends Project {
 | 
			
		||||
 | 
			
		||||
    public MockProject(String name) { super(name) }
 | 
			
		||||
 | 
			
		||||
    public Issue createNewIssue(Map options) {
 | 
			
		||||
        return new MockIssue(options.id ?: 'n/a',
 | 
			
		||||
            options.c ?: Category.TASK, options.s ?: Status.NEW,
 | 
			
		||||
            options.p ?: 5)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public Project createNewProject(String name) {
 | 
			
		||||
        return new MockProject(name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public boolean delete() { return true }
 | 
			
		||||
    public boolean deleteProject(Project project) { return true }
 | 
			
		||||
    public boolean deleteIssue(Issue issue) { return true }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
class MockRepository extends Repository {
 | 
			
		||||
 | 
			
		||||
    public void persist() {}
 | 
			
		||||
 | 
			
		||||
    public Project[] getRootProjects() { return [] as Project[] }
 | 
			
		||||
 | 
			
		||||
    public Project createNewProject(String name) {
 | 
			
		||||
        return new MockProject(name)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit
 | 
			
		||||
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import static org.junit.Assert.assertEquals
 | 
			
		||||
 | 
			
		||||
import static com.jdbernard.pit.Status.toStatus
 | 
			
		||||
 | 
			
		||||
public class StatusTest {
 | 
			
		||||
 | 
			
		||||
    @Test void testToStatus() {
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('REASSIGNED')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('REJECTED')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('NEW')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('RESOLVED')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED,
 | 
			
		||||
            toStatus('VALIDATION_REQUIRED')
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('REA')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('REJ')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('NEW')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('RES')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED,
 | 
			
		||||
            toStatus('VAL')
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('reassigned')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('rejected')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('new')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('resolved')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED,
 | 
			
		||||
            toStatus('validation_required')
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('rea')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('rej')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('new')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('res')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED,
 | 
			
		||||
            toStatus('val')
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('A')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('J')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('N')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('S')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED, toStatus('V')
 | 
			
		||||
 | 
			
		||||
        assertEquals Status.REASSIGNED, toStatus('a')
 | 
			
		||||
        assertEquals Status.REJECTED,   toStatus('j')
 | 
			
		||||
        assertEquals Status.NEW,        toStatus('n')
 | 
			
		||||
        assertEquals Status.RESOLVED  , toStatus('s')
 | 
			
		||||
        assertEquals Status.VALIDATION_REQUIRED, toStatus('v')
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,228 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.file
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
import org.junit.*
 | 
			
		||||
import static org.junit.Assert.assertTrue
 | 
			
		||||
import static org.junit.Assert.assertFalse
 | 
			
		||||
import static org.junit.Assert.assertEquals
 | 
			
		||||
 | 
			
		||||
class FileIssueTest {
 | 
			
		||||
 | 
			
		||||
    def issues
 | 
			
		||||
    File testDir
 | 
			
		||||
 | 
			
		||||
    @Before void makeIssueFiles() {
 | 
			
		||||
        File issueFile
 | 
			
		||||
        issues = []
 | 
			
		||||
 | 
			
		||||
        testDir = new File('testdir')
 | 
			
		||||
        testDir.mkdirs()
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(testDir, '0001fn1.rst')
 | 
			
		||||
        issueFile.write(
 | 
			
		||||
            "Add the killer feature to the killer app.\n" +
 | 
			
		||||
            "=========================================\n\n" +
 | 
			
		||||
            "Make our killer app shine!.")
 | 
			
		||||
        issues << new FileIssue(issueFile)
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(testDir, '0002ts5.rst')
 | 
			
		||||
        issueFile.write(
 | 
			
		||||
            "Obtain donuts.\n" +
 | 
			
		||||
            "==============\n\n" +
 | 
			
		||||
            "The office is seriously lacking in sugary donuts.\n\n" +
 | 
			
		||||
            "We must rectify this at once!")
 | 
			
		||||
        issues << new FileIssue(issueFile)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After void deleteIssueFiles() {
 | 
			
		||||
        assert testDir.deleteDir()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetCategory() {
 | 
			
		||||
        
 | 
			
		||||
        assertEquals issues[0].category, Category.FEATURE
 | 
			
		||||
        assertEquals issues[1].category, Category.TASK
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            issues[0].category = Category.TASK
 | 
			
		||||
            issues[1].category = Category.BUG
 | 
			
		||||
        } catch (Exception e) { 
 | 
			
		||||
            Assert.fail("An unexpected Exception occurred: "
 | 
			
		||||
                + e.getLocalizedMessage())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals issues[0].category, Category.TASK
 | 
			
		||||
        assertEquals issues[1].category, Category.BUG
 | 
			
		||||
 | 
			
		||||
        assertTrue new File(testDir, '0001tn1.rst').exists()
 | 
			
		||||
        assertTrue new File(testDir, '0002bs5.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0001fn1.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0002ts5.rst').exists()
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetCategoryFails() {
 | 
			
		||||
        FileInputStream fin
 | 
			
		||||
        try {
 | 
			
		||||
            // get a lock to the file to prevent the rename
 | 
			
		||||
            def issueFile = new File('0001fn1.rst')
 | 
			
		||||
            fin = new FileInputStream(issueFile)
 | 
			
		||||
 | 
			
		||||
            // try to set the category
 | 
			
		||||
            issues[0].category = Category.TASK
 | 
			
		||||
 | 
			
		||||
            // should throw IOE before here
 | 
			
		||||
            Assert.fail()
 | 
			
		||||
        } catch (IOException ioe) {
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fin != null) fin.close()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetStatus() {
 | 
			
		||||
 | 
			
		||||
        assertEquals issues[0].status, Status.NEW
 | 
			
		||||
        assertEquals issues[1].status, Status.RESOLVED
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            issues[0].status = Status.RESOLVED
 | 
			
		||||
            issues[1].status = Status.REJECTED
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("An unexpected Exception occurred: "
 | 
			
		||||
                + e.getLocalizedMessage())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertTrue new File(testDir, '0001fs1.rst').exists()
 | 
			
		||||
        assertTrue new File(testDir, '0002tj5.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0001fn1.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0002ts5.rst').exists()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetStatusFails() {
 | 
			
		||||
        FileInputStream fin
 | 
			
		||||
        try {
 | 
			
		||||
            // get a lock to the file to prevent the rename
 | 
			
		||||
            def issueFile = new File('0001fn1.rst')
 | 
			
		||||
            fin = new FileInputStream(issueFile)
 | 
			
		||||
 | 
			
		||||
            // try to set the status
 | 
			
		||||
            issues[0].status = Status.REJECTED
 | 
			
		||||
 | 
			
		||||
            // should throw IOE before here
 | 
			
		||||
            Assert.fail()
 | 
			
		||||
        } catch (IOException ioe) {
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fin != null) fin.close()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    @Test void testSetPriority() {
 | 
			
		||||
 | 
			
		||||
        assertEquals issues[0].priority, 1
 | 
			
		||||
        assertEquals issues[1].priority, 5
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            issues[0].priority = 2
 | 
			
		||||
            issues[1].priority = 9
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("An unexpected Exception occurred: "
 | 
			
		||||
                + e.getLocalizedMessage())
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        assertEquals issues[0].priority, 2
 | 
			
		||||
        assertEquals issues[1].priority, 9
 | 
			
		||||
        
 | 
			
		||||
        assertTrue new File(testDir, '0001fn2.rst').exists()
 | 
			
		||||
        assertTrue new File(testDir, '0002ts9.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0001fn1.rst').exists()
 | 
			
		||||
        assertFalse new File(testDir, '0002ts5.rst').exists()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetPriorityFails() {
 | 
			
		||||
        FileInputStream fin
 | 
			
		||||
        try {
 | 
			
		||||
            // get a lock to the file to prevent the rename
 | 
			
		||||
            def issueFile = new File('0001fn1.rst')
 | 
			
		||||
            fin = new FileInputStream(issueFile)
 | 
			
		||||
 | 
			
		||||
            // try to set the priority
 | 
			
		||||
            issues[0].priority = 9
 | 
			
		||||
 | 
			
		||||
            // should throw IOE before here
 | 
			
		||||
            Assert.fail()
 | 
			
		||||
        } catch (IOException ioe) {
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
 | 
			
		||||
        } finally {
 | 
			
		||||
            if (fin != null) fin.close()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testConstruction() {
 | 
			
		||||
        File issueFile = new File(testDir, '0001fn1.rst')
 | 
			
		||||
        Issue issue = new FileIssue(issueFile)
 | 
			
		||||
 | 
			
		||||
        assertEquals issue.id        , "0001"
 | 
			
		||||
        assertEquals issue.category  , Category.FEATURE
 | 
			
		||||
        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.source    , issueFile
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testSetTextFails() {
 | 
			
		||||
        try {
 | 
			
		||||
            // make the issue file un-writable
 | 
			
		||||
            def issueFile = new File('0001fn1.rst')
 | 
			
		||||
            if (issueFile.setReadOnly()) {
 | 
			
		||||
 | 
			
		||||
                // try to write something
 | 
			
		||||
                issues[0].text = "This should fail to be written."
 | 
			
		||||
 | 
			
		||||
                // should throw IOE before here
 | 
			
		||||
                Assert.fail()
 | 
			
		||||
            } else {
 | 
			
		||||
                println "Could not run testSetTextFails, unable to change " +
 | 
			
		||||
                    "the test isseu file's permissions."
 | 
			
		||||
            }
 | 
			
		||||
        } catch (IOException ioe) {
 | 
			
		||||
        } catch (Exception e) {
 | 
			
		||||
            Assert.fail("Unexpected exception: " + e.getLocalizedMessage())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testMakeFilename() {
 | 
			
		||||
        assertEquals FileIssue.makeFilename('0001', Category.BUG,
 | 
			
		||||
            Status.NEW, 5),         '0001bn5.rst'
 | 
			
		||||
        assertEquals FileIssue.makeFilename('0010', Category.FEATURE,
 | 
			
		||||
            Status.REASSIGNED, 1),  '0010fa1.rst'
 | 
			
		||||
        assertEquals FileIssue.makeFilename('0002', Category.FEATURE,
 | 
			
		||||
            Status.REJECTED, 3),    '0002fj3.rst'
 | 
			
		||||
        assertEquals FileIssue.makeFilename('0001', Category.BUG,
 | 
			
		||||
            Status.RESOLVED, -2),   '0001bs0.rst'
 | 
			
		||||
        assertEquals FileIssue.makeFilename('0001', Category.TASK,
 | 
			
		||||
            Status.VALIDATION_REQUIRED, 10)  , '0001tv9.rst'
 | 
			
		||||
        assertEquals FileIssue.makeFilename('00101', Category.BUG,
 | 
			
		||||
            Status.NEW, 5),         '00101bn5.rst'
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            FileIssue.makeFilename('badid', Category.BUG, Status.NEW, 5)
 | 
			
		||||
            assertTrue 'Issue.makeFilename() succeeded with bad id input.', false
 | 
			
		||||
        } catch (IllegalArgumentException iae) {}
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            FileIssue.makeFilename('0002', null, Status.NEW, 5)
 | 
			
		||||
            assertTrue 'Issue.makeFilename() succeeded given no Category.', false
 | 
			
		||||
        } catch (IllegalArgumentException iae) {}
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            FileIssue.makeFilename('0002', Category.BUG, null, 5)
 | 
			
		||||
            assertTrue 'Issue.makeFilename() succeeded given no Status.', false
 | 
			
		||||
        } catch (IllegalArgumentException iae) {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,162 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.file
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
import org.junit.After
 | 
			
		||||
import org.junit.Before
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import static org.junit.Assert.assertEquals
 | 
			
		||||
import static org.junit.Assert.assertFalse
 | 
			
		||||
import static org.junit.Assert.assertNotNull
 | 
			
		||||
import static org.junit.Assert.assertTrue
 | 
			
		||||
 | 
			
		||||
class FileProjectTest {
 | 
			
		||||
 | 
			
		||||
    File testDir
 | 
			
		||||
    Project rootProj
 | 
			
		||||
 | 
			
		||||
    @Before void createTestProjects() {
 | 
			
		||||
 | 
			
		||||
        testDir = new File('testdir')
 | 
			
		||||
        assert !testDir.exists()
 | 
			
		||||
        testDir.mkdirs()
 | 
			
		||||
 | 
			
		||||
        /* TEST SUITE:
 | 
			
		||||
            /testdir/
 | 
			
		||||
                0001t5.rst
 | 
			
		||||
                0002b5.rst
 | 
			
		||||
                0003f2.rst
 | 
			
		||||
 | 
			
		||||
                subproj1/
 | 
			
		||||
                    0001f3.rst
 | 
			
		||||
                    0002b4.rst
 | 
			
		||||
 | 
			
		||||
                emptyproj/
 | 
			
		||||
 | 
			
		||||
        */
 | 
			
		||||
 | 
			
		||||
        def issueFile = new File(testDir, '0001tn5.rst')
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
        issueFile.write('Test Issue 1\n' +
 | 
			
		||||
                        '============\n\n' +
 | 
			
		||||
                        'This is the first test issue.')
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(testDir, '0002ba5.rst')
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
        issueFile.write('Test Bug\n' +
 | 
			
		||||
                        '========\n\n' +
 | 
			
		||||
                        'Yeah, it is a test bug.')
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(testDir, '0003fs2.rst')
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
        issueFile.write('Important Feature Request\n' +
 | 
			
		||||
                        '=========================\n\n' +
 | 
			
		||||
                        'Here is our sweet feature. Please implement it!')
 | 
			
		||||
 | 
			
		||||
        def subDir = new File(testDir, 'subproj1')
 | 
			
		||||
        subDir.mkdirs()
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(subDir, '0001fv3.rst')
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
        issueFile.write('First feature in subproject\n' +
 | 
			
		||||
                        '===========================\n\n' +
 | 
			
		||||
                        'Please make the grubblers grobble.')
 | 
			
		||||
 | 
			
		||||
        issueFile = new File(subDir, '0002bj4.rst')
 | 
			
		||||
        issueFile.createNewFile()
 | 
			
		||||
        issueFile.write('Zippners are not zippning.\n' +
 | 
			
		||||
                        '==========================\n\n' +
 | 
			
		||||
                'For some reason, the Zippners are bilperring, not zippning.')
 | 
			
		||||
 | 
			
		||||
        subDir = new File(testDir, 'emptyproj')
 | 
			
		||||
        subDir.mkdirs()
 | 
			
		||||
 | 
			
		||||
        rootProj = new FileProject(testDir)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @After void deleteTestProjects() {
 | 
			
		||||
        assert testDir.deleteDir()
 | 
			
		||||
 | 
			
		||||
        if (rootProj.source.exists())
 | 
			
		||||
            assert rootProj.source.deleteDir()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testConstruction() {
 | 
			
		||||
         Project proj = new FileProject(testDir)
 | 
			
		||||
 | 
			
		||||
        assertEquals proj.name,             'testdir'
 | 
			
		||||
        assertEquals proj.issues.size(),    3
 | 
			
		||||
        assertEquals proj.projects.size(),  2
 | 
			
		||||
 | 
			
		||||
        // Issue construction in general is under test in IssueTest
 | 
			
		||||
        // just check that the issues actually exists
 | 
			
		||||
        assertEquals proj.issues['0001'].id, '0001'
 | 
			
		||||
        assertEquals proj.issues['0001'].title, 'Test Issue 1'
 | 
			
		||||
 | 
			
		||||
        assertEquals proj.issues['0002'].id, '0002'
 | 
			
		||||
        assertEquals proj.issues['0002'].title, 'Test Bug'
 | 
			
		||||
 | 
			
		||||
        assertEquals proj.issues['0003'].id, '0003'
 | 
			
		||||
        assertEquals proj.issues['0003'].title, 'Important Feature Request'
 | 
			
		||||
 | 
			
		||||
        // check sub-project behaviour
 | 
			
		||||
        assertNotNull proj.projects.subproj1
 | 
			
		||||
        assertEquals proj.projects.subproj1.name,               'subproj1'
 | 
			
		||||
        assertEquals proj.projects.subproj1.issues.size(),      2
 | 
			
		||||
        assertEquals proj.projects.subproj1.projects.size(),    0
 | 
			
		||||
        assertEquals proj.projects.subproj1.issues['0001'].id,  '0001'
 | 
			
		||||
        assertEquals proj.projects.subproj1.issues['0002'].id,  '0002'
 | 
			
		||||
        assertEquals proj.projects.subproj1.issues['0001'].title,
 | 
			
		||||
            'First feature in subproject'
 | 
			
		||||
        assertEquals proj.projects.subproj1.issues['0002'].title,
 | 
			
		||||
            'Zippners are not zippning.'
 | 
			
		||||
 | 
			
		||||
        assertNotNull proj.projects.emptyproj
 | 
			
		||||
        assertEquals proj.projects.emptyproj.issues.size(), 0
 | 
			
		||||
        assertEquals proj.projects.emptyproj.projects.size(), 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testRename() {
 | 
			
		||||
        assert rootProj.name == 'testdir'
 | 
			
		||||
 | 
			
		||||
        rootProj.name = 'renamedTestDir'
 | 
			
		||||
 | 
			
		||||
        assertEquals rootProj.name, 'renamedTestDir'
 | 
			
		||||
        assertTrue new File('renamedTestDir').exists()
 | 
			
		||||
 | 
			
		||||
        assert rootProj.source.deleteDir()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test void testCreateNewIssue() {
 | 
			
		||||
 | 
			
		||||
        // test correct increment of id, application of values
 | 
			
		||||
        def newIssue = rootProj.createNewIssue(category: Category.BUG,
 | 
			
		||||
            status: Status.REASSIGNED, priority: 4,
 | 
			
		||||
            text: 'A newly made bug report.\n'+
 | 
			
		||||
                  '========================\n\n' +
 | 
			
		||||
                  'Testing the Project.createNewIssue() method.')
 | 
			
		||||
 | 
			
		||||
        assertEquals newIssue.id,       '0004'
 | 
			
		||||
        assertEquals newIssue.category, Category.BUG
 | 
			
		||||
        assertEquals newIssue.status,   Status.REASSIGNED
 | 
			
		||||
        assertEquals newIssue.priority, 4
 | 
			
		||||
        assertEquals newIssue.text, 'A newly made bug report.\n'+
 | 
			
		||||
                                    '========================\n\n' +
 | 
			
		||||
                                    'Testing the Project.createNewIssue() method.'
 | 
			
		||||
        assertEquals rootProj.issues[(newIssue.id)], newIssue
 | 
			
		||||
 | 
			
		||||
        //test defaults and creation of issue in an empty project
 | 
			
		||||
        newIssue = rootProj.projects.emptyproj.createNewIssue()
 | 
			
		||||
 | 
			
		||||
        assertEquals newIssue.id,       '0000'
 | 
			
		||||
        assertEquals newIssue.priority, 5
 | 
			
		||||
        assertEquals newIssue.category, Category.TASK
 | 
			
		||||
        assertEquals newIssue.status,   Status.NEW
 | 
			
		||||
        assertEquals newIssue.text,     'Default issue title.\n' +
 | 
			
		||||
                                        '====================\n'
 | 
			
		||||
 | 
			
		||||
        assertEquals rootProj.projects.emptyproj.issues[(newIssue.id)],
 | 
			
		||||
            newIssue
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.xml
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.*
 | 
			
		||||
import groovy.util.Node
 | 
			
		||||
import org.junit.Test
 | 
			
		||||
import static org.junit.Assert.assertEquals
 | 
			
		||||
import static org.junit.Assert.assertFalse
 | 
			
		||||
import static org.junit.Assert.assertTrue
 | 
			
		||||
 | 
			
		||||
public class XmlIssueTest {
 | 
			
		||||
 | 
			
		||||
    Node issueNode = new Node(null, 'Issue',
 | 
			
		||||
        [id: '0000', category: 'BUG', status: 'RESOLVED', priority: 1],
 | 
			
		||||
        'Test Issue')
 | 
			
		||||
 | 
			
		||||
    @Test public void testDummyTest() {}
 | 
			
		||||
 | 
			
		||||
    /*@Test public void testNodeConstructor() {
 | 
			
		||||
        XmlIssue issue = new XmlIssue(issueNode)
 | 
			
		||||
 | 
			
		||||
        assertEquals issue.text, 'Test Issue'
 | 
			
		||||
        assertEquals issue.id, '0000'
 | 
			
		||||
        assertEquals issue.category, Category.BUG
 | 
			
		||||
        assertEquals issue.status, Status.RESOLVED
 | 
			
		||||
        assertEquals issue.priority, 1
 | 
			
		||||
    }*/
 | 
			
		||||
}
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
<project name="Personal Issue Tracker CLI">
 | 
			
		||||
 | 
			
		||||
    <property file="project.properties"/>
 | 
			
		||||
 | 
			
		||||
    <import file="../jdb-build-1.6.xml"/>
 | 
			
		||||
 | 
			
		||||
    <target name="init">
 | 
			
		||||
        <fail
 | 
			
		||||
            unless="env.GROOVY_HOME"
 | 
			
		||||
            message="GROOVY_HOME environment variable is not set."/>
 | 
			
		||||
        <echo message="GROOVY_HOME: ${env.GROOVY_HOME}"/>
 | 
			
		||||
 | 
			
		||||
        <fail message="Could not find PIT ${version} library.">
 | 
			
		||||
            <condition>
 | 
			
		||||
                <not>
 | 
			
		||||
                    <available
 | 
			
		||||
                        file="${basedir}/../libpit/release/libpit-${version}.jar"/>
 | 
			
		||||
                </not>
 | 
			
		||||
            </condition>
 | 
			
		||||
        </fail>
 | 
			
		||||
    </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>
 | 
			
		||||
 | 
			
		||||
    <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"/>
 | 
			
		||||
    </target>
 | 
			
		||||
</project>
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
#Sun, 11 Dec 2011 21:04:03 -0600
 | 
			
		||||
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
 | 
			
		||||
lib.dir=lib
 | 
			
		||||
lib.local=true
 | 
			
		||||
release.dir=release
 | 
			
		||||
release.jar=pit-cli-${application.version}.jar
 | 
			
		||||
main.class=com.jdbernard.pit.PersonalIssueTrackerCLI
 | 
			
		||||
@@ -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 }}
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
#Griffon Metadata file
 | 
			
		||||
#Thu Aug 05 10:29:59 CDT 2010
 | 
			
		||||
app.archetype=default
 | 
			
		||||
app.griffon.version=0.9
 | 
			
		||||
app.name=pit-swing
 | 
			
		||||
app.version=2.5.1
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
application {
 | 
			
		||||
    title = 'PitSwing'
 | 
			
		||||
    startupGroups = ['PIT']
 | 
			
		||||
 | 
			
		||||
    // Should Griffon exit when no Griffon created frames are showing?
 | 
			
		||||
    autoShutdown = true
 | 
			
		||||
 | 
			
		||||
    // If you want some non-standard application class, apply it here
 | 
			
		||||
    //frameClass = 'javax.swing.JFrame'
 | 
			
		||||
}
 | 
			
		||||
mvcGroups {
 | 
			
		||||
    // MVC Group for "ProjectPanel"
 | 
			
		||||
    'ProjectPanel' {
 | 
			
		||||
        model = 'com.jdbernard.pit.swing.ProjectPanelModel'
 | 
			
		||||
        view = 'com.jdbernard.pit.swing.ProjectPanelView'
 | 
			
		||||
        controller = 'com.jdbernard.pit.swing.ProjectPanelController'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MVC Group for "NewIssueDialog"
 | 
			
		||||
    'NewIssueDialog' {
 | 
			
		||||
        model = 'com.jdbernard.pit.swing.NewIssueDialogModel'
 | 
			
		||||
        view = 'com.jdbernard.pit.swing.NewIssueDialogView'
 | 
			
		||||
        controller = 'com.jdbernard.pit.swing.NewIssueDialogController'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // MVC Group for "PIT"
 | 
			
		||||
    'PIT' {
 | 
			
		||||
        model = 'com.jdbernard.pit.swing.PITModel'
 | 
			
		||||
        view = 'com.jdbernard.pit.swing.PITView'
 | 
			
		||||
        controller = 'com.jdbernard.pit.swing.PITController'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,135 +0,0 @@
 | 
			
		||||
// key signing information
 | 
			
		||||
environments {
 | 
			
		||||
    development {
 | 
			
		||||
        signingkey {
 | 
			
		||||
            params {
 | 
			
		||||
                sigfile = 'GRIFFON'
 | 
			
		||||
                keystore = "${basedir}/griffon-app/conf/keys/devKeystore"
 | 
			
		||||
                alias = 'development'
 | 
			
		||||
                storepass = 'BadStorePassword'
 | 
			
		||||
                keypass   = 'BadKeyPassword'
 | 
			
		||||
                lazy      = true // only sign when unsigned
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    test {
 | 
			
		||||
        griffon {
 | 
			
		||||
            jars {
 | 
			
		||||
                sign = false
 | 
			
		||||
                pack = false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    production {
 | 
			
		||||
        signingkey {
 | 
			
		||||
            params {
 | 
			
		||||
                sigfile = 'GRIFFON'
 | 
			
		||||
                keystore = 'CHANGE ME'
 | 
			
		||||
                alias = 'CHANGE ME'
 | 
			
		||||
                // NOTE: for production keys it is more secure to rely on key prompting
 | 
			
		||||
                // no value means we will prompt //storepass = 'BadStorePassword'
 | 
			
		||||
                // no value means we will prompt //keypass   = 'BadKeyPassword'
 | 
			
		||||
                lazy = false // sign, regardless of existing signatures
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        griffon {
 | 
			
		||||
            jars {
 | 
			
		||||
                sign = true
 | 
			
		||||
                pack = true
 | 
			
		||||
                destDir = "${basedir}/staging"
 | 
			
		||||
            }
 | 
			
		||||
            webstart {
 | 
			
		||||
                codebase = 'CHANGE ME'
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
griffon {
 | 
			
		||||
    memory {
 | 
			
		||||
        //max = '64m'
 | 
			
		||||
        //min = '2m'
 | 
			
		||||
        //maxPermSize = '64m'
 | 
			
		||||
    }
 | 
			
		||||
    jars {
 | 
			
		||||
        sign = false
 | 
			
		||||
        pack = false
 | 
			
		||||
        destDir = "${basedir}/staging"
 | 
			
		||||
        jarName = "${appName}.jar"
 | 
			
		||||
    }
 | 
			
		||||
    extensions {
 | 
			
		||||
        jarUrls = []
 | 
			
		||||
        jnlpUrls = []
 | 
			
		||||
        /*
 | 
			
		||||
        props {
 | 
			
		||||
            someProperty = 'someValue'
 | 
			
		||||
        }
 | 
			
		||||
        resources {
 | 
			
		||||
            linux { // windows, macosx, solaris
 | 
			
		||||
                jars = []
 | 
			
		||||
                nativelibs = []
 | 
			
		||||
                props {
 | 
			
		||||
                    someProperty = 'someValue'
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        */
 | 
			
		||||
    }
 | 
			
		||||
    webstart {
 | 
			
		||||
        codebase = "${new File(griffon.jars.destDir).toURI().toASCIIString()}"
 | 
			
		||||
        jnlp = 'application.jnlp'
 | 
			
		||||
    }
 | 
			
		||||
    applet {
 | 
			
		||||
        jnlp = 'applet.jnlp'
 | 
			
		||||
        html = 'applet.html'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// required for custom environments
 | 
			
		||||
signingkey {
 | 
			
		||||
    params {
 | 
			
		||||
        def env = griffon.util.Environment.current.name
 | 
			
		||||
        sigfile = 'GRIFFON-' + env
 | 
			
		||||
        keystore = "${basedir}/griffon-app/conf/keys/${env}Keystore"
 | 
			
		||||
        alias = env
 | 
			
		||||
        // storepass = 'BadStorePassword'
 | 
			
		||||
        // keypass   = 'BadKeyPassword'
 | 
			
		||||
        lazy      = true // only sign when unsigned
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
griffon.project.dependency.resolution = {
 | 
			
		||||
    // inherit Griffon' default dependencies
 | 
			
		||||
    inherits("global") {
 | 
			
		||||
    }
 | 
			
		||||
    log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
 | 
			
		||||
    repositories {
 | 
			
		||||
        griffonPlugins()
 | 
			
		||||
        griffonHome()
 | 
			
		||||
        griffonCentral()
 | 
			
		||||
 | 
			
		||||
        // uncomment the below to enable remote dependency resolution
 | 
			
		||||
        // from public Maven repositories
 | 
			
		||||
        //mavenLocal()
 | 
			
		||||
        //mavenCentral()
 | 
			
		||||
        //mavenRepo "http://snapshots.repository.codehaus.org"
 | 
			
		||||
        //mavenRepo "http://repository.codehaus.org"
 | 
			
		||||
        //mavenRepo "http://download.java.net/maven/2/"
 | 
			
		||||
        //mavenRepo "http://repository.jboss.com/maven2/"
 | 
			
		||||
    }
 | 
			
		||||
    dependencies {
 | 
			
		||||
        // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
 | 
			
		||||
 | 
			
		||||
        // runtime 'mysql:mysql-connector-java:5.1.5'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
griffon {
 | 
			
		||||
    doc {
 | 
			
		||||
        logo = '<a href="http://griffon.codehaus.org" target="_blank"><img alt="The Griffon Framework" src="../img/griffon.png" border="0"/></a>'
 | 
			
		||||
        sponsorLogo = "<br/>"
 | 
			
		||||
        footer = "<br/><br/>Made with Griffon (0.9)"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
root {
 | 
			
		||||
    'groovy.swing.SwingBuilder' {
 | 
			
		||||
        controller = ['Threading']
 | 
			
		||||
        view = '*'
 | 
			
		||||
    }
 | 
			
		||||
    'griffon.app.ApplicationBuilder' {
 | 
			
		||||
        view = '*'
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
// log4j configuration
 | 
			
		||||
log4j {
 | 
			
		||||
    appender.stdout = 'org.apache.log4j.ConsoleAppender'
 | 
			
		||||
    appender.'stdout.layout'='org.apache.log4j.PatternLayout'
 | 
			
		||||
    appender.'stdout.layout.ConversionPattern'='[%r] %c{2} %m%n'
 | 
			
		||||
    appender.errors = 'org.apache.log4j.FileAppender'
 | 
			
		||||
    appender.'errors.layout'='org.apache.log4j.PatternLayout'
 | 
			
		||||
    appender.'errors.layout.ConversionPattern'='[%r] %c{2} %m%n'
 | 
			
		||||
    appender.'errors.File'='stacktrace.log'
 | 
			
		||||
    rootLogger='error,stdout'
 | 
			
		||||
    logger {
 | 
			
		||||
        griffon='error'
 | 
			
		||||
        StackTrace='error,errors'
 | 
			
		||||
        org {
 | 
			
		||||
            codehaus.griffon.commons='info' // core / classloading
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    additivity.StackTrace=false
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
import org.slf4j.LoggerFactory
 | 
			
		||||
 | 
			
		||||
onNewInstance = { klass, type, instance ->
 | 
			
		||||
    instance.metaClass.logger = LoggerFactory.getLogger(klass.name)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
 | 
			
		||||
        "http://www.w3.org/TR/html4/loose.dtd">
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <title></title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
 | 
			
		||||
<script src="http://java.com/js/deployJava.js"></script>
 | 
			
		||||
<script>
 | 
			
		||||
    var attributes = {id: '@griffonAppName@',
 | 
			
		||||
                      codebase:'@griffonAppCodebase@',
 | 
			
		||||
                      code:'@griffonAppletClass@',
 | 
			
		||||
                      archive:'@appletJars@',
 | 
			
		||||
                      width:'@applet.width@', height:'@applet.height@'} ;
 | 
			
		||||
    var parameters = {fontSize:16,
 | 
			
		||||
                      java_arguments: "-Djnlp.packEnabled=true",
 | 
			
		||||
                      jnlp_href:'@griffonAppCodebase@/applet.jnlp',
 | 
			
		||||
                      draggable:'true',
 | 
			
		||||
                      image:'griffon.png',
 | 
			
		||||
                      boxmessage:'Loading @griffonAppName@',
 | 
			
		||||
                      boxbgcolor:'#FFFFFF', boxfgcolor:'#000000',
 | 
			
		||||
                      codebase_lookup: 'false'@applet.script.params@} ;
 | 
			
		||||
    var version = '1.5.0' ;
 | 
			
		||||
    deployJava.runApplet(attributes, parameters, version);
 | 
			
		||||
</script>
 | 
			
		||||
<!--
 | 
			
		||||
<APPLET CODEBASE='@griffonAppCodebase@'
 | 
			
		||||
        CODE='@griffonAppletClass@'
 | 
			
		||||
        ARCHIVE='@appletJars@'
 | 
			
		||||
        WIDTH='@applet.width@' HEIGHT='@applet.height@'>
 | 
			
		||||
    <PARAM NAME="java_arguments" VALUE="-Djnlp.packEnabled=true">
 | 
			
		||||
    <PARAM NAME='jnlp_href' VALUE='@griffonAppCodebase@/applet.jnlp'>
 | 
			
		||||
    <PARAM NAME='dragggable' VALUE='true'>
 | 
			
		||||
    <PARAM NAME='image' VALUE='griffon.png'>
 | 
			
		||||
    <PARAM NAME='boxmessage' VALUE='Loading @griffonAppName@'>
 | 
			
		||||
    <PARAM NAME='boxbgcolor' VALUE='#FFFFFF'>
 | 
			
		||||
    <PARAM NAME='boxfgcolor' VALUE='#000000'>
 | 
			
		||||
    <PARAM NAME='codebase_lookup' VALUE='false'>
 | 
			
		||||
@applet.tag.params@
 | 
			
		||||
</APPLET>
 | 
			
		||||
-->
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!DOCTYPE jnlp SYSTEM "http://java.sun.com/dtd/JNLP-1.5.dtd">
 | 
			
		||||
<jnlp
 | 
			
		||||
    version="@griffonAppVersion@"
 | 
			
		||||
    codebase="@griffonAppCodebase@"
 | 
			
		||||
    href="@jnlpFileName@"
 | 
			
		||||
>
 | 
			
		||||
    <information>
 | 
			
		||||
        <title>@griffonAppName@</title>
 | 
			
		||||
        <vendor>@griffonAppName@</vendor>
 | 
			
		||||
        <!--<homepage href="http://app.example.com/"/>-->
 | 
			
		||||
        <!--fallback description-->
 | 
			
		||||
        <description>@griffonAppName@</description>
 | 
			
		||||
        <description kind="one-line">@griffonAppName@</description>
 | 
			
		||||
        <description kind="short">@griffonAppName@</description>
 | 
			
		||||
        <description kind="tooltip">@griffonAppName@</description>
 | 
			
		||||
        <!-- fallback icon -->
 | 
			
		||||
        <icon href="griffon-icon-48x48.png" kind="default" width="48" height="48"/>
 | 
			
		||||
        <!-- icon used for splash screen -->
 | 
			
		||||
        <icon href="griffon.png" kind="splash" width="381" height="123"/>
 | 
			
		||||
        <!-- icon used in menu -->
 | 
			
		||||
        <icon href="griffon-icon-16x16.png" kind="shortcut" width="16" height="16"/>
 | 
			
		||||
        <!-- icon used on desktop -->
 | 
			
		||||
        <icon href="griffon-icon-32x32.png" kind="shortcut" width="32" height="32"/>
 | 
			
		||||
<!-- to create shortcuts, uncomment this
 | 
			
		||||
        <shortcut online="true">
 | 
			
		||||
            <desktop/>
 | 
			
		||||
            <menu submenu="@griffonAppName@"/>
 | 
			
		||||
        </shortcut>
 | 
			
		||||
-->
 | 
			
		||||
        <offline-allowed/>
 | 
			
		||||
    </information>
 | 
			
		||||
    <security>
 | 
			
		||||
        <all-permissions/>
 | 
			
		||||
        <!--<j2ee-application-client-permissions/>-->
 | 
			
		||||
    </security>
 | 
			
		||||
    <resources>
 | 
			
		||||
        <property name="griffon.runmode" value="applet"/>
 | 
			
		||||
        <property name="jnlp.packEnabled" value="true"/>
 | 
			
		||||
        <j2se version="1.5+" @memoryOptions@/>
 | 
			
		||||
        <!-- auto-added jars follow, griffon-rt, app, and groovy -->
 | 
			
		||||
@jnlpJars@
 | 
			
		||||
        <!-- Add all extra jars below here, or the app may break -->
 | 
			
		||||
@jnlpExtensions@
 | 
			
		||||
@jnlpProperties@
 | 
			
		||||
    </resources>
 | 
			
		||||
@jnlpResources@
 | 
			
		||||
  <applet-desc
 | 
			
		||||
      documentbase="@griffonAppCodebase@"
 | 
			
		||||
      name="@griffonAppName@Applet"
 | 
			
		||||
      main-class="@griffonAppletClass@"
 | 
			
		||||
      width="@applet.width@"
 | 
			
		||||
      height="@applet.height@">
 | 
			
		||||
      <!-- params are ignored when referenced from web page for 6u10 -->
 | 
			
		||||
    <!--<param name="key1" value="value1"/>-->
 | 
			
		||||
    <!--<param name="key2" value="value2"/>-->
 | 
			
		||||
@applet.tag.params@
 | 
			
		||||
  </applet-desc>
 | 
			
		||||
</jnlp>
 | 
			
		||||
@@ -1,54 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!DOCTYPE jnlp SYSTEM "http://java.sun.com/dtd/JNLP-1.5.dtd">
 | 
			
		||||
<jnlp
 | 
			
		||||
    version="@griffonAppVersion@"
 | 
			
		||||
    codebase="@griffonAppCodebase@"
 | 
			
		||||
    href="@jnlpFileName@"
 | 
			
		||||
>
 | 
			
		||||
    <information>
 | 
			
		||||
        <title>@griffonAppName@</title>
 | 
			
		||||
        <vendor>@griffonAppName@</vendor>
 | 
			
		||||
        <!--<homepage href="http://app.example.com/"/>-->
 | 
			
		||||
        <!--fallback description-->
 | 
			
		||||
        <description>@griffonAppName@</description>
 | 
			
		||||
        <description kind="one-line">@griffonAppName@</description>
 | 
			
		||||
        <description kind="short">@griffonAppName@</description>
 | 
			
		||||
        <description kind="tooltip">@griffonAppName@</description>
 | 
			
		||||
        <!-- fallback icon -->
 | 
			
		||||
        <icon href="griffon-icon-48x48.png" kind="default" width="48" height="48"/>
 | 
			
		||||
        <!-- icon used for splash screen -->
 | 
			
		||||
        <icon href="griffon.png" kind="splash" width="381" height="123"/>
 | 
			
		||||
        <!-- icon used in menu -->
 | 
			
		||||
        <icon href="griffon-icon-16x16.png" kind="shortcut" width="16" height="16"/>
 | 
			
		||||
        <!-- icon used on desktop -->
 | 
			
		||||
        <icon href="griffon-icon-32x32.png" kind="shortcut" width="32" height="32"/>
 | 
			
		||||
<!-- to create shortcuts, uncomment this
 | 
			
		||||
        <shortcut online="true">
 | 
			
		||||
            <desktop/>
 | 
			
		||||
            <menu submenu="@griffonAppName@"/>
 | 
			
		||||
        </shortcut>
 | 
			
		||||
        <offline-allowed/>
 | 
			
		||||
-->
 | 
			
		||||
    </information>
 | 
			
		||||
    <security>
 | 
			
		||||
        <all-permissions/>
 | 
			
		||||
        <!--<j2ee-application-client-permissions/>-->
 | 
			
		||||
    </security>
 | 
			
		||||
    <resources>
 | 
			
		||||
        <property name="griffon.runmode" value="webstart"/>
 | 
			
		||||
        <property name="jnlp.packEnabled" value="true"/>
 | 
			
		||||
        <j2se version="1.5+" @memoryOptions@/>
 | 
			
		||||
        <!-- auto-added jars follow, griffon-rt, app, and groovy -->
 | 
			
		||||
@jnlpJars@
 | 
			
		||||
        <!-- Add all extra jars below here, or the app may break -->
 | 
			
		||||
@jnlpExtensions@
 | 
			
		||||
@jnlpProperties@
 | 
			
		||||
    </resources>
 | 
			
		||||
@jnlpResources@
 | 
			
		||||
  <application-desc main-class="@griffonApplicationClass@">
 | 
			
		||||
      <!-- params are ignored when referenced from web page for 6u10 -->
 | 
			
		||||
    <!--<param name="key1" value="value1"/>-->
 | 
			
		||||
    <!--<param name="key2" value="value2"/>-->
 | 
			
		||||
@applet.tag.params@
 | 
			
		||||
  </application-desc>
 | 
			
		||||
</jnlp>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 1.0 KiB  | 
| 
		 Before Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 31 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 KiB  | 
| 
		 Before Width: | Height: | Size: 5.1 KiB  | 
| 
		 Before Width: | Height: | Size: 5.8 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB  | 
@@ -1,26 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Category
 | 
			
		||||
import com.jdbernard.pit.Status
 | 
			
		||||
 | 
			
		||||
class NewIssueDialogController {
 | 
			
		||||
    // these will be injected by Griffon
 | 
			
		||||
    def model
 | 
			
		||||
    def view
 | 
			
		||||
 | 
			
		||||
    void mvcGroupInit(Map args) {
 | 
			
		||||
        // this method is called after model and view are injected
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def show = {
 | 
			
		||||
        view.titleTextField.text = ""
 | 
			
		||||
        model.text = ""
 | 
			
		||||
        view.categoryComboBox.selectedItem = Category.BUG
 | 
			
		||||
        model.category = Category.BUG
 | 
			
		||||
        view.statusComboBox.selectedItem = Status.NEW
 | 
			
		||||
        model.status = Status.NEW
 | 
			
		||||
        view.prioritySpinner.setValue(5)
 | 
			
		||||
        model.priority = 5
 | 
			
		||||
        view.dialog.visible = true
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,172 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Category
 | 
			
		||||
import com.jdbernard.pit.FileProject
 | 
			
		||||
import javax.swing.JFileChooser
 | 
			
		||||
import javax.swing.SwingUtilities
 | 
			
		||||
 | 
			
		||||
class PITController {
 | 
			
		||||
 | 
			
		||||
    // these will be injected by Griffon
 | 
			
		||||
    def model
 | 
			
		||||
    def view
 | 
			
		||||
 | 
			
		||||
    void mvcGroupInit(Map args) {
 | 
			
		||||
 | 
			
		||||
        model.newIssueDialogMVC = buildMVCGroup('NewIssueDialog')
 | 
			
		||||
 | 
			
		||||
        SwingUtilities.invokeAndWait {
 | 
			
		||||
            model.issueListRenderer = new IssueTableCellRenderer()
 | 
			
		||||
 | 
			
		||||
            File pitHome, pitrcFile, pitswingrcFile
 | 
			
		||||
            boolean logDbg = logger.isDebugEnabled()
 | 
			
		||||
            Properties config = new Properties()
 | 
			
		||||
 | 
			
		||||
            // look for config directory
 | 
			
		||||
            pitHome = new File(System.getProperty('user.home'), '.pit')
 | 
			
		||||
            if (logDbg) logger.debug("$pitHome is " +
 | 
			
		||||
                (pitHome.exists() ? '' : 'not ') + "present.")
 | 
			
		||||
 | 
			
		||||
            // look for general config options
 | 
			
		||||
            pitrcFile = new File(pitHome, 'pitrc')
 | 
			
		||||
            if (logDbg) logger.debug("$pitrcFile is " + 
 | 
			
		||||
                (pitrcFile.exists() ? '' : 'not ') + "present.")
 | 
			
		||||
 | 
			
		||||
            // load general config (if present)
 | 
			
		||||
            if (pitrcFile.exists() && pitrcFile.canRead()) {
 | 
			
		||||
                pitrcFile.withInputStream { config.load(it) }
 | 
			
		||||
                if (logDbg) logger.debug("Loaded pitrc")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // look for swing specific config
 | 
			
		||||
            pitswingrcFile = new File(pitHome, 'pitswingrc')
 | 
			
		||||
            if (logDbg) logger.debug("$pitswingrcFile is " + 
 | 
			
		||||
                (pitswingrcFile.exists() ? '' : 'not ') + "present.")
 | 
			
		||||
 | 
			
		||||
            // load swing specific config (if present)
 | 
			
		||||
            if (pitswingrcFile.exists() && pitswingrcFile.canRead()) {
 | 
			
		||||
                pitswingrcFile.withInputStream { config.load(it) }
 | 
			
		||||
                if (logDbg) logger.debug("Loaded pitswingrc")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Process configurable options
 | 
			
		||||
            // ----------------------------
 | 
			
		||||
 | 
			
		||||
            if (logDbg) {
 | 
			
		||||
                logger.debug("Configurable properties:")
 | 
			
		||||
                config.keySet().each { logger.debug(it) }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // add custom category templates
 | 
			
		||||
            Category.values().each { category ->
 | 
			
		||||
                def expectedKey = "issue." + category.name().toLowerCase() +
 | 
			
		||||
                    ".template"
 | 
			
		||||
                if (logDbg) logger.debug("Looking for key: $expectedKey")
 | 
			
		||||
 | 
			
		||||
                config.keySet().each { currentKey ->
 | 
			
		||||
                    if (currentKey == expectedKey)
 | 
			
		||||
                        model.templates[(category)] =
 | 
			
		||||
                            config.getProperty(expectedKey, "")
 | 
			
		||||
                    if (logDbg) logger.debug("Template for category $category: '" +
 | 
			
		||||
                        model.templates[(category)] + "'")
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // load custom issueListRenderer
 | 
			
		||||
            // TODO: not yet supported (maybe no need)
 | 
			
		||||
 | 
			
		||||
            // load initial repositories
 | 
			
		||||
            if (config.containsKey('initial-repositories')) {
 | 
			
		||||
                def initRepos = config.getProperty('initial-repositories', '')
 | 
			
		||||
                initRepos = initRepos.split(/[:;,]/)
 | 
			
		||||
                initRepos.each { repoPath -> loadProject(new File(repoPath)) }
 | 
			
		||||
                if (logDbg) logger.debug("Init repos: '$initRepos'")
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // load custom issue css
 | 
			
		||||
            if (config.containsKey('issue.display.css')) {
 | 
			
		||||
                def issueCSS = config.getProperty('issue.display.css', "")
 | 
			
		||||
 | 
			
		||||
                // look for a file relative to the pit home directory
 | 
			
		||||
                def cssFile
 | 
			
		||||
 | 
			
		||||
                // use short-circuit logic to test several possible locations
 | 
			
		||||
                if ((cssFile = new File(pitHome, issueCSS)).exists() ||
 | 
			
		||||
                    (cssFile = new File(pitHome.parentFile(), issueCSS)).exists() ||
 | 
			
		||||
                    (cssFile = new File(issueCSS)).exists())
 | 
			
		||||
                    issueCSS = cssFile.text
 | 
			
		||||
 | 
			
		||||
                if (logDbg) logger.debug("CSS for issue display: $issueCSS")
 | 
			
		||||
                model.issueCSS = issueCSS
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void refreshIssues() {
 | 
			
		||||
        model.projectPanelMVCs.each { title, mvc ->
 | 
			
		||||
            mvc.controller.refreshIssues()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def openProject = { evt = null ->
 | 
			
		||||
        if (view.openDialog.showOpenDialog(view.frame) !=
 | 
			
		||||
            JFileChooser.APPROVE_OPTIONS) return
 | 
			
		||||
 | 
			
		||||
        loadProject(view.openDialog.selectedFile)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def loadProject = { File projectDir ->
 | 
			
		||||
        def newMVC
 | 
			
		||||
 | 
			
		||||
        // if this is not a valid directory, do nothing
 | 
			
		||||
        // TODO: log to the user that this is not a valid directory
 | 
			
		||||
        if (!projectDir.exists() || !projectDir.isDirectory()) return
 | 
			
		||||
 | 
			
		||||
        // create new ProjectPanel MVC
 | 
			
		||||
        newMVC = buildMVCGroup('ProjectPanel',
 | 
			
		||||
            mainMVC: [model: model, view: view, controller: this],
 | 
			
		||||
            newIssueDialogMVC: model.newIssueDialogMVC,
 | 
			
		||||
            issueCellRenderer: model.issueListRenderer,
 | 
			
		||||
            issueCSS: model.issueCSS,
 | 
			
		||||
            rootProject: new FileProject(projectDir))
 | 
			
		||||
        newMVC.model.id = projectDir.name
 | 
			
		||||
 | 
			
		||||
        // if we already have a tab with this id
 | 
			
		||||
        if (model.projectPanelMVCs[(newMVC.model.id)]) {
 | 
			
		||||
 | 
			
		||||
            // try using the canonical path
 | 
			
		||||
            newMVC.model.id = projectDir.canonicalPath
 | 
			
		||||
 | 
			
		||||
            // still not unique?
 | 
			
		||||
            if (model.projectPanelMVCs[(newMVC.model.id)]) {
 | 
			
		||||
 | 
			
		||||
                // first time this has happened?
 | 
			
		||||
                if (!model.projectIdMap[(newMVC.model.id)])
 | 
			
		||||
                    model.projectIdMap[(newMVC.model.id)] = 0
 | 
			
		||||
 | 
			
		||||
                // no? increment
 | 
			
		||||
                else model.projectIdMap[(newMVC.model.id)] =
 | 
			
		||||
                    model.projectIdMap[(newMVC.model.id)] + 1
 | 
			
		||||
 | 
			
		||||
                // use our new, unique id
 | 
			
		||||
                newMVC.model.id += "-" + model.projectIdMap[(newMVC.model.id)]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        model.projectPanelMVCs[(newMVC.model.id)] = newMVC
 | 
			
		||||
        view.mainTabbedPane.addTab(newMVC.model.id, newMVC.view.panel)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def closeProject = { evt = null ->
 | 
			
		||||
        model.projectPanelMVCs.remove(view.mainTabbedPane.getTitleAt(
 | 
			
		||||
            view.mainTabbedPane.selectedIndex))
 | 
			
		||||
        view.mainTabbedPane.remove(view.mainTabbedPane.selectedComponent)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def shutdown = { evt = null ->
 | 
			
		||||
        app.shutdown()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -1,222 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Category
 | 
			
		||||
import com.jdbernard.pit.FileProject
 | 
			
		||||
import com.jdbernard.pit.FlatProjectView
 | 
			
		||||
import com.jdbernard.pit.Issue
 | 
			
		||||
import com.jdbernard.pit.Project
 | 
			
		||||
import com.jdbernard.pit.Status
 | 
			
		||||
import javax.swing.DefaultListModel
 | 
			
		||||
import javax.swing.JOptionPane
 | 
			
		||||
import javax.swing.tree.DefaultMutableTreeNode
 | 
			
		||||
import javax.swing.tree.DefaultTreeModel
 | 
			
		||||
import org.dom4j.Document
 | 
			
		||||
import org.dom4j.io.OutputFormat
 | 
			
		||||
import org.dom4j.io.XMLWriter
 | 
			
		||||
import org.nuiton.jrst.JRSTGenerator
 | 
			
		||||
import org.nuiton.jrst.JRSTReader
 | 
			
		||||
 | 
			
		||||
class ProjectPanelController {
 | 
			
		||||
    // these will be injected by Griffon
 | 
			
		||||
    def model
 | 
			
		||||
    def view
 | 
			
		||||
 | 
			
		||||
    def jrstReader
 | 
			
		||||
    def jrstGen
 | 
			
		||||
 | 
			
		||||
    static URL rst2htmlXSL =
 | 
			
		||||
        ProjectPanelController.class.getResource("/rst2xhtml.xsl")
 | 
			
		||||
 | 
			
		||||
    void mvcGroupInit(Map args) {
 | 
			
		||||
        jrstReader = new JRSTReader()
 | 
			
		||||
        jrstGen = new JRSTGenerator()
 | 
			
		||||
 | 
			
		||||
        refreshProject()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * displayProject
 | 
			
		||||
     * @param project Project to display
 | 
			
		||||
     */
 | 
			
		||||
    void displayProject(Project project) {
 | 
			
		||||
        if (!project) return
 | 
			
		||||
 | 
			
		||||
        view.issueTextArea.text = ""
 | 
			
		||||
        view.issueTextDisplay.text = ""
 | 
			
		||||
        view.issueTextPanelLayout.show(view.issueTextPanel, 'display')
 | 
			
		||||
 | 
			
		||||
        // build a new IssueTableModel if none cached
 | 
			
		||||
        if (!model.projectTableModels[(project.name)]) {
 | 
			
		||||
            def itm = new IssueTableModel(project,
 | 
			
		||||
                model.filter ?: model.mainMVC.model.filter)
 | 
			
		||||
            itm.categoryIcons = model.mainMVC.model.categoryIcons
 | 
			
		||||
            itm.statusIcons = model.mainMVC.model.statusIcons
 | 
			
		||||
            model.projectTableModels[(project.name)] = itm
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        view.issueTable.setModel(model.projectTableModels[(project.name)])
 | 
			
		||||
 | 
			
		||||
        def tcm = view.issueTable.columnModel
 | 
			
		||||
        tcm.getColumn(0).maxWidth = 24
 | 
			
		||||
        tcm.getColumn(1).maxWidth = 40
 | 
			
		||||
        tcm.getColumn(2).maxWidth = 35
 | 
			
		||||
        if (view.issueTable.model.columnCount == 5)
 | 
			
		||||
            tcm.getColumn(4).maxWidth = 150
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void displayIssue(Issue issue) {
 | 
			
		||||
        if (!issue) return
 | 
			
		||||
 | 
			
		||||
        // hack because binding view.issueTextArea.font to
 | 
			
		||||
        // mainMVC.model.issueDetailFont causes problems
 | 
			
		||||
        if (view.issueTextArea.font != model.mainMVC.model.issueDetailFont)
 | 
			
		||||
            view.issueTextArea.font  = model.mainMVC.model.issueDetailFont
 | 
			
		||||
 | 
			
		||||
        view.issueTextArea.text = issue.text
 | 
			
		||||
        view.issueTextArea.caretPosition = 0
 | 
			
		||||
        view.issueTextDisplay.text = rst2html(issue.text)
 | 
			
		||||
        view.issueTextDisplay.caretPosition = 0
 | 
			
		||||
        view.issueTextPanelLayout.show(view.issueTextPanel, 'display')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void showProejctPopup(Project project, def x, def y) {
 | 
			
		||||
        model.popupProject = project
 | 
			
		||||
        view.projectPopupMenu.show(view.projectTree, x, y)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void showIssuePopup(Issue issue, def x, def y) {
 | 
			
		||||
        model.popupIssue = issue
 | 
			
		||||
        view.issuePopupMenu.show(view.issueTable, x, y)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void refreshProject() {
 | 
			
		||||
        if (model.rootProject) {
 | 
			
		||||
            def rootNode = new DefaultMutableTreeNode()
 | 
			
		||||
            def flatview = new FlatProjectView('All Issues')
 | 
			
		||||
            flatview.projects[(model.rootProject.name)] = model.rootProject
 | 
			
		||||
            rootNode.add(new DefaultMutableTreeNode(flatview))
 | 
			
		||||
            rootNode.add(makeNodes(model.rootProject))
 | 
			
		||||
            view.projectTree.model = new DefaultTreeModel(rootNode)
 | 
			
		||||
        } else {
 | 
			
		||||
            view.projectTree.model = new DefaultTreeModel(
 | 
			
		||||
                new DefaultMutableTreeNode())
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void refreshIssues() {
 | 
			
		||||
        model.projectTableModels.clear()
 | 
			
		||||
        displayProject(model.selectedProject)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def makeNodes(Project project) {
 | 
			
		||||
        def rootNode = new DefaultMutableTreeNode(project)
 | 
			
		||||
        project.eachProject(model.filter ?: model.mainMVC.model.filter)
 | 
			
		||||
            { rootNode.add(makeNodes(it)) }
 | 
			
		||||
        return rootNode
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def newProject = { evt ->
 | 
			
		||||
        def name = JOptionPane.showInputDialog(model.mainMVC.view.frame,
 | 
			
		||||
            'Project name:', 'New Project...', JOptionPane.QUESTION_MESSAGE)
 | 
			
		||||
 | 
			
		||||
        def project
 | 
			
		||||
 | 
			
		||||
        if (evt.source == view.newProjectButton)
 | 
			
		||||
            project = model.selectedProject ?: model.rootProject
 | 
			
		||||
        else project = model.popupProject ?: model.rootProject
 | 
			
		||||
        def newProject = project.createNewProject(name)
 | 
			
		||||
 | 
			
		||||
        project.projects[(newProject.name)] = newProject
 | 
			
		||||
        refreshProject()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def deleteProject = { evt ->
 | 
			
		||||
        def project
 | 
			
		||||
 | 
			
		||||
        if (evt.source == view.deleteProjectButton)
 | 
			
		||||
            project = model.selectedProject ?: model.rootProject
 | 
			
		||||
        else project = model.popupProject ?: model.rootProject
 | 
			
		||||
 | 
			
		||||
        project.delete()
 | 
			
		||||
 | 
			
		||||
        model.rootProject = new FileProject(model.rootProject.source)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def newIssue = { evt = null ->
 | 
			
		||||
        model.newIssueDialogMVC.controller.show()
 | 
			
		||||
        if (model.newIssueDialogMVC.model.accept) {
 | 
			
		||||
            def nidModel = model.newIssueDialogMVC.model
 | 
			
		||||
            def issueText = nidModel.text
 | 
			
		||||
 | 
			
		||||
            if (model.mainMVC.model.templates[(nidModel.category)]) {
 | 
			
		||||
                issueText = model.mainMVC.model.templates[(nidModel.category)]
 | 
			
		||||
                issueText = issueText.replaceFirst(/TITLE/,
 | 
			
		||||
                    nidModel.text)
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            def issue = model.selectedProject.createNewIssue(
 | 
			
		||||
                category: nidModel.category,
 | 
			
		||||
                status: nidModel.status,
 | 
			
		||||
                priority: nidModel.priority,
 | 
			
		||||
                text: issueText)
 | 
			
		||||
            model.projectTableModels[(model.selectedProject.name)] = null
 | 
			
		||||
            displayProject(model.selectedProject)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def deleteIssue = { evt ->
 | 
			
		||||
        def issue
 | 
			
		||||
        if (evt.source == view.deleteIssueButton)
 | 
			
		||||
            issue = getSelectedIssue()
 | 
			
		||||
        else issue = model.popupIssue
 | 
			
		||||
 | 
			
		||||
        model.selectedProject.issues.remove(issue.id)
 | 
			
		||||
        view.issueTable.model.issues.remove(issue)
 | 
			
		||||
 | 
			
		||||
        issue.delete()
 | 
			
		||||
        view.issueTable.invalidate()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def getSelectedIssue() {
 | 
			
		||||
        if (view.issueTable.selectionModel.isSelectionEmpty())
 | 
			
		||||
            return null
 | 
			
		||||
 | 
			
		||||
        return view.issueTable.model.issues[view.issueTable.
 | 
			
		||||
            convertRowIndexToModel(view.issueTable.selectedRow)]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    String rst2html(String rst) {
 | 
			
		||||
        Document doc
 | 
			
		||||
        StringWriter outString
 | 
			
		||||
        StringBuilder result = new StringBuilder()
 | 
			
		||||
 | 
			
		||||
        // read the RST in with the RST parser
 | 
			
		||||
        new StringReader(rst).withReader { doc = jrstReader.read(it) }
 | 
			
		||||
 | 
			
		||||
        // transform to XHTML
 | 
			
		||||
        doc = jrstGen.transform(doc, rst2htmlXSL)
 | 
			
		||||
 | 
			
		||||
        // write to the StringWriter
 | 
			
		||||
        outString = new StringWriter()
 | 
			
		||||
        outString.withWriter { new XMLWriter(it, new OutputFormat("", true)).write(doc) }
 | 
			
		||||
 | 
			
		||||
        // java's embeded html is primitive, we need to massage the results
 | 
			
		||||
        outString.toString().eachLine { line ->
 | 
			
		||||
            
 | 
			
		||||
            // remove the XML version and encoding, title element, meta elems
 | 
			
		||||
            if (line =~ /<\?.*\?>/ || line =~ /<meta.*$/ || line =~ /<title.*$/) { return }
 | 
			
		||||
 | 
			
		||||
            // all other elements, remove all class, xmlns attributes
 | 
			
		||||
            def m = (line =~ /(<\S+)(\s*(class|xmlns)=".*"\s*)*(\/?>.*)/)
 | 
			
		||||
            if (m) line = m[0][1] + m[0][4]
 | 
			
		||||
 | 
			
		||||
            result.append(line)
 | 
			
		||||
 | 
			
		||||
            // add in the CSS information to the head
 | 
			
		||||
            if (line =~ /<head>/) result.append('<style type="text/css">' +
 | 
			
		||||
                model.issueCSS + '</style>')
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return result.toString()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This script is executed inside the UI thread, so be sure to  call 
 | 
			
		||||
 * long running code in another thread.
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options
 | 
			
		||||
 * - execOutside { // your code }
 | 
			
		||||
 * - execFuture { // your code }
 | 
			
		||||
 * - Thread.start { // your code }
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options to run code again inside the UI thread
 | 
			
		||||
 * - execAsync { // your code }
 | 
			
		||||
 * - execSync { // your code }
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import groovy.swing.SwingBuilder
 | 
			
		||||
import griffon.util.GriffonPlatformHelper
 | 
			
		||||
import static griffon.util.GriffonApplicationUtils.*
 | 
			
		||||
 | 
			
		||||
GriffonPlatformHelper.tweakForNativePlatform(app)
 | 
			
		||||
SwingBuilder.lookAndFeel('org.pushingpixels.substance.api.skin.SubstanceCremeCoffeeLookAndFeel', 'nimbus', ['metal', [boldFonts: false]])
 | 
			
		||||
 | 
			
		||||
// make config directory
 | 
			
		||||
def confDir = new File(System.getProperty('user.home'), '.pit')
 | 
			
		||||
if (!confDir.exists()) confDir.mkdirs()
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This script is executed inside the UI thread, so be sure to  call
 | 
			
		||||
 * long running code in another thread.
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options
 | 
			
		||||
 * - execOutside { // your code }
 | 
			
		||||
 * - execFuture { // your code }
 | 
			
		||||
 * - Thread.start { // your code }
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options to run code again inside the UI thread
 | 
			
		||||
 * - execAsync { // your code }
 | 
			
		||||
 * - execSync { // your code }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This script is executed inside the UI thread, so be sure to  call
 | 
			
		||||
 * long running code in another thread.
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options
 | 
			
		||||
 * - execOutside { // your code }
 | 
			
		||||
 * - execFuture { // your code }
 | 
			
		||||
 * - Thread.start { // your code }
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options to run code again inside the UI thread
 | 
			
		||||
 * - execAsync { // your code }
 | 
			
		||||
 * - execSync { // your code }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This script is executed inside the UI thread, so be sure to  call
 | 
			
		||||
 * long running code in another thread.
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options
 | 
			
		||||
 * - execOutside { // your code }
 | 
			
		||||
 * - execFuture { // your code }
 | 
			
		||||
 * - Thread.start { // your code }
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options to run code again inside the UI thread
 | 
			
		||||
 * - execAsync { // your code }
 | 
			
		||||
 * - execSync { // your code }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * This script is executed inside the UI thread, so be sure to  call
 | 
			
		||||
 * long running code in another thread.
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options
 | 
			
		||||
 * - execOutside { // your code }
 | 
			
		||||
 * - execFuture { // your code }
 | 
			
		||||
 * - Thread.start { // your code }
 | 
			
		||||
 *
 | 
			
		||||
 * You have the following options to run code again inside the UI thread
 | 
			
		||||
 * - execAsync { // your code }
 | 
			
		||||
 * - execSync { // your code }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Category
 | 
			
		||||
import com.jdbernard.pit.Status
 | 
			
		||||
import groovy.beans.Bindable
 | 
			
		||||
 | 
			
		||||
class NewIssueDialogModel {
 | 
			
		||||
    @Bindable boolean accept
 | 
			
		||||
    String text
 | 
			
		||||
    Category category
 | 
			
		||||
    Status status
 | 
			
		||||
    int priority
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Category
 | 
			
		||||
import com.jdbernard.pit.Filter
 | 
			
		||||
import com.jdbernard.pit.Issue
 | 
			
		||||
import com.jdbernard.pit.Project
 | 
			
		||||
import com.jdbernard.pit.Status
 | 
			
		||||
import groovy.beans.Bindable
 | 
			
		||||
import java.awt.Font
 | 
			
		||||
import javax.swing.ImageIcon
 | 
			
		||||
 | 
			
		||||
class PITModel {
 | 
			
		||||
 | 
			
		||||
    // filter for projects and classes
 | 
			
		||||
    Filter filter = new Filter(categories: [],
 | 
			
		||||
        status: [Status.NEW, Status.VALIDATION_REQUIRED])
 | 
			
		||||
 | 
			
		||||
    def issueListRenderer
 | 
			
		||||
 | 
			
		||||
    // map of category -> issue template
 | 
			
		||||
    Map<Category, String> templates = [:]
 | 
			
		||||
 | 
			
		||||
    String issueCSS = getClass().getResourceAsStream("/default-issue.css").text
 | 
			
		||||
    
 | 
			
		||||
    Map<Category, ImageIcon> categoryIcons = [:]
 | 
			
		||||
    Map<Category, ImageIcon> statusIcons = [:]
 | 
			
		||||
 | 
			
		||||
    def newIssueDialogMVC
 | 
			
		||||
    Map projectPanelMVCs = [:]
 | 
			
		||||
 | 
			
		||||
    Map projectIdMap = [:]
 | 
			
		||||
 | 
			
		||||
    @Bindable Font issueDetailFont = new Font(Font.MONOSPACED, Font.PLAIN, 10)
 | 
			
		||||
}
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
package com.jdbernard.pit.swing
 | 
			
		||||
 | 
			
		||||
import com.jdbernard.pit.Filter
 | 
			
		||||
import com.jdbernard.pit.Issue
 | 
			
		||||
import com.jdbernard.pit.Project
 | 
			
		||||
import groovy.beans.Bindable
 | 
			
		||||
 | 
			
		||||
class ProjectPanelModel {
 | 
			
		||||
 | 
			
		||||
    // other GUI components
 | 
			
		||||
    def mainMVC
 | 
			
		||||
    def newIssueDialogMVC
 | 
			
		||||
 | 
			
		||||
    // data owned by this panel
 | 
			
		||||
    String id
 | 
			
		||||
    @Bindable Project rootProject
 | 
			
		||||
    @Bindable Project popupProject = null
 | 
			
		||||
    @Bindable Project selectedProject = null
 | 
			
		||||
    @Bindable Issue popupIssue = null
 | 
			
		||||
 | 
			
		||||
    String issueCSS = ""
 | 
			
		||||
 | 
			
		||||
    // cache the models
 | 
			
		||||
    def projectTableModels = [:]
 | 
			
		||||
    def issueCellRenderer
 | 
			
		||||
 | 
			
		||||
    // local filter for projects and issues
 | 
			
		||||
    Filter filter
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 733 B  | 
| 
		 Before Width: | Height: | Size: 1.2 KiB  | 
| 
		 Before Width: | Height: | Size: 774 B  | 
| 
		 Before Width: | Height: | Size: 587 B  | 
@@ -1,22 +0,0 @@
 | 
			
		||||
body {
 | 
			
		||||
  font-size: small;
 | 
			
		||||
}
 | 
			
		||||
h1 {
 | 
			
		||||
  font-size: medium;
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
h2 {
 | 
			
		||||
  font-size: small;
 | 
			
		||||
}
 | 
			
		||||
h3 {
 | 
			
		||||
  font-size: small;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
h4 {
 | 
			
		||||
  font-size: small;
 | 
			
		||||
  font-weight: normal;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
table,th,td{
 | 
			
		||||
  border-style: solid;
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 715 B  | 
| 
		 Before Width: | Height: | Size: 670 B  | 
| 
		 Before Width: | Height: | Size: 537 B  | 
| 
		 Before Width: | Height: | Size: 306 B  | 
| 
		 Before Width: | Height: | Size: 411 B  | 
| 
		 Before Width: | Height: | Size: 822 B  | 
@@ -1,10 +0,0 @@
 | 
			
		||||
log4j.rootLogger=DEBUG,stdout,fileout
 | 
			
		||||
 | 
			
		||||
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
 | 
			
		||||
log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout
 | 
			
		||||
 | 
			
		||||
log4j.appender.fileout=org.apache.log4j.FileAppender
 | 
			
		||||
log4j.appender.fileout.file=pit-swing.log
 | 
			
		||||
log4j.appender.fileout.layout=org.apache.log4j.PatternLayout
 | 
			
		||||
log4j.appender.fileout.layout.ConversionPattern=%-5p %C %d{DATE}: %m%n
 | 
			
		||||
log4j.appender.fileout.threshold=INFO
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 146 B  | 
| 
		 Before Width: | Height: | Size: 559 B  | 
| 
		 Before Width: | Height: | Size: 322 B  | 
| 
		 Before Width: | Height: | Size: 221 B  | 
@@ -1,495 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 | 
			
		||||
<xsl:stylesheet version="1.0"
 | 
			
		||||
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 | 
			
		||||
	xmlns="http://www.w3.org/TR/xhtml1/strict">
 | 
			
		||||
 | 
			
		||||
	<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="/document">	
 | 
			
		||||
	<html>
 | 
			
		||||
	  <head>
 | 
			
		||||
	    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 | 
			
		||||
	    <meta name="generator" content="JRST http://maven-site.nuiton.org/jrst" />
 | 
			
		||||
	    <title><xsl:value-of select="title"/></title>
 | 
			
		||||
	  </head>
 | 
			
		||||
		<body>
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</body>
 | 
			
		||||
	</html>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="comment">
 | 
			
		||||
		<xsl:comment>
 | 
			
		||||
            <xsl:text> </xsl:text>
 | 
			
		||||
	  		<xsl:apply-templates/>
 | 
			
		||||
            <xsl:text> </xsl:text>
 | 
			
		||||
	  	</xsl:comment>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="title">
 | 
			
		||||
	   <xsl:if test="name(..)='document'">
 | 
			
		||||
		   <h1 class="mainTitle">
 | 
			
		||||
     		 <xsl:apply-templates/>
 | 
			
		||||
		   </h1>
 | 
			
		||||
	   </xsl:if>
 | 
			
		||||
	   
 | 
			
		||||
		<xsl:if test="not(name(..)='document')">
 | 
			
		||||
			<xsl:element name="h{count(ancestor::section) + 1}">
 | 
			
		||||
				<xsl:attribute name="class">title</xsl:attribute>
 | 
			
		||||
				<xsl:if test="@refid">
 | 
			
		||||
					<a class="toc-backref" href="#{@refid}" id="{../@id}"><xsl:apply-templates/></a>
 | 
			
		||||
				</xsl:if>
 | 
			
		||||
				<xsl:if test="not(@refid)">
 | 
			
		||||
					<xsl:apply-templates/>
 | 
			
		||||
				</xsl:if>
 | 
			
		||||
			</xsl:element>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="subtitle">
 | 
			
		||||
		<xsl:element name="h2">
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</xsl:element>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	<!-- just eat it -->
 | 
			
		||||
	<xsl:template match="substitution_definition">
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="docinfo">
 | 
			
		||||
	  <table class="docinfo" frame="void" rules="none">
 | 
			
		||||
		<col class="docinfo-name" />
 | 
			
		||||
		<col class="docinfo-content" />
 | 
			
		||||
		<tbody valign="top">
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</tbody>
 | 
			
		||||
	  </table>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="organization|address|contact|version|revision|status|date|copyright">
 | 
			
		||||
		<tr>
 | 
			
		||||
			<th class="docinfo-name">
 | 
			
		||||
				<xsl:value-of select="name(.)"/> : 
 | 
			
		||||
			</th>
 | 
			
		||||
			<td class="docinfo-content">
 | 
			
		||||
				 <xsl:apply-templates/>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
			
 | 
			
		||||
	<xsl:template match="author">
 | 
			
		||||
		<xsl:if test="not(../../authors)">
 | 
			
		||||
				<tr>
 | 
			
		||||
					<th class="docinfo-name">
 | 
			
		||||
						<xsl:value-of select="name(.)"/> :
 | 
			
		||||
					</th>
 | 
			
		||||
					<td class="docinfo-content">
 | 
			
		||||
						<xsl:apply-templates/>
 | 
			
		||||
					</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
    	</xsl:if>
 | 
			
		||||
		<xsl:if test="../../authors">
 | 
			
		||||
			<xsl:variable name="num" select="position()"/>
 | 
			
		||||
			<xsl:if test="$num=1">
 | 
			
		||||
				<tr>
 | 
			
		||||
					<th class="docinfo-name">
 | 
			
		||||
						<xsl:value-of select="authors"/>authors :
 | 
			
		||||
					</th>
 | 
			
		||||
					<td class="docinfo-content">
 | 
			
		||||
						<xsl:apply-templates/>
 | 
			
		||||
					</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</xsl:if>
 | 
			
		||||
			<xsl:if test="$num>1">
 | 
			
		||||
				<tr>
 | 
			
		||||
					<th>
 | 
			
		||||
						
 | 
			
		||||
					</th>
 | 
			
		||||
					<td class="docinfo-content">
 | 
			
		||||
						<xsl:apply-templates/>
 | 
			
		||||
					</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</xsl:if>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
		
 | 
			
		||||
	<xsl:template match="transition">
 | 
			
		||||
	  <hr/>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="section">
 | 
			
		||||
	  <a name="{@id}"></a>
 | 
			
		||||
	  <xsl:apply-templates/>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
    <xsl:template match="list_item/paragraph[1] | definition_list_item/*/paragraph[1] | field/*/paragraph[1] | option/*/paragraph[1]">
 | 
			
		||||
            <!--XXX - Unclear how to handle multi-paragraph list items.
 | 
			
		||||
             | Certainly when they're single paragraphs, we don't want them
 | 
			
		||||
             | wrapped in a <P> tag.  This seems to work okay.
 | 
			
		||||
             +-->
 | 
			
		||||
            <xsl:apply-templates/>
 | 
			
		||||
    </xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="paragraph">
 | 
			
		||||
	  <p><xsl:apply-templates/></p>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="reference">
 | 
			
		||||
		<xsl:if test="@refid">
 | 
			
		||||
			<a href="{@refuri}#{@refid}" id="{@id}"><xsl:apply-templates/></a>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
		<xsl:if test="not(@refid)">
 | 
			
		||||
			<a href="{@refuri}" id="{@id}"><xsl:apply-templates/></a>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="emphasis">
 | 
			
		||||
	  <em><xsl:apply-templates/></em>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="strong">
 | 
			
		||||
	  <b><xsl:apply-templates/></b>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="literal">
 | 
			
		||||
		<code><xsl:value-of select="text()"/></code>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="literal_block">
 | 
			
		||||
		<pre class="literal_block"><xsl:value-of select="text()"/></pre>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="bullet_list">
 | 
			
		||||
		<ul><xsl:apply-templates/></ul>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="enumerated_list">
 | 
			
		||||
		<ol>
 | 
			
		||||
		  	<xsl:choose>
 | 
			
		||||
			  	<xsl:when test="@enumtype='arabic'">
 | 
			
		||||
			  		<xsl:attribute name="type">1</xsl:attribute>
 | 
			
		||||
			    </xsl:when>
 | 
			
		||||
				<xsl:when test="@enumtype='loweralpha'">
 | 
			
		||||
					<xsl:attribute name="type">a</xsl:attribute>
 | 
			
		||||
				</xsl:when>
 | 
			
		||||
				<xsl:when test="@enumtype='upperalpha'">
 | 
			
		||||
					<xsl:attribute name="type">A</xsl:attribute>
 | 
			
		||||
				</xsl:when>
 | 
			
		||||
	            <xsl:when test="@enumtype='lowerroman'">
 | 
			
		||||
	                    <xsl:attribute name="type">i</xsl:attribute>
 | 
			
		||||
	            </xsl:when>
 | 
			
		||||
	            <xsl:when test="@enumtype='upperroman'">
 | 
			
		||||
	                    <xsl:attribute name="type">I</xsl:attribute>
 | 
			
		||||
	            </xsl:when>
 | 
			
		||||
            </xsl:choose>
 | 
			
		||||
	        <xsl:copy-of select="@start"/>
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</ol>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="list_item">
 | 
			
		||||
		<li><xsl:apply-templates/></li>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="field_list">
 | 
			
		||||
		<div class="field_list"><xsl:apply-templates/></div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="field">
 | 
			
		||||
 | 
			
		||||
		<xsl:if test="not(../../docinfo)">
 | 
			
		||||
			<div class="field"><xsl:apply-templates/></div>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
		
 | 
			
		||||
		<xsl:if test="../../docinfo">
 | 
			
		||||
			<tr>
 | 
			
		||||
				<th class="docinfo-name">
 | 
			
		||||
					<xsl:value-of select="field_name/text()"/> :
 | 
			
		||||
				</th>
 | 
			
		||||
				<td>
 | 
			
		||||
					<xsl:apply-templates select="field_body/*"/>
 | 
			
		||||
				</td>
 | 
			
		||||
			</tr>
 | 
			
		||||
		</xsl:if>
 | 
			
		||||
		
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="field_name">
 | 
			
		||||
		<span class="field_name"><xsl:apply-templates/></span>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="field_body">
 | 
			
		||||
		<span class="field_body"><xsl:apply-templates/></span>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="definition_list">
 | 
			
		||||
		<dl class="definition_list"><xsl:apply-templates/></dl>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="definition_list_item">
 | 
			
		||||
		<xsl:apply-templates/>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="term">
 | 
			
		||||
		<dt class="term"><xsl:apply-templates/><xsl:call-template name="classifier"/></dt>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template name="classifier">
 | 
			
		||||
	    <xsl:for-each select="../classifier">
 | 
			
		||||
			<span class="classifier"><xsl:apply-templates/></span>
 | 
			
		||||
		</xsl:for-each>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="classifier">
 | 
			
		||||
		<!-- do nothing -->
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="definition">
 | 
			
		||||
		<dd class="definition"><xsl:apply-templates/></dd>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="image">
 | 
			
		||||
                <xsl:choose>
 | 
			
		||||
                    <xsl:when test="(@target) and (@align)">
 | 
			
		||||
                        <div class="align-{@align}" align="{@align}">
 | 
			
		||||
                            <a href="{@target}">
 | 
			
		||||
                                <xsl:call-template name="img" />
 | 
			
		||||
                            </a>
 | 
			
		||||
			</div>
 | 
			
		||||
                    </xsl:when>
 | 
			
		||||
                    <xsl:when test="@target">
 | 
			
		||||
                        <a href="{@target}">
 | 
			
		||||
                            <xsl:call-template name="img" />
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </xsl:when>
 | 
			
		||||
                    <xsl:when test="@align">
 | 
			
		||||
                        <div class="align-{@align}" align="{@align}">
 | 
			
		||||
                            <xsl:call-template name="img" />
 | 
			
		||||
			</div>
 | 
			
		||||
                    </xsl:when>
 | 
			
		||||
                    <xsl:otherwise>
 | 
			
		||||
                        <xsl:call-template name="img" />
 | 
			
		||||
                    </xsl:otherwise>
 | 
			
		||||
                </xsl:choose>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template name="img">
 | 
			
		||||
            <xsl:element name="img">
 | 
			
		||||
                <xsl:attribute name="alt"><xsl:value-of select="@alt"/></xsl:attribute>
 | 
			
		||||
                <xsl:attribute name="src"><xsl:value-of select="@uri"/></xsl:attribute>
 | 
			
		||||
                <xsl:if test="@width"><xsl:attribute name="width"><xsl:value-of select="@width"/></xsl:attribute></xsl:if>
 | 
			
		||||
                <xsl:if test="@height"><xsl:attribute name="height"><xsl:value-of select="@height"/></xsl:attribute></xsl:if>
 | 
			
		||||
                <xsl:apply-templates/>
 | 
			
		||||
            </xsl:element>
 | 
			
		||||
        </xsl:template>
 | 
			
		||||
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="footer">
 | 
			
		||||
		<hr/>
 | 
			
		||||
		<p class="footer"><xsl:apply-templates/></p>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="header">
 | 
			
		||||
		<p class="header"><xsl:apply-templates/></p>
 | 
			
		||||
		<hr/>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<!--
 | 
			
		||||
	 | Table
 | 
			
		||||
	 +-->
 | 
			
		||||
	<xsl:template match="table">
 | 
			
		||||
		<table border="1">
 | 
			
		||||
			<colgroup>
 | 
			
		||||
				<xsl:apply-templates select="tgroup/colspec"/>
 | 
			
		||||
			</colgroup>
 | 
			
		||||
			<xsl:apply-templates select="./tgroup/thead|./tgroup/tbody"/>
 | 
			
		||||
		</table>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="tgroup/colspec">
 | 
			
		||||
		<col width="{@colwidth}%"/>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="row">
 | 
			
		||||
		<tr><xsl:apply-templates/></tr>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="thead">
 | 
			
		||||
		<thead><xsl:apply-templates/></thead>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="thead/row/entry">
 | 
			
		||||
		<th>
 | 
			
		||||
			<xsl:if test="@morecols"><xsl:attribute name="colspan"><xsl:value-of select="@morecols+1"/></xsl:attribute></xsl:if>
 | 
			
		||||
			<xsl:if test="@morerows"><xsl:attribute name="rowspan"><xsl:value-of select="@morerows+1"/></xsl:attribute></xsl:if>
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</th>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="tbody">
 | 
			
		||||
		<tbody><xsl:apply-templates/></tbody>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
 | 
			
		||||
	<xsl:template match="tbody/row/entry">
 | 
			
		||||
		<td>
 | 
			
		||||
			<xsl:if test="@morecols"><xsl:attribute name="colspan"><xsl:value-of select="@morecols+1"/></xsl:attribute></xsl:if>
 | 
			
		||||
			<xsl:if test="@morerows"><xsl:attribute name="rowspan"><xsl:value-of select="@morerows+1"/></xsl:attribute></xsl:if>
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</td>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="admonition">
 | 
			
		||||
		<div class="admonition">
 | 
			
		||||
			<div class="{@class}">
 | 
			
		||||
				<p class="{title}">
 | 
			
		||||
					<xsl:apply-templates select="./title"/>
 | 
			
		||||
				</p>
 | 
			
		||||
				<p class="body">
 | 
			
		||||
					<xsl:apply-templates select="child::*[position()>1]"/>
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="attention|caution|danger|error|hint|important|note|tip|warning">
 | 
			
		||||
		<div class="{name(.)}">
 | 
			
		||||
			<p class="title"><xsl:value-of select="name(.)"/> :</p>
 | 
			
		||||
			<p class="body">
 | 
			
		||||
				<xsl:apply-templates/>
 | 
			
		||||
			</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="block_quote">
 | 
			
		||||
		
 | 
			
		||||
		<blockquote>
 | 
			
		||||
			<xsl:if test="./attribution">
 | 
			
		||||
				<p><xsl:apply-templates select="child::*[position()=1]"/></p>
 | 
			
		||||
				<p class="attribution">
 | 
			
		||||
					<xsl:apply-templates select="./attribution"/>
 | 
			
		||||
				</p>
 | 
			
		||||
			</xsl:if>
 | 
			
		||||
			<xsl:if test="not(./attribution)">
 | 
			
		||||
				<xsl:apply-templates select="child::*"/>
 | 
			
		||||
			</xsl:if>
 | 
			
		||||
		</blockquote>
 | 
			
		||||
		
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="doctest_block">
 | 
			
		||||
		<pre class="doctest_block">
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</pre>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="line_block">
 | 
			
		||||
		<div class="line_block">
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="line">
 | 
			
		||||
		<div class="line">
 | 
			
		||||
			<xsl:apply-templates/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="sidebar">
 | 
			
		||||
		<div class="sidebar">
 | 
			
		||||
			<p class="title">
 | 
			
		||||
				<xsl:apply-templates select="./title"/>
 | 
			
		||||
			</p>
 | 
			
		||||
			<xsl:if test="./subtitle">
 | 
			
		||||
				<p class="subtitle">
 | 
			
		||||
					<xsl:apply-templates select="./subtitle"/>
 | 
			
		||||
				</p>
 | 
			
		||||
			</xsl:if>
 | 
			
		||||
			<xsl:choose>
 | 
			
		||||
				<xsl:when test="./subtitle">
 | 
			
		||||
					<xsl:apply-templates select="child::*[position()>2]"/>
 | 
			
		||||
				</xsl:when>
 | 
			
		||||
				<xsl:otherwise>
 | 
			
		||||
					<xsl:apply-templates select="child::*[position()>1]"/>
 | 
			
		||||
				</xsl:otherwise>
 | 
			
		||||
			</xsl:choose>
 | 
			
		||||
			
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="topic">
 | 
			
		||||
		<div class="topic">
 | 
			
		||||
			<p class="title">
 | 
			
		||||
				<xsl:apply-templates select="./title"/>
 | 
			
		||||
			</p>
 | 
			
		||||
			<xsl:apply-templates select="child::*[position()>1]"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="option_list">
 | 
			
		||||
		<table class="option_list">
 | 
			
		||||
			<col class="option" />
 | 
			
		||||
			<col class="description" />
 | 
			
		||||
			<tbody valign="top">
 | 
			
		||||
				<xsl:apply-templates/>
 | 
			
		||||
			</tbody>
 | 
			
		||||
		</table>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="option_list_item">
 | 
			
		||||
		<tr>
 | 
			
		||||
			<td class="option-group">
 | 
			
		||||
				<kbd>
 | 
			
		||||
					<xsl:apply-templates select="./option_group/option"/>
 | 
			
		||||
				</kbd>
 | 
			
		||||
			</td>
 | 
			
		||||
			<td>
 | 
			
		||||
				<xsl:apply-templates select="./description"/>
 | 
			
		||||
			</td>
 | 
			
		||||
		</tr>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="option">
 | 
			
		||||
		<span class="option">
 | 
			
		||||
			<xsl:value-of select="option_string/text()"/>
 | 
			
		||||
			<xsl:value-of select="./option_argument/@delimiter"/>
 | 
			
		||||
			<xsl:apply-templates select="./option_argument"/>
 | 
			
		||||
		</span>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="option_argument">
 | 
			
		||||
		<var>
 | 
			
		||||
			<xsl:value-of select="text()"/>,
 | 
			
		||||
		</var>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
		
 | 
			
		||||
	<xsl:template match="footnote">
 | 
			
		||||
		<table class="footnote" frame="void" id="{@id}" rules="none">
 | 
			
		||||
			<colgroup>
 | 
			
		||||
				<col class="label"/>
 | 
			
		||||
				<col/>
 | 
			
		||||
			</colgroup>
 | 
			
		||||
			<tbody valign="top">
 | 
			
		||||
				<tr>
 | 
			
		||||
					<td class="label">
 | 
			
		||||
						<a class="backref" href="#{@backrefs}" name="{id}">
 | 
			
		||||
							[<xsl:value-of select="label"/>]
 | 
			
		||||
						</a>
 | 
			
		||||
					</td>
 | 
			
		||||
					<td>
 | 
			
		||||
						<!--
 | 
			
		||||
	 					| <xsl:value-of select="child::*[position()>1]"/>
 | 
			
		||||
						 +-->
 | 
			
		||||
						<xsl:apply-templates select="child::*[position()>1]"/>
 | 
			
		||||
					</td>
 | 
			
		||||
				</tr>
 | 
			
		||||
			</tbody>
 | 
			
		||||
		</table>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
	<xsl:template match="footnote_reference">
 | 
			
		||||
		<a class="footnote_reference" href="#{@refid}" id="{@id}" name="{@id}">
 | 
			
		||||
			[<xsl:value-of select="text()"/>]
 | 
			
		||||
		</a>
 | 
			
		||||
	</xsl:template>
 | 
			
		||||
	
 | 
			
		||||
</xsl:stylesheet>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 688 B  | 
| 
		 Before Width: | Height: | Size: 40 KiB  |