dev_flow 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +200 -0
- data/ROADMAP +15 -0
- data/Rakefile +3 -0
- data/bin/dw +82 -0
- data/dev_flow.gemspec +18 -0
- data/examples/ROADMAP +75 -0
- data/examples/ROADMAP_SPEC +75 -0
- data/examples/members.yml +4 -0
- data/lib/dev_flow/app.rb +252 -0
- data/lib/dev_flow/commands/cleanup.rb +20 -0
- data/lib/dev_flow/commands/close.rb +69 -0
- data/lib/dev_flow/commands/complete.rb +42 -0
- data/lib/dev_flow/commands/info.rb +103 -0
- data/lib/dev_flow/commands/init.rb +87 -0
- data/lib/dev_flow/commands/progress.rb +35 -0
- data/lib/dev_flow/commands/ur.rb +17 -0
- data/lib/dev_flow/girc.rb +161 -0
- data/lib/dev_flow/member.rb +10 -0
- data/lib/dev_flow/roadmap.rb +132 -0
- data/lib/dev_flow/task.rb +174 -0
- data/lib/dev_flow/task_console.rb +46 -0
- data/lib/dev_flow/version.rb +3 -0
- data/lib/dev_flow.rb +26 -0
- data/spec/app_spec.rb +16 -0
- data/spec/roadmap_spec.rb +28 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/task_spec.rb +182 -0
- data/spec/version_spec.rb +50 -0
- data/tags +105 -0
- metadata +119 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
module DevFlow
|
2
|
+
class Girc
|
3
|
+
attr_accessor :git
|
4
|
+
|
5
|
+
def initialize cmd = 'git', v = true
|
6
|
+
@git = cmd
|
7
|
+
@v = v
|
8
|
+
end
|
9
|
+
|
10
|
+
def info msg
|
11
|
+
return unless @v
|
12
|
+
puts "[GITC] #{msg}" if msg.size > 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# general informations
|
16
|
+
# -----------------------
|
17
|
+
# return modified files (without additions/deletions)
|
18
|
+
def modified_files
|
19
|
+
files = Array.new
|
20
|
+
`#{@git} status`.split("\n").each do |line|
|
21
|
+
if /modified\:\s+(?<file_>.+)/ =~ line
|
22
|
+
files << File.expand_path(file_)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
files
|
26
|
+
end
|
27
|
+
|
28
|
+
# return config list as a hash
|
29
|
+
def config
|
30
|
+
h = Hash.new
|
31
|
+
`#{@git} config --list`.split("\n").each do |line|
|
32
|
+
key, value = line.split("=")
|
33
|
+
h[key] = value
|
34
|
+
end
|
35
|
+
h
|
36
|
+
end
|
37
|
+
|
38
|
+
# return the value of user.name in configuration
|
39
|
+
def me
|
40
|
+
config["user.name"] || "?"
|
41
|
+
end
|
42
|
+
|
43
|
+
# all branches include remote branches
|
44
|
+
def branches
|
45
|
+
branch_list = Array.new
|
46
|
+
`#{@git} branch -a`.split("\n").each do |line|
|
47
|
+
line.gsub!('* ', '')
|
48
|
+
line.gsub!(/\s/, '')
|
49
|
+
branch_list << line unless branch_list.include? line
|
50
|
+
end
|
51
|
+
branch_list
|
52
|
+
end
|
53
|
+
|
54
|
+
# the branch currently working on
|
55
|
+
def current_branch
|
56
|
+
`#{@git} branch`.split("\n").each do |line|
|
57
|
+
if /\*/.match line
|
58
|
+
return line.gsub('* ', '')
|
59
|
+
end
|
60
|
+
end
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def remote_list
|
65
|
+
lst = Array.new
|
66
|
+
`#{@git} remote -v`.split("\n").each do |line|
|
67
|
+
rn = line.split(/\s+/)[0]
|
68
|
+
lst << rn unless lst.include? rn
|
69
|
+
end
|
70
|
+
lst
|
71
|
+
end
|
72
|
+
|
73
|
+
# is the working directory has modified file
|
74
|
+
def wd_clean?
|
75
|
+
clean = true
|
76
|
+
`#{@git} status`.split("\n").each do |line|
|
77
|
+
clean = false if /Changes/.match line
|
78
|
+
end
|
79
|
+
clean
|
80
|
+
end
|
81
|
+
|
82
|
+
# whether the current directory is a git working directory
|
83
|
+
def in_git_dir?
|
84
|
+
`#{@git} status` =~ /fatal/ ? false : true
|
85
|
+
end
|
86
|
+
|
87
|
+
# modifications
|
88
|
+
# --------------------
|
89
|
+
|
90
|
+
# pull from the remote use fetch/merge
|
91
|
+
def pull! remote = 'origin'
|
92
|
+
cb = self.current_branch
|
93
|
+
info "Fetch from #{remote}"
|
94
|
+
rslt = `#{@git} fetch #{remote}`
|
95
|
+
raise "fetch failed with message: #{rslt}" unless $?.success?
|
96
|
+
info rslt
|
97
|
+
info `#{@git} merge #{remote}/#{cb}`
|
98
|
+
end
|
99
|
+
|
100
|
+
# create a new branch, if remote set, push it to remote too
|
101
|
+
# then switch to that branch
|
102
|
+
def new_branch! branch, remote=nil
|
103
|
+
raise "You need clean up you working directory" unless wd_clean?
|
104
|
+
raise "Branch #{branch} already exists" if self.branches.include? branch
|
105
|
+
`#{@git} checkout -b #{branch}`
|
106
|
+
`#{@git} push #{remote} #{branch}` if remote
|
107
|
+
end
|
108
|
+
|
109
|
+
# delete a branch
|
110
|
+
def del_branch! branch, remote=nil
|
111
|
+
rslt = `#{@git} branch -d #{branch}`
|
112
|
+
raise "Cat not delete branch #{branch}: #{rslt}" unless $?.success?
|
113
|
+
`#{@git} push #{remote} :#{branch}` if remote
|
114
|
+
end
|
115
|
+
|
116
|
+
def stash!
|
117
|
+
unless wd_clean?
|
118
|
+
info "Save you change to stash"
|
119
|
+
`#{@git} add .`
|
120
|
+
`#{@git} stash`
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def stash_pop!
|
125
|
+
raise "You may clean up you work directroy first before pop out from the stash" unless wd_clean?
|
126
|
+
info "Pop out from you last stash"
|
127
|
+
`#{@git} stash pop`
|
128
|
+
end
|
129
|
+
|
130
|
+
# remote from a specified remote ref
|
131
|
+
def rebase! remote = 'origin', branch = 'develop'
|
132
|
+
cb = self.current_branch
|
133
|
+
stashed = false
|
134
|
+
unless self.wd_clean?
|
135
|
+
self.stash!
|
136
|
+
stashed = true
|
137
|
+
end
|
138
|
+
|
139
|
+
if branch == self.current_branch
|
140
|
+
info "Rebase pull from remote"
|
141
|
+
`#{@git} pull --rebase #{remote} #{branch}`
|
142
|
+
else
|
143
|
+
info "Switch to branch #{branch}"
|
144
|
+
`#{@git} fetch #{remote}`
|
145
|
+
rslt = `#{@git} checkout #{branch}`
|
146
|
+
raise "Checkout failed: #{rslt}" unless $?.success?
|
147
|
+
info "Update (rabase) branch"
|
148
|
+
rslt = `#{@git} pull --rebase #{remote} #{branch}`
|
149
|
+
raise "Rebase pull for #{branch} failed: #{rslt}" unless $?.success?
|
150
|
+
info "Switch back to branch #{cb}"
|
151
|
+
`#{@git} checkout #{cb}`
|
152
|
+
info "Rebase from #{branch}"
|
153
|
+
rslt = `#{@git} rebase #{branch}`
|
154
|
+
raise "Rebase with #{branch} failed: #{rslt}" unless $?.success?
|
155
|
+
end
|
156
|
+
|
157
|
+
self.stash_pop! if stashed
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module DevFlow
|
2
|
+
## a road map represents a list of tasks
|
3
|
+
class RoadMap
|
4
|
+
attr_accessor :file, :config, :tasks,
|
5
|
+
:branch_tasks, # branch name to task hash
|
6
|
+
:ln_tasks, # line number to task hash, used for rewrite
|
7
|
+
:top_tasks # level 1 task list (used for id calculation)
|
8
|
+
|
9
|
+
def initialize file, config
|
10
|
+
@file, @config = file, config
|
11
|
+
@tasks = Array.new
|
12
|
+
@branch_tasks = Hash.new
|
13
|
+
@ln_tasks = Hash.new
|
14
|
+
@top_tasks = Array.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def last_task
|
18
|
+
@tasks.last
|
19
|
+
end
|
20
|
+
|
21
|
+
def title
|
22
|
+
@config[:title]
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse file = nil
|
26
|
+
self.file = file if file
|
27
|
+
fh = File.open(self.file, "r:utf-8")
|
28
|
+
head_part = ""
|
29
|
+
in_header = 0
|
30
|
+
fh.each do |line|
|
31
|
+
if /^\%\s*\-\-\-+/ =~ line
|
32
|
+
in_header += 1
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
# before any task defined, parse line begin with % as head field:
|
37
|
+
if in_header == 1 and @tasks.size == 0
|
38
|
+
head_part += line
|
39
|
+
end
|
40
|
+
|
41
|
+
if /^\s*\[(?<plus_>[\+\s]+)\]\s(?<contents_>.+)/ =~ line
|
42
|
+
if @tasks.size == 0 and head_part.size > 0
|
43
|
+
hhash = YAML.load(head_part)
|
44
|
+
members = @config["members"] || {}
|
45
|
+
members.merge!(hhash["members"]) if hhash["members"]
|
46
|
+
@config = @config.merge hhash
|
47
|
+
@config["members"] = members
|
48
|
+
end
|
49
|
+
line.chomp!
|
50
|
+
task = Task.new(plus_.to_s.count("+"), self.file, $.).parse(contents_, @config)
|
51
|
+
task.validate! # raise for format errors
|
52
|
+
raise "branch name #{task.branch_name} already used on #{self.file}:#{self.branch_tasks[task.branch_name].ln}" if self.branch_tasks[task.branch_name]
|
53
|
+
if task.is_a?(Task)
|
54
|
+
# find perant for the task:
|
55
|
+
parent = self.find_parent task.level
|
56
|
+
if parent
|
57
|
+
parent.children << task
|
58
|
+
task.parent = parent
|
59
|
+
# task.id = (sprintf "%d%d", parent.id, parent.child_number).to_i
|
60
|
+
end
|
61
|
+
@tasks << task
|
62
|
+
@branch_tasks[task.branch_name] = task
|
63
|
+
@ln_tasks[task.ln] = task
|
64
|
+
@top_tasks << task if task.level == 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
fh.close
|
69
|
+
|
70
|
+
# check and set dependencies
|
71
|
+
self.tasks.each do |task|
|
72
|
+
task.dependencie_names.each do |branch|
|
73
|
+
d_task = @branch_tasks[branch]
|
74
|
+
raise "task #{task.branch_name} (#{task.file}:#{task.ln}) has dependency #{branch} not found on the file" unless d_task
|
75
|
+
task.dependencies << d_task
|
76
|
+
end
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
## the last task in row that less than the given level is parent task
|
82
|
+
def find_parent level
|
83
|
+
self.tasks.reverse.each { |t| return t if t.level < level }
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
def rewrite! task_hash
|
88
|
+
# task_hash: {task_line_as_integer => progress_as_integer}
|
89
|
+
task_hash.each do |ln, progress|
|
90
|
+
raise "invalid line number #{ln}" unless ln.to_s =~ /^\d+$/ and ln > 0
|
91
|
+
raise "invalid progress #{progress}" unless progress.to_s =~ /^\d+$/ and progress > 0 and progress <= 100
|
92
|
+
end
|
93
|
+
|
94
|
+
file = self.file
|
95
|
+
tmp_file = self.file + ".tmp"
|
96
|
+
|
97
|
+
# backup the file to tmp_file
|
98
|
+
FileUtils.mv file, tmp_file
|
99
|
+
tfh = File.open(tmp_file, "r:utf-8")
|
100
|
+
wfh = File.open(file, "w:utf-8")
|
101
|
+
|
102
|
+
tfh.each do |line|
|
103
|
+
if task_hash[$.]
|
104
|
+
progress = task_hash[$.]
|
105
|
+
task = @ln_tasks[$.]
|
106
|
+
|
107
|
+
if progress == 100
|
108
|
+
com_date = DateTime.now.strftime("%Y/%m/%d")
|
109
|
+
com_date = DateTime.now.strftime("%m/%d") if DateTime.now.year == @config["year"]
|
110
|
+
progress = com_date
|
111
|
+
end
|
112
|
+
|
113
|
+
new_line = line
|
114
|
+
if /(?<resource_>\@[a-z\@\;]+)(\:[PD\d\/]+)?/ =~ line
|
115
|
+
new_line.gsub!(/(?<resource_>\@[a-z\@\;]+)(\:[PD\d\/]+)?/, resource_ + ":" + progress.to_s)
|
116
|
+
elsif /(?<dep_>\-\>.+)$/ =~ line
|
117
|
+
new_line.gsub!(/\s*\-\>.+$/, '@' + self.headers["leader"] + ":" + progress.to_s + " " + dep_)
|
118
|
+
else
|
119
|
+
new_line += '@' + self.headers["leader"] + ":" + progress.to_s
|
120
|
+
end
|
121
|
+
wfh.puts new_line
|
122
|
+
else
|
123
|
+
wfh.puts line
|
124
|
+
end
|
125
|
+
end
|
126
|
+
tfh.close
|
127
|
+
wfh.close
|
128
|
+
|
129
|
+
FileUtils.rm tmp_file
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# representations for a task in roadmap and in Gantt chart.
|
2
|
+
# ==========================================================
|
3
|
+
#
|
4
|
+
# @author: Huang Wei <huangw@pe-po.com>
|
5
|
+
# version 1.0a
|
6
|
+
|
7
|
+
module DevFlow
|
8
|
+
## Task object represent a single line on the Gantt chart
|
9
|
+
class Task
|
10
|
+
attr_accessor :file, :ln, # which line of file defined the task
|
11
|
+
:level, :branch_name, :display_name, :resources, :resource_names,
|
12
|
+
:progress, :completed_at, # complete date time
|
13
|
+
:dependencies, # depends on those tasks (list of branch_names)
|
14
|
+
:dependencie_names, # denpendencies in branch names (for display)
|
15
|
+
:start_date, :end_date,
|
16
|
+
:parent, :children, # all in branch_names first
|
17
|
+
:is_pending, :is_deleted
|
18
|
+
|
19
|
+
# initialize with level, file and line number
|
20
|
+
def initialize level,file="-",ln=0
|
21
|
+
@level = level.to_i
|
22
|
+
raise "invalid level #{level}" unless level.to_i > 0
|
23
|
+
@file, @ln = file, ln
|
24
|
+
@children = Array.new
|
25
|
+
@dependencies = Array.new
|
26
|
+
@dependencie_names = Array.new
|
27
|
+
@progress = 0
|
28
|
+
@resources = Array.new
|
29
|
+
@resource_names = Array.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# filter methods
|
33
|
+
def is_milestone?
|
34
|
+
self.branch_name =~ /^(milestone|release)\_/ ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_release?
|
38
|
+
self.branch_name =~ /^release\_/ ? true : false
|
39
|
+
end
|
40
|
+
|
41
|
+
def is_completed?
|
42
|
+
self.progress == 100
|
43
|
+
end
|
44
|
+
|
45
|
+
def is_pending?
|
46
|
+
self.is_pending ? true :false
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_deleted?
|
50
|
+
self.is_deleted ? true : false
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_parent?
|
54
|
+
self.children.size > 0 ? true : false
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_urgent? # usually orange
|
58
|
+
today = DateTime.now.strftime("%Y%m%d").to_i
|
59
|
+
start_day = self.start_date.strftime("%Y%m%d").to_i
|
60
|
+
end_day = self.end_date.strftime("%Y%m%d").to_i
|
61
|
+
|
62
|
+
return true if start_day == today and progress == 0
|
63
|
+
return true if end_day == today and self.progress < 100
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_delayed? # usually red
|
68
|
+
today = DateTime.now.strftime("%Y%m%d").to_i
|
69
|
+
start_day = self.start_date.strftime("%Y%m%d").to_i
|
70
|
+
end_day = self.end_date.strftime("%Y%m%d").to_i
|
71
|
+
return true if self.progress < 100 and today > end_day
|
72
|
+
return true if self.progress == 0 and today > start_day
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
## check whether the task is well defined, raise error otherwise.
|
77
|
+
def validate!
|
78
|
+
if self.is_milestone?
|
79
|
+
unless self.level == 1
|
80
|
+
raise "you can only tag a top level task as a release, on #{self.file}:#{self.ln}" if self.branch_name =~ /^release\_/
|
81
|
+
end
|
82
|
+
end
|
83
|
+
raise "resource not found on #{self.file}:#{self.ln}" unless self.resources.size > 0
|
84
|
+
raise "display_name not found on #{self.file}:#{self.ln}" unless self.display_name
|
85
|
+
raise "valid start_date not found on #{self.file}:#{self.ln}" unless self.start_date
|
86
|
+
raise "wrong date on #{self.file}:#{self.ln}" unless self.start_date and self.end_date and self.start_date <= self.end_date
|
87
|
+
|
88
|
+
self
|
89
|
+
end
|
90
|
+
|
91
|
+
## parse the line from file:ln (line number), initialize the
|
92
|
+
# task object
|
93
|
+
def parse line, headers = {}
|
94
|
+
line = line.strip # delete head/trailing spaces
|
95
|
+
|
96
|
+
/^((?<branch_name_>[a-zA-Z0-9_\-\#\.\/]+):)?\s*(?<display_name_>.+)\s+(?<start_date_>(\d\d\d\d\/)?\d\d?\/\d\d?)(-(?<end_date_>(\d\d\d\d\/)?\d\d?\/\d\d?))?(\s+\@(?<resource_>[a-z\@\;]+))?(\:(?<status_>(P|D))?(?<progress_>[\d\/]+)?)?(\s+\-\>\s*(?<dependencies_>.+))?$/ =~ line
|
97
|
+
|
98
|
+
# assign branch name and display name
|
99
|
+
self.branch_name = branch_name_
|
100
|
+
|
101
|
+
self.display_name = display_name_
|
102
|
+
|
103
|
+
# assign start and end date
|
104
|
+
end_date_ = start_date_ unless end_date_
|
105
|
+
raise "no valid start date found on #{self.file}:#{self.ln}" unless start_date_ and start_date_.size > 0
|
106
|
+
if headers["year"]
|
107
|
+
start_date_ = headers["year"].to_s + "/" + start_date_ unless start_date_ =~ /^\d\d\d\d/
|
108
|
+
end_date_ = headers["year"].to_s + "/" + end_date_ unless end_date_ =~ /^\d\d\d\d/
|
109
|
+
end
|
110
|
+
|
111
|
+
begin
|
112
|
+
self.start_date = DateTime.parse(start_date_)
|
113
|
+
self.end_date = DateTime.parse(end_date_)
|
114
|
+
rescue Exception => e
|
115
|
+
raise "invalid date on #{self.file}:#{self.ln}"
|
116
|
+
end
|
117
|
+
|
118
|
+
# assign for the resources (user name)
|
119
|
+
unless resource_
|
120
|
+
if headers["leader"]
|
121
|
+
resource_ = headers["leader"]
|
122
|
+
else
|
123
|
+
raise "no resource defined on #{self.file}:#{self.ln}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
resource_.gsub!("\@", "") # @ for other user is optional
|
128
|
+
resource_.split(";").each do |r|
|
129
|
+
self.resources << r
|
130
|
+
# if the user is listed on known members
|
131
|
+
rname = r
|
132
|
+
if headers["members"] and headers["members"][r] and headers["members"][r][0]
|
133
|
+
rname = headers["members"][r][0]
|
134
|
+
end
|
135
|
+
self.resource_names << rname
|
136
|
+
end
|
137
|
+
|
138
|
+
if dependencies_
|
139
|
+
dependencies_.strip!
|
140
|
+
dependencies_.gsub!(/;$/, "")
|
141
|
+
self.dependencie_names = dependencies_.split(/;/)
|
142
|
+
end
|
143
|
+
|
144
|
+
# pending or deleted status
|
145
|
+
if status_
|
146
|
+
if status_ == "P"
|
147
|
+
self.is_pending = true
|
148
|
+
elsif status_ == "D"
|
149
|
+
self.is_deleted = true
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# progress
|
154
|
+
if progress_
|
155
|
+
if progress_ =~ /^\d\d?$/ and progress_.to_i > 0 and progress_.to_i < 100
|
156
|
+
self.progress = progress_.to_i
|
157
|
+
elsif progress_ =~ /^\d\d\d\d\/\d\d?\/\d\d?$/
|
158
|
+
self.progress = 100
|
159
|
+
self.completed_at = DateTime.parse(progress_)
|
160
|
+
elsif progress_ =~ /^\d\d?\/\d\d?$/ and headers["year"]
|
161
|
+
self.progress = 100
|
162
|
+
self.completed_at = DateTime.parse(headers["year"].to_s + "/" + progress_.to_s)
|
163
|
+
else
|
164
|
+
msg = "worng format around progress parameter '#{progress_}', on #{self.file}:#{self.ln}"
|
165
|
+
msg += " (HINT: use date to complete a task, not 100)" if progress_.to_i == 100
|
166
|
+
raise msg
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
end # class Task
|
174
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module DevFlow
|
2
|
+
class Task
|
3
|
+
|
4
|
+
def as_title header = ' '
|
5
|
+
name = self.display_name
|
6
|
+
name = self.display_name.bold if self.is_workable?
|
7
|
+
name = self.display_name.blue if self.progress > 0
|
8
|
+
name = self.display_name.green if self.is_completed?
|
9
|
+
name = self.display_name.magenta if self.is_urgent?
|
10
|
+
name = self.display_name.red if self.is_delayed?
|
11
|
+
|
12
|
+
if self.progress > 0 and self.progress < 100
|
13
|
+
on_branch = sprintf "(=> %s, %02d%%)", self.branch_name.bold, self.progress
|
14
|
+
end
|
15
|
+
|
16
|
+
title = sprintf("%s[%s]%s%s", ' '*(self.level-1), header, name, on_branch)
|
17
|
+
end
|
18
|
+
|
19
|
+
## a task is completable if all children complated
|
20
|
+
def is_completable?
|
21
|
+
return false if self.is_completed?
|
22
|
+
self.children.each do |child|
|
23
|
+
return false unless child.is_completed?
|
24
|
+
end
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
## a task is workable (can be started) if all children
|
29
|
+
# task and dependent task are completed
|
30
|
+
def is_workable?
|
31
|
+
# trivial: if already completed, do not start again
|
32
|
+
return false if self.is_completed?
|
33
|
+
return false if self.is_pending or self.is_deleted
|
34
|
+
|
35
|
+
self.dependencies.each do |t|
|
36
|
+
return false unless t.is_completed?
|
37
|
+
end
|
38
|
+
|
39
|
+
self.children.each do |t|
|
40
|
+
return false unless t.is_completed?
|
41
|
+
end
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/lib/dev_flow.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# model layer
|
6
|
+
require 'dev_flow/task'
|
7
|
+
require 'dev_flow/roadmap'
|
8
|
+
require 'dev_flow/member'
|
9
|
+
|
10
|
+
# presenter
|
11
|
+
require 'dev_flow/task_console'
|
12
|
+
|
13
|
+
# application and commands
|
14
|
+
require 'dev_flow/app'
|
15
|
+
require 'dev_flow/version'
|
16
|
+
|
17
|
+
# other helper and libraries
|
18
|
+
require 'dev_flow/girc'
|
19
|
+
|
20
|
+
module DevFlow
|
21
|
+
def self.invoke! config, command
|
22
|
+
require "dev_flow/commands/#{command}"
|
23
|
+
klass = command.to_s.capitalize
|
24
|
+
eval(klass).new(config, command).process!
|
25
|
+
end
|
26
|
+
end
|
data/spec/app_spec.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe DevFlow::App do
|
4
|
+
describe "#all_member_names" do
|
5
|
+
subject (:app) do
|
6
|
+
DevFlow::App.new({members_file:"examples/members.yml", roadmap:"examples/ROADMAP"}, "info")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should read 7 members" do
|
10
|
+
app.all_member_names.size.should eq(7)
|
11
|
+
app.all_member_names.include?("huangw").should be_true
|
12
|
+
app.all_member_names.include?("wangqh").should be_true
|
13
|
+
app.all_member_names.include?("sunyr").should be_true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DevFlow::RoadMap do
|
4
|
+
describe "#parse" do
|
5
|
+
context "valid file" do
|
6
|
+
subject (:roadmap) do
|
7
|
+
DevFlow::RoadMap.new('examples/ROADMAP', {}).parse
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should be a Roadmap object" do
|
11
|
+
roadmap.is_a?(DevFlow::RoadMap).should be_true
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should assign parents well" do
|
15
|
+
roadmap.ln_tasks[28].parent.is_a?(DevFlow::Task)
|
16
|
+
roadmap.ln_tasks[28].level.should eq(2)
|
17
|
+
roadmap.ln_tasks[28].parent.branch_name.should eq("scope")
|
18
|
+
roadmap.ln_tasks[28].parent.level.should eq(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should assign dependencies well" do
|
22
|
+
roadmap.ln_tasks[70].dependencies.size.should eq(2)
|
23
|
+
roadmap.ln_tasks[70].dependencies[0].branch_name.should eq("release_api_design_0.1")
|
24
|
+
roadmap.ln_tasks[70].dependencies[1].branch_name.should eq("model_spec")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|