dev_flow 0.0.4

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.
@@ -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,10 @@
1
+ module DevFlow
2
+ class Member
3
+ attr_accessor :name, :display_name, :email
4
+
5
+ def initialize name, display_name, email
6
+ @name, @display_name, @email = name, display_name, email
7
+ end
8
+
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module DevFlow
2
+ VERSION = '0.0.4'
3
+ 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
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+ require 'dev_flow'
3
+
4
+ RSpec.configure do |config|
5
+ config.color_enabled = true
6
+ config.formatter = 'documentation'
7
+ end