twdeps 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/Guardfile +12 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/Rakefile +10 -0
- data/bin/twdeps +59 -0
- data/examples/party.png +0 -0
- data/lib/twdeps/graph.rb +111 -0
- data/lib/twdeps/priority_mapper.rb +9 -0
- data/lib/twdeps/project.rb +33 -0
- data/lib/twdeps/project_presenter.rb +20 -0
- data/lib/twdeps/tag.rb +48 -0
- data/lib/twdeps/task.rb +52 -0
- data/lib/twdeps/task_mapper.rb +30 -0
- data/lib/twdeps/task_presenter.rb +28 -0
- data/lib/twdeps/task_repository.rb +77 -0
- data/lib/twdeps/version.rb +5 -0
- data/lib/twdeps.rb +22 -0
- data/test/fixtures/no_deps.json +7 -0
- data/test/fixtures/party.json +7 -0
- data/test/fixtures/party2.json +7 -0
- data/test/fixtures/party_taxes.json +10 -0
- data/test/helpers/plain_graph_parser.rb +68 -0
- data/test/integration/test_dependencies.rb +17 -0
- data/test/test_helper.rb +29 -0
- data/test/unit/test_graph.rb +51 -0
- data/test/unit/test_priority_mapper.rb +27 -0
- data/test/unit/test_project.rb +55 -0
- data/test/unit/test_repository.rb +64 -0
- data/test/unit/test_tag.rb +57 -0
- data/test/unit/test_tag_habtm.rb +69 -0
- data/test/unit/test_task.rb +121 -0
- data/twdeps.gemspec +28 -0
- metadata +240 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
language: ruby
|
2
|
+
|
3
|
+
rvm:
|
4
|
+
- 1.9.3
|
5
|
+
|
6
|
+
before_script:
|
7
|
+
- mkdir ~/.task
|
8
|
+
- echo data.location=~/.task > ~/.taskrc
|
9
|
+
- task count
|
10
|
+
|
11
|
+
before_install:
|
12
|
+
- echo | sudo add-apt-repository ppa:ultrafredde/ppa
|
13
|
+
- sudo apt-get update
|
14
|
+
- sudo apt-get install task
|
15
|
+
- sudo apt-get install graphviz
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
guard 'bundler' do
|
2
|
+
watch('Gemfile')
|
3
|
+
watch(/^.+\.gemspec/)
|
4
|
+
end
|
5
|
+
|
6
|
+
guard :test, :test_paths => ['test/unit', 'test/integration'] do
|
7
|
+
watch('lib/twdeps.rb'){"test"}
|
8
|
+
watch(%r{^lib/twdeps/(.+)\.rb$}){|m| "test/unit/test_#{m[1]}.rb"}
|
9
|
+
watch(%r{^test/unit/test_(.+)\.rb$})
|
10
|
+
watch('test/test_helper.rb'){"test"}
|
11
|
+
watch('test/helpers/**/*'){"test"}
|
12
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Nicholas E. Rabenau
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# TaskWarrior Dependency Visualization
|
2
|
+
|
3
|
+
Visualizes dependencies between TaskWarrior tasks.
|
4
|
+
|
5
|
+
[![Build Status](https://secure.travis-ci.org/nerab/twdeps.png?branch=master)](http://travis-ci.org/nerab/twdeps)
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
Given a set of interdependent tasks described in the TaskWarrior [tutorial](http://taskwarrior.org/projects/taskwarrior/wiki/Tutorial2#DEPENDENCIES), the tasks are
|
10
|
+
|
11
|
+
1. Exported from TaskWarrior as JSON, then
|
12
|
+
1. Piped into `twdeps`, and finally
|
13
|
+
1. The output is directed to a PNG file.
|
14
|
+
|
15
|
+
Result:
|
16
|
+
|
17
|
+
![party](/nerab/twdeps/raw/master/examples/party.png)
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
$ gem install twdeps
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
# Create a dependency graph as PNG and pipe it to a file
|
26
|
+
# See [Limitations](Limitations) below for why we need the extra task parms
|
27
|
+
task export rc.json.array=on rc.verbose=nothing | twdeps > deps.png
|
28
|
+
|
29
|
+
# Same but spefify output format
|
30
|
+
task export | twdeps --format svg > deps.svg
|
31
|
+
|
32
|
+
# Create a graph from a previously exported file
|
33
|
+
task export > tasks.json
|
34
|
+
cat tasks.json | twdeps > deps.png
|
35
|
+
|
36
|
+
# Display graph in browser without creating an intermediate file
|
37
|
+
# Requires bcat to be installed
|
38
|
+
task export | twdeps --format svg | bcat
|
39
|
+
|
40
|
+
## Dependencies
|
41
|
+
|
42
|
+
The graph is generated with [ruby-graphviz](https://github.com/glejeune/Ruby-Graphviz), which in turn requires a local [Graphviz](http://graphviz.org/) installation (e.g. `brew install graphviz`).
|
43
|
+
|
44
|
+
[bcat](http://rtomayko.github.com/bcat/) is required for piping into a browser.
|
45
|
+
|
46
|
+
## Limitations
|
47
|
+
|
48
|
+
Due to [two](http://taskwarrior.org/issues/1017) [bugs](http://taskwarrior.org/issues/1013) in JSON export, TaskWarrior 2.0 needs the command line options `rc.json.array=on` and `rc.verbose=nothing`.
|
49
|
+
|
50
|
+
## Contributing
|
51
|
+
|
52
|
+
1. Fork it
|
53
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
54
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
55
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
56
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/bin/twdeps
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.require
|
5
|
+
|
6
|
+
require 'trollop'
|
7
|
+
|
8
|
+
def log(msg)
|
9
|
+
STDERR.puts "#{File.basename($0)}: #{msg}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def die(msg = nil)
|
13
|
+
log(msg) unless msg.nil?
|
14
|
+
exit 1
|
15
|
+
end
|
16
|
+
|
17
|
+
opts = Trollop::options do
|
18
|
+
version "#{File.basename($0)} v#{TaskWarrior::Dependencies::VERSION} (c) 2012 Nicolas E. Rabenau"
|
19
|
+
banner <<-EOS
|
20
|
+
Visualizes dependencies between TaskWarrior tasks.
|
21
|
+
|
22
|
+
Usage:
|
23
|
+
#{File.basename($0)} [options]
|
24
|
+
|
25
|
+
where [options] are:
|
26
|
+
|
27
|
+
EOS
|
28
|
+
opt :format, "Specify output format", :default => 'svg'
|
29
|
+
opt :trace, "Enable trace output", :default => false
|
30
|
+
end
|
31
|
+
|
32
|
+
include TaskWarrior
|
33
|
+
include TaskWarrior::Dependencies
|
34
|
+
|
35
|
+
Trollop::die :format, "must be one of #{Graph.formats.join(', ')}" unless Graph.formats.include?(opts[:format])
|
36
|
+
|
37
|
+
begin
|
38
|
+
repo = Repository.new(ARGF.read)
|
39
|
+
master = Graph.new(File.basename($0)) # TODO Move this to a CommandlinePresenter
|
40
|
+
|
41
|
+
# Add all projects (will add their tasks and dependencies recursively)
|
42
|
+
repo.projects.each do |project|
|
43
|
+
master << project
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add all project-less tasks as toplevel nodes
|
47
|
+
repo.tasks.reject{|t| t.project}.each do |task|
|
48
|
+
master << task
|
49
|
+
end
|
50
|
+
|
51
|
+
puts master.render(opts[:format])
|
52
|
+
rescue
|
53
|
+
if opts[:trace]
|
54
|
+
log($!)
|
55
|
+
$@.each{|line| log(line)}
|
56
|
+
else
|
57
|
+
die($!)
|
58
|
+
end
|
59
|
+
end
|
data/examples/party.png
ADDED
Binary file
|
data/lib/twdeps/graph.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
module TaskWarrior
|
2
|
+
module Dependencies
|
3
|
+
class NullPresenter
|
4
|
+
def attributes
|
5
|
+
{:label => 'Unknown', :fontcolor => 'red'}
|
6
|
+
end
|
7
|
+
|
8
|
+
def id
|
9
|
+
'null'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Builds a dependency graph
|
14
|
+
#
|
15
|
+
# +thing+ is added as node with all of its dependencies. A presenter is used to present the task as node label.
|
16
|
+
# +thing.id.to_s+ is called for the identifier. It must be unique within the graph and all of its dependencies.
|
17
|
+
#
|
18
|
+
# +thing.dependencies(thing)+ is called if +thing+ responds to it. It is expected to return a list
|
19
|
+
# of things the thing depends on. Each thing may have its own dependencies which will be resolved recursively.
|
20
|
+
#
|
21
|
+
# Design influenced by https://github.com/glejeune/Ruby-Graphviz/blob/852ee119e4e9850f682f0a0089285c36ee16280f/bin/gem2gv
|
22
|
+
#
|
23
|
+
class Graph
|
24
|
+
class << self
|
25
|
+
def formats
|
26
|
+
Constants::FORMATS
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Build a new Graph for +thing+
|
32
|
+
# # TODO Accept a presenter that would default to GlobalPresenter with {:rankdir => 'BT'}
|
33
|
+
def initialize(name = :G, attributes = [])
|
34
|
+
@graph = GraphViz::new(name, attributes)
|
35
|
+
@dependencies = []
|
36
|
+
@edges = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def <<(task_or_project)
|
40
|
+
if task_or_project.respond_to?(:dependencies)
|
41
|
+
task = task_or_project
|
42
|
+
nodeA = find_or_create_node(task)
|
43
|
+
create_edges(nodeA, task.dependencies)
|
44
|
+
|
45
|
+
# resolve all dependencies we don't know yet
|
46
|
+
task.dependencies.each do |dependency|
|
47
|
+
unless @dependencies.include?(dependency)
|
48
|
+
@dependencies << dependency
|
49
|
+
self << dependency
|
50
|
+
end
|
51
|
+
end
|
52
|
+
else
|
53
|
+
# it's a project
|
54
|
+
project = task_or_project
|
55
|
+
cluster = Graph.new(presenter(project).id, presenter(project).attributes)
|
56
|
+
|
57
|
+
project.tasks.each do |task|
|
58
|
+
cluster << task
|
59
|
+
end
|
60
|
+
|
61
|
+
# add all nodes and edges from cluster as a subgraph to @graph
|
62
|
+
@graph.add_graph(cluster.graph)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def render(format)
|
67
|
+
@graph.output(format => String)
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
attr_reader :graph
|
72
|
+
|
73
|
+
private
|
74
|
+
def create_edges(nodeA, nodes)
|
75
|
+
nodes.each do |node|
|
76
|
+
nodeB = find_or_create_node(node)
|
77
|
+
create_edge(nodeA, nodeB)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_or_create_node(thing)
|
82
|
+
@graph.get_node(presenter(thing).id) || create_node(thing)
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_node(thing)
|
86
|
+
@graph.add_nodes(presenter(thing).id, presenter(thing).attributes)
|
87
|
+
end
|
88
|
+
|
89
|
+
def create_edge(nodeA, nodeB)
|
90
|
+
edge = [nodeA, nodeB]
|
91
|
+
unless @edges.include?(edge) # GraphViz lacks get_edge, so we need to track existing edges ourselfes
|
92
|
+
@edges << edge
|
93
|
+
@graph.add_edges(nodeA, nodeB)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def presenter(thing)
|
98
|
+
# TODO Will counter-caching the presenters improve performance?
|
99
|
+
if thing.nil?
|
100
|
+
NullPresenter.new
|
101
|
+
else
|
102
|
+
if thing.respond_to?(:dependencies)
|
103
|
+
TaskPresenter.new(thing)
|
104
|
+
else
|
105
|
+
ProjectPresenter.new(thing)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
module TaskWarrior
|
4
|
+
class Project
|
5
|
+
attr_reader :name, :tasks
|
6
|
+
|
7
|
+
include ActiveModel::Validations
|
8
|
+
validates :name, :presence => true
|
9
|
+
validate :name_may_not_contain_spaces
|
10
|
+
|
11
|
+
def initialize(name, tasks = [])
|
12
|
+
@name = name
|
13
|
+
@tasks = tasks
|
14
|
+
@tasks.each{|t| t.project = self}
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(task)
|
18
|
+
@tasks << task
|
19
|
+
task.project = self
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
"Project #{name} (#{@tasks.size} tasks)"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def name_may_not_contain_spaces
|
28
|
+
if !name.blank? and name[/\s/]
|
29
|
+
errors.add(:name, "may not contain spaces")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module TaskWarrior
|
2
|
+
module Dependencies
|
3
|
+
#
|
4
|
+
# Presents a project's attributes suitable for a GraphViz cluster
|
5
|
+
#
|
6
|
+
class ProjectPresenter
|
7
|
+
def initialize(project)
|
8
|
+
@project = project
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes
|
12
|
+
{:label => @project.name}
|
13
|
+
end
|
14
|
+
|
15
|
+
def id
|
16
|
+
"cluster_#{@project.name}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/twdeps/tag.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
module TaskWarrior
|
4
|
+
class Tag
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
include ActiveModel::Validations
|
8
|
+
validates :name, :presence => true
|
9
|
+
validate :name_may_not_contain_spaces
|
10
|
+
|
11
|
+
def initialize(tag_or_name, tasks = [])
|
12
|
+
if tag_or_name.respond_to?(:name)
|
13
|
+
@name = tag_or_name.name
|
14
|
+
@tasks = tag_or_name.tasks
|
15
|
+
else
|
16
|
+
@name = tag_or_name
|
17
|
+
@tasks = []
|
18
|
+
end
|
19
|
+
|
20
|
+
tasks.each{|task|
|
21
|
+
self << task
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(task)
|
26
|
+
@tasks << task unless @tasks.include?(task)
|
27
|
+
end
|
28
|
+
|
29
|
+
def tasks
|
30
|
+
@tasks #.dup
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"Tag: #{name} (#{@tasks.size} tasks)"
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
name == other.name
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def name_may_not_contain_spaces
|
43
|
+
if !name.blank? and name[/\s/]
|
44
|
+
errors.add(:name, "may not contain spaces")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/twdeps/task.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
|
3
|
+
module TaskWarrior
|
4
|
+
class Task
|
5
|
+
attr_accessor :description, :id, :entry, :status, :uuid, :project, :dependencies, :parent, :children, :priority
|
6
|
+
|
7
|
+
include ActiveModel::Validations
|
8
|
+
validates :description, :id, :entry, :status, :uuid, :presence => true
|
9
|
+
|
10
|
+
validates :id, :numericality => { :only_integer => true, :greater_than => 0}
|
11
|
+
|
12
|
+
validates :uuid, :format => {:with => /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/,
|
13
|
+
:message => "'%{value}' does not match the expected format of a UUID"}
|
14
|
+
|
15
|
+
validates :status, :inclusion => {:in => [:pending, :waiting, :complete], :message => "%{value} is not a valid status"}
|
16
|
+
|
17
|
+
validates :priority, :inclusion => {
|
18
|
+
:in => [:high, :medium, :low],
|
19
|
+
:allow_nil => true,
|
20
|
+
:allow_blank => true,
|
21
|
+
:message => "%{value} is not a valid priority"
|
22
|
+
}
|
23
|
+
|
24
|
+
validate :entry_cannot_be_in_the_future
|
25
|
+
|
26
|
+
def initialize(description)
|
27
|
+
@description = description
|
28
|
+
@dependencies = []
|
29
|
+
@children = []
|
30
|
+
@tags = []
|
31
|
+
end
|
32
|
+
|
33
|
+
def tags
|
34
|
+
@tags
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
"Task '#{description}'".tap{|result| result << " <#{uuid}>" if uuid}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def entry_cannot_be_in_the_future
|
43
|
+
begin
|
44
|
+
if !entry.blank? and entry > DateTime.now
|
45
|
+
errors.add(:entry, "can't be in the future")
|
46
|
+
end
|
47
|
+
rescue
|
48
|
+
errors.add(:entry, "must be comparable to DateTime")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module TaskWarrior
|
2
|
+
#
|
3
|
+
# A DataMapper that makes new Tasks from a JSON representation
|
4
|
+
#
|
5
|
+
class TaskMapper
|
6
|
+
class << self
|
7
|
+
def map(json)
|
8
|
+
Task.new(json['description']).tap{|t|
|
9
|
+
t.id = json['id'].to_i
|
10
|
+
t.uuid = json['uuid']
|
11
|
+
t.entry = DateTime.parse(json['entry'])
|
12
|
+
t.status = json['status'].to_sym
|
13
|
+
t.project = json['project']
|
14
|
+
|
15
|
+
if json['depends']
|
16
|
+
if json['depends'].respond_to?(:split)
|
17
|
+
t.dependencies = json['depends'].split(',')
|
18
|
+
else
|
19
|
+
t.dependencies = json['depends']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
t.parent = json['parent'] # Children will be cross-indexed in the repository
|
24
|
+
t.priority = PriorityMapper.map(json['priority'])
|
25
|
+
json['tags'].each{|tag| t.tags << tag} if json['tags']
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module TaskWarrior
|
2
|
+
module Dependencies
|
3
|
+
#
|
4
|
+
# Presents a task's attributes suitable for a GraphViz node
|
5
|
+
#
|
6
|
+
class TaskPresenter
|
7
|
+
def initialize(task)
|
8
|
+
@task = task
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes
|
12
|
+
attrs = {:label => @task.description}
|
13
|
+
attrs.merge!({:tooltip => "Status: #{@task.status}"})
|
14
|
+
|
15
|
+
# TODO Once we see the urgency in the JSON export, we can color-code the nodes
|
16
|
+
# http://taskwarrior.org/issues/973
|
17
|
+
# attrs.merge!({:fillcolor => 'red', :style => 'filled'})
|
18
|
+
|
19
|
+
attrs.merge!({:fontcolor => 'gray', :color => 'gray'}) if :completed == @task.status
|
20
|
+
attrs
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
@task.uuid
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module TaskWarrior
|
4
|
+
class SimpleTag
|
5
|
+
attr_reader :name, :tasks
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
@tasks = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(task)
|
13
|
+
@tasks << task unless @tasks.include?(task)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Repository
|
18
|
+
def initialize(input)
|
19
|
+
@tasks = {}
|
20
|
+
@projects = Hash.new{|hash, key| hash[key] = Project.new(key)}
|
21
|
+
@tags = Hash.new{|hash, key| hash[key] = Tag.new(key)}
|
22
|
+
|
23
|
+
JSON.parse(input).each{|json|
|
24
|
+
task = TaskWarrior::TaskMapper.map(json)
|
25
|
+
@tasks[task.uuid] = task
|
26
|
+
@projects[task.project].tasks << task if task.project
|
27
|
+
|
28
|
+
# Create a new Tag object in @tags that is the value for each tag name
|
29
|
+
task.tags.each{|tag_name| @tags[tag_name] << task}
|
30
|
+
}
|
31
|
+
|
32
|
+
# Replace the uuid of each dependency with the real task
|
33
|
+
@tasks.each_value{|task| task.dependencies.map!{|uuid| @tasks[uuid]}}
|
34
|
+
|
35
|
+
# Replace the project property of each task with a proper Project object carrying a name and all of the project's tasks
|
36
|
+
@tasks.each_value{|task| task.project = @projects[task.project] if task.project}
|
37
|
+
|
38
|
+
# Add child tasks to their parent, but keep them in the global index
|
39
|
+
@tasks.each_value do |task|
|
40
|
+
if task.parent
|
41
|
+
parent = @tasks[task.parent]
|
42
|
+
|
43
|
+
if parent # we know the parent
|
44
|
+
parent.children << task
|
45
|
+
task.parent = parent
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def tasks
|
52
|
+
# Do not expose child tasks directly
|
53
|
+
@tasks.values.reject{|t| t.parent}
|
54
|
+
end
|
55
|
+
|
56
|
+
# direct lookup by uuid
|
57
|
+
def [](uuid)
|
58
|
+
@tasks[uuid]
|
59
|
+
end
|
60
|
+
|
61
|
+
def projects
|
62
|
+
@projects.values
|
63
|
+
end
|
64
|
+
|
65
|
+
def project(name)
|
66
|
+
@projects[name] if @projects.has_key?(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def tags
|
70
|
+
@tags.values
|
71
|
+
end
|
72
|
+
|
73
|
+
def tag(name)
|
74
|
+
@tags[name] if @tags.has_key?(name)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/twdeps.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "twdeps/version"
|
2
|
+
|
3
|
+
require "twdeps/task"
|
4
|
+
require "twdeps/project"
|
5
|
+
require "twdeps/tag"
|
6
|
+
|
7
|
+
require "twdeps/task_mapper"
|
8
|
+
require "twdeps/priority_mapper"
|
9
|
+
require "twdeps/task_repository"
|
10
|
+
|
11
|
+
require "twdeps/graph"
|
12
|
+
require "twdeps/task_presenter"
|
13
|
+
require "twdeps/project_presenter"
|
14
|
+
|
15
|
+
require "graphviz"
|
16
|
+
require "json"
|
17
|
+
|
18
|
+
module TaskWarrior
|
19
|
+
module Dependencies
|
20
|
+
# Your code goes here...
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
[{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
|
2
|
+
{"id":2,"description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
|
3
|
+
{"id":3,"description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
|
4
|
+
{"id":4,"description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
|
5
|
+
{"id":5,"description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
|
6
|
+
{"id":6,"description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
|
7
|
+
]
|
@@ -0,0 +1,7 @@
|
|
1
|
+
[{"id":1,"description":"Select a free weekend in November","entry":"20120629T191421Z","priority":"H","project":"party","status":"pending","uuid":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","annotations":[{"entry":"20120629T191534Z","description":"the 13th looks good"}]},
|
2
|
+
{"id":2,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a","description":"Select and book a venue","entry":"20120629T191634Z","priority":"H","project":"party","status":"pending","uuid":"c992448a-f1ea-4982-8461-47f0705ff509"},
|
3
|
+
{"id":3,"depends":"9f6f3738-1c08-4f45-8eb4-1e90864c7588","description":"Mail invitations","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"3b53178e-d5a4-45e0-afc2-1292db58a59a"},
|
4
|
+
{"id":4,"depends":"6fd0ba4a-ab67-49cd-ac69-64aa999aff8a,c992448a-f1ea-4982-8461-47f0705ff509","description":"Select a caterer","entry":"20120629T191919Z","project":"party","status":"pending","uuid":"c590941b-eb10-4569-bdc9-0e339f79305e"},
|
5
|
+
{"id":5,"depends":"c992448a-f1ea-4982-8461-47f0705ff509","description":"Design invitations","entry":"20120629T191919Z","priority":"H","project":"party","status":"pending","tags":["mall"],"uuid":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a"},
|
6
|
+
{"id":6,"depends":"e5a867b7-0116-457d-ba43-9ac2bee6ad2a","description":"Print invitations","entry":"20120629T191920Z","project":"party","status":"pending","tags":["mall"],"uuid":"9f6f3738-1c08-4f45-8eb4-1e90864c7588"}
|
7
|
+
]
|