An example ClojureScript+Node.js scripting app; A git-backed text-based ticket tracker
JavaScript Clojure Shell
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


Caution - this is a work in progress

I built this app to tease out ClojureScript's scripting story while building a useful application.

Specifically, if you're going to write small apps in CLJS+Node.js - YOU NEED TO USE PROMISES! (See Google Closure's Result lib)

Promises let you escape from callback hell, handle real exceptions, structure your code like you normally would in Clojure, and program with values.

The rationale behind ttt

It seems that the moment an engineering team becomes constrained, all out-of-band tools and processes get dropped.

One of those processes, ticket tracking, conveys a lot of information about the team and project, and serves as a fundamental mode of async communication. Ironically, one of the best times to collect metrics and keep track of everything is during "crunch-time."

ttt is an exploration into the idea that our tools should work automatically (in-band), based on the context and data exhaust that exists within our daily operations.

Not all of the functionality is present - namely releases, resolve, and I'm re-writing search/query with core.logic

This code will most likely not run for you in its current state. It is built against my own custom version of the ClojureScript code base and Closure Lib. However, the single ttt-example file should run for you. You can also see it running in the intro screencast

Here's the idea, git ttt

A simple issue tracking system built into the standard git workflow with git hooks. All extra functionality happens with plugins.

It's a git-backed text-based ticket tracker - git ttt

Simplest usage

To open a ticket

 git ttt -m "This is something we need to do" --type=work --release=0.1 --points=5
 git ttt -m "Here's the description of another tictet" -t work -r 0.1 -p 5

To contribute to a ticket

 git commit -m "Getting closer to solving the first thing [:1]"

To view a ticket's current info

 git ttt :1

To update a ticket

 git ttt :1 -r 0.2

To create a sub (or referencing) ticket

 git ttt -m "One more ticket" --sub=1 -t work -r 0.1 -p 5
 git ttt :3 -s2      # Now this is a sub-ticket for ticket #2

To close a ticket

 git commit -m "Fixed the first thing [closed :1]"
 git commit -m "Fixed the other thing [closed :2 :3]"

To see an overview of your tickets

 git ttt

To see an overview of all your releases

 git ttt st
 git ttt status

To see ALL tickets

 git ttt st -a

To search for tickets

 git ttt 'release-0.[1-2]'                    # Find any ticket that has anything matching the regular expression string
 git ttt '[?x :where [?x :owner "paul"]]'
 git ttt '[?x :owner "paul"]'
 git ttt '[?x :point #"[0-2]H"]'              # High impact tickets with 0-2 effort

Get selective - Select the id, type, and points of every open ticket where I'm the owner

 git ttt '[(?x :id :type :points) :where [?x :owner "" :current-state :open]]'


  • When a release's tickets are all closed, a tag is created. This can be toggled off in the configuration file.
  • Ticket types are configurable in a file, along with the default type
  • Point scales are configurable in a file, along with the default
  • Point scales can be any list. For example Fibonacci effort and project impact together [0L 0M 0H 1L 1M ... 8L 8M 8H]
  • Points internally are captured as unicode strings
  • Ticket states are configurable in a file
  • The config file is in the repo, in EDN format.ttt/tttrc
  • Tickets with contributed work are toggled "in-process" (or whatever state follows open)
  • Tickets can be moved into new states using commit messages: Example commit message [custom-state :5]
  • Tickets can have commits/work attributed to them, just listing them in the commit message: Example commit message [:6 :7]
  • Tickets do not have to belong to a release
  • Tickets that reference a release will result in an error being thrown
  • Tickets are owned by the last person who touches them (unless a pluging changes that behavior)
  • Everything in the data structure is timestamped with Unix Time // UTC
  • Releases are in the data structure (an EDN file, tickets.edn)
  • Releases are created with tickets via the --release*= or -RR option
  • Releases can be merged or made into "subprojects" if their :name points to another release keyword
  • Releases are deleted by editing the data file directly; If a ticket is closed and its release is not there, it is ignored.
  • Releases should be treated like epics
  • All other functionality is added by plugins (the base system is built upon the same foundation as plugins)
  • Plugins are stored in .ttt/plugins/ and toggled in the config file .ttt/.tttrc
  • Plugins are free to register new commands/options
  • Install involves referencing the ttt script in your ~/.gitconfig
  • Alternatively, ttt script can be copied to a repo and used directly (without the git ... prefix)
  • ttt will modify your repo's githooks as needed
  • ttt is initialized with the first use of the git ttt command
  • ttt can be removed from a repo with git ttt destroy

The proposed data structure

    :releases {
        :r0.1 {:name "0.1", :created TIMESTAMP, :completed nil}
        :r0.1-mistake {:name :r0.1 :created TIMESTAMP, :completed nil
    :tickets [
        {:id 3
         :summary "One more ticket"
         :description ""
         :reported-by "email from .gitconfig"
         :owner "email from .gitconfig"
         :closed-by "email from .gitconfig"
         :points "5"
         :type "work"
         :states {:open TIMESTAMP
                  :in-progress TIMESTAMP
                  :closed TIMESTAMP
         :current-state :open
         :history [
             { :commit "git hash"
               :at TIMESTAMP
         :sub 2
         :branch "branch name of the last in-progress commit"
         :release "0.1"

Requirements and installation

ttt needs node to be installed.

To install for ALL git projects, save the ttt file somewhere (preferably on your path) and then:

 git config --global --add alias.root '!pwd -P'
 git config --global --add alias.ttt '!/full/path/to/ttt'

And set your local post-commit git hook to:

 git ttt __core__ process-git `git log -n1 --pretty=format:"%H %ae %s"` 

To test ttt on a single project, just add the root alias, check the ttt file into your repo, and call it directly:

 `./ttt -m "Making a new ticket"`

And set your local post-commit git hook to:

 `git root`/ttt __core__ process-git `git log -n1 --pretty=format:"%H %ae %s"` 

A more advanced post-commit hook can be found in this project. It will automerge and maintain the ticket file for you.


Project, team, and git metrics


Ticket state hooks


Saved searches

TODO - just adds a new map to the data structure {:searches { "email from .gitconfig" {:tag "query", ...}}}

Document/Report generation

TODO - should generate Markdown and HTML

Ticket priorities


Searching through ttt

Searching is powered by core.logic.


Wait... won't there be conflicts all the time?

If you commit early and often, there will rarely be conflicts. ttt encourages you to do the right thing and helps reinforce best practices. If a conflict does happen, just git ttt resolve - this will reorder the conflicting tickets based on timestamps and update all the ticket numbers.

You're also free to fix conflicts by hand (which just involves renumbering the tickets), submit a patch to make resolve better, or write a plugin.

Why are the tickets in a vector and not in a map?

The logical keys for map-based tickets would be :1, :2, :3 - but if you're numerically indexing something, you're better off just using a vector. Internally, tickets know their own position within the list via the :id key.

How does ttt handle concurrent ticket updates?

A lock file is used to ensure that only one edit can happen at a time. When the lock file is present, ttt quits and notifies the user. If the lock file is mistakenly there, you can just rm .tttlock

Why aren't ticket IDs proper keywords?

Keywords cannot start with an integer. Rather than introduce a project prefix to all tickets, the current choice is to make all IDs integers. In the future, this may change (perhaps via a plugin to introduce project-level controls), but all calling semantics will be the same.

Extending ttt; writing plugins

The __core__ API

Quick notes: The plugins can be written in any language, but need to read in from stdin and push to stdout.

ttt itself is the public API: ttt __core__ function-name will return the output of the function


Copyright © 2012 Paul deGrandis

Distributed under the Eclipse Public License, the same as Clojure. Please see the LICENSE_epl.html for details.