dev_flow 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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