twdeps 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](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
|
+

|
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
|
+
]
|