flash_flow 1.2.1 → 1.2.2.a

Sign up to get free protection for your applications and to get access to all the features.
@@ -32,10 +32,30 @@ module FlashFlow
32
32
  issue_tracker.release_notes(hours, file) if issue_tracker.respond_to?(:release_notes)
33
33
  end
34
34
 
35
+ def story_deployable?(story_id)
36
+ issue_tracker.story_deployable?(story_id) if issue_tracker.respond_to?(:story_deployable?)
37
+ end
38
+
39
+ def story_link(story_id)
40
+ issue_tracker.story_link(story_id) if issue_tracker.respond_to?(:story_link)
41
+ end
42
+
43
+ def story_title(story_id)
44
+ issue_tracker.story_title(story_id) if issue_tracker.respond_to?(:story_title)
45
+ end
46
+
47
+ def release_keys(story_id)
48
+ issue_tracker.release_keys(story_id) if issue_tracker.respond_to?(:release_keys)
49
+ end
50
+
51
+ def stories_for_release(release_key)
52
+ issue_tracker.stories_for_release(release_key) if issue_tracker.respond_to?(:stories_for_release)
53
+ end
54
+
35
55
  private
36
56
 
37
57
  def git
38
- @git ||= Git.new(Config.configuration.git, Config.configuration.logger)
58
+ @git ||= ShadowGit.new(Config.configuration.git, Config.configuration.logger)
39
59
  end
40
60
 
41
61
  def get_branches
@@ -11,10 +11,11 @@ module FlashFlow
11
11
  @branches = branches
12
12
  @git = git
13
13
  @timezone = opts['timezone'] || "UTC"
14
+ @project_id = opts['project_id']
14
15
 
15
16
  PivotalTracker::Client.token = opts['token']
16
17
  PivotalTracker::Client.use_ssl = true
17
- @project = PivotalTracker::Project.find(opts['project_id'])
18
+ @release_label_prefix = Regexp.new("^#{opts['release_label_prefix'] || 'release'}", Regexp::IGNORECASE)
18
19
  end
19
20
 
20
21
  def stories_pushed
@@ -56,8 +57,42 @@ module FlashFlow
56
57
  print_release_notes(release_notes, file)
57
58
  end
58
59
 
60
+ def story_deployable?(story_id)
61
+ story = get_story(story_id)
62
+
63
+ story.current_state == 'accepted'
64
+ end
65
+
66
+ def story_link(story_id)
67
+ story = get_story(story_id)
68
+
69
+ story.url if story
70
+ end
71
+
72
+ def story_title(story_id)
73
+ story = get_story(story_id)
74
+
75
+ story.name if story
76
+ end
77
+
78
+ def release_keys(story_id)
79
+ story = get_story(story_id)
80
+
81
+ return [] if story.labels.nil?
82
+
83
+ story.labels.split(",").map(&:strip).select { |label| label =~ @release_label_prefix }.map(&:strip)
84
+ end
85
+
86
+ def stories_for_release(release_key)
87
+ stories_for_label(release_key)
88
+ end
89
+
59
90
  private
60
91
 
92
+ def project
93
+ @project ||= PivotalTracker::Project.find(@project_id)
94
+ end
95
+
61
96
  def undeliver(story_id)
62
97
  story = get_story(story_id)
63
98
 
@@ -111,7 +146,12 @@ module FlashFlow
111
146
  end
112
147
 
113
148
  def done_and_current_stories
114
- [@project.iteration(:done).last(2).map(&:stories) + @project.iteration(:current).stories].flatten
149
+ [project.iteration(:done).last(2).map(&:stories) + project.iteration(:current).stories].flatten
150
+ end
151
+
152
+ def stories_for_label(label)
153
+ @stories_for_label ||= {}
154
+ @stories_for_label[label] ||= project.stories.all(label: label).map(&:id)
115
155
  end
116
156
 
117
157
  def release_by(release_stories, hours)
@@ -128,7 +168,8 @@ module FlashFlow
128
168
  end
129
169
 
130
170
  def get_story(story_id)
131
- @project.stories.find(story_id)
171
+ @stories ||= {}
172
+ @stories[story_id] ||= project.stories.find(story_id)
132
173
  end
133
174
 
134
175
  def already_has_comment?(story, comment)
@@ -0,0 +1,6 @@
1
+ module FlashFlow
2
+ module MergeMaster
3
+ end
4
+ end
5
+
6
+ require 'flash_flow/merge_master/status'
@@ -0,0 +1,144 @@
1
+ <html>
2
+ <head>
3
+ <style>
4
+ .story {
5
+ padding: 15px 0;
6
+ }
7
+
8
+ span {
9
+ margin-left: 30px;
10
+ }
11
+
12
+ .branch {
13
+ border: solid black 1px;
14
+ margin: 30px 0;
15
+ padding: 30px;
16
+ }
17
+
18
+ img {
19
+ margin: 60px;
20
+ }
21
+
22
+ .good-to-go {
23
+ background-color: lightgreen;
24
+ }
25
+
26
+ .status.shippable {
27
+ color: green;
28
+ }
29
+
30
+ .status {
31
+ margin-right: 30px;
32
+ }
33
+
34
+ .status.not-shippable {
35
+ color: lightpink;
36
+ }
37
+
38
+ .story .status {
39
+ margin-left: 30px;
40
+ width: 14%;
41
+ min-width: 100px;
42
+ display: inline-block;
43
+ }
44
+
45
+ .release {
46
+ padding-left: 15px;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <% @branches.each do |branch, branch_hash| %>
52
+ <div class="branch <%= branch_hash[:shippable?] ? 'good-to-go' : '' %>">
53
+ <h2 class="status <%= branch_hash[:shippable?] ? 'shippable' : 'not-shippable' %>">
54
+ <%= branch_hash[:shippable?] ? 'Ready to ship' : 'Can not ship' %>
55
+ </h2>
56
+
57
+ <h2>
58
+ <span class="status <%= branch_hash[:branch_can_ship?] ? 'shippable' : 'not-shippable' %>"><%= branch_hash[:branch_can_ship?] ? 'Code ready' : 'Code not ready' %></span>
59
+ <a href='<%= branch_hash[:branch_url] %>'><%= branch_hash[:name] %></a>
60
+ </h2>
61
+
62
+ <% (branch_hash[:connected_branches].to_a - [branch]).each do |connected_branch| %>
63
+ <% connected_branch_hash = @branches[connected_branch] %>
64
+ <h2>
65
+ <span style="margin-right: 30px;">Must ship with: </span>
66
+ <a href='<%= connected_branch_hash[:branch_url] %>'><%= connected_branch_hash[:name] %></a>
67
+ </h2>
68
+ <% end %>
69
+
70
+ <div class="grouping">
71
+ <% if branch_hash[:stories].empty? %>
72
+ <h3>
73
+ This branch has no stories
74
+ </h3>
75
+ <% else %>
76
+ <h3>
77
+ Stories for this branch
78
+ </h3>
79
+ <% branch_hash[:my_stories].each do |story_id| %>
80
+ <% story = @stories[story_id] %>
81
+ <div class="story">
82
+ <span class="status <%= story[:can_ship?] ? 'shippable' : 'not-shippable' %>"><%= story[:can_ship?] ? 'Story ready' : 'Story not ready' %></span>
83
+ <a href='<%= story[:url] %>'><%= story[:title] %></a>
84
+ <span><%= story[:release_keys].empty? ? '' : "Related releases: #{story[:release_keys].join(", ")}" %></span>
85
+ </div>
86
+ <% end %>
87
+
88
+ <% unless (branch_hash[:stories] - branch_hash[:my_stories]).empty? %>
89
+ <h3>
90
+ Stories that must be released with this branch
91
+ </h3>
92
+ <% (branch_hash[:stories] - branch_hash[:my_stories]).each do |story_id| %>
93
+ <% story = @stories[story_id] %>
94
+ <div class="story">
95
+ <span class="status <%= story[:can_ship?] ? 'shippable' : 'not-shippable' %>"><%= story[:can_ship?] ? 'Story ready' : 'Story not ready' %></span>
96
+ <a href='<%= story[:url] %>'><%= story[:title] %></a>
97
+ <span><%= story[:release_keys].empty? ? '' : "Related releases: #{story[:release_keys].join(", ")}" %></span>
98
+ </div>
99
+ <% end %>
100
+ <% end %>
101
+ <% end %>
102
+ </div>
103
+
104
+ <div class="grouping">
105
+ <% if branch_hash[:releases].empty? %>
106
+ <h3>
107
+ This branch has no associated releases
108
+ </h3>
109
+ <% else %>
110
+ <h3>
111
+ Releases for this branch
112
+ </h3>
113
+ <% branch_hash[:releases].each do |release_key| %>
114
+ <div class="release">
115
+ <h4><%= release_key %></h4>
116
+ <% @releases[release_key][:stories].each do |story_id| %>
117
+ <% story = @stories[story_id] %>
118
+ <div class="story">
119
+ <span class="status <%= story[:can_ship?] ? 'shippable' : 'not-shippable' %>"><%= story[:can_ship?] ? 'Story ready' : 'Story not ready' %></span>
120
+ <a href='<%= story[:url] %>'><%= story[:title] %></a>
121
+ <span><%= story[:release_keys].empty? ? '' : "Related releases: #{story[:release_keys].join(", ")}" %></span>
122
+ </div>
123
+ <% end %>
124
+ </div>
125
+ <% end %>
126
+ <% end %>
127
+ </div>
128
+
129
+ <% if branch_hash[:stories].size > 1 %>
130
+ <% if branch_hash[:image] %>
131
+ <img src='file:///<%= branch_hash[:image] %>'/>
132
+ <% else %>
133
+ <img src=""/>
134
+
135
+ <h2 class="warning">GraphViz not installed.</h2>
136
+ <h4 class="warning">On a Mac, "brew install graphviz" to see a graphical version of your branch's dependencies</h4>
137
+ <% end %>
138
+ <% end %>
139
+ </div>
140
+
141
+
142
+ <% end %>
143
+ </body>
144
+ </html>
@@ -0,0 +1,135 @@
1
+ require 'graphviz'
2
+
3
+ module FlashFlow
4
+ module MergeMaster
5
+ class ReleaseGraph
6
+ attr_accessor :branches, :issue_tracker
7
+
8
+ def self.build(branches, issue_tracker)
9
+ instance = new(branches, issue_tracker)
10
+ instance.build
11
+ instance
12
+ end
13
+
14
+ def initialize(branches, issue_tracker)
15
+ @issue_tracker = issue_tracker
16
+ @branches = branches
17
+ end
18
+
19
+ def output(png_file)
20
+ graph.output(png: png_file)
21
+ png_file
22
+ rescue StandardError => e
23
+ if e.message =~ /GraphViz/i
24
+ nil
25
+ else
26
+ raise e
27
+ end
28
+ end
29
+
30
+ def build
31
+ queue = []
32
+
33
+ branches.each do |branch|
34
+ seen_branches[branch] = true
35
+ queue.unshift([branch, graph.add_node(branch.ref)]) #, color: branch[:shippable?] ? 'green' : 'red', shape: 'record', label: "<f0>#{branch[:name]}")
36
+ end
37
+
38
+ while !queue.empty?
39
+ element, node = queue.pop
40
+
41
+ case
42
+ when seen_branches.has_key?(element)
43
+ element.stories.to_a.map(&:to_s).each do |story_id|
44
+ story_id = story_id.to_s
45
+ seen_stories[story_id] = true
46
+ find_or_add_node(node, queue, story_id)
47
+ end
48
+
49
+ when seen_stories.has_key?(element)
50
+ issue_tracker.release_keys(element).each do |release_key|
51
+ seen_releases[release_key] = true
52
+ find_or_add_node(node, queue, release_key)
53
+ end
54
+
55
+ when seen_releases.has_key?(element)
56
+ issue_tracker.stories_for_release(element).map(&:to_s).each do |story_id|
57
+ story_id = story_id.to_s
58
+ seen_stories[story_id] = true
59
+ find_or_add_node(node, queue, story_id)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def connected_branches(node_id)
66
+ visited = connected(node_id)
67
+
68
+ seen_branches.keys.select { |k| visited[k.ref] }
69
+ end
70
+
71
+ def connected_stories(node_id)
72
+ visited = connected(node_id)
73
+
74
+ seen_stories.keys.select { |k| visited[k] }
75
+ end
76
+
77
+ def connected_releases(node_id)
78
+ visited = connected(node_id)
79
+
80
+ seen_releases.keys.select { |k| visited[k] }
81
+ end
82
+
83
+ private
84
+
85
+ def find_or_add_node(node, queue, identifier)
86
+ other_node = graph.find_node(identifier)
87
+ unless other_node
88
+ other_node = graph.add_node(identifier)
89
+ queue.unshift([identifier, other_node])
90
+ end
91
+
92
+ graph.add_edge(node, other_node) unless all_neighbors(node).include?(other_node)
93
+ end
94
+
95
+ def seen_branches
96
+ @seen_branches ||= {}
97
+ end
98
+
99
+ def seen_releases
100
+ @seen_releases ||= {}
101
+ end
102
+
103
+ def seen_stories
104
+ @seen_stories ||= {}
105
+ end
106
+
107
+
108
+ def connected(node_id)
109
+ node = graph.find_node(node_id)
110
+ visited = {}
111
+ search(node, visited) if node
112
+
113
+ visited
114
+ end
115
+
116
+ def all_neighbors(node)
117
+ (node.neighbors | node.incidents)
118
+ end
119
+
120
+ def search(node, visited)
121
+ return if visited[node.id]
122
+
123
+ visited[node.id] = true
124
+
125
+ all_neighbors(node).each do |neighbor|
126
+ search(neighbor, visited)
127
+ end
128
+ end
129
+
130
+ def graph
131
+ @graph ||= GraphViz.new(:G, :type => :graph)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,108 @@
1
+ require 'flash_flow/merge_master/release_graph'
2
+
3
+ module FlashFlow
4
+ module MergeMaster
5
+ class Status
6
+ attr_reader :issue_tracker, :collection, :stories, :releases
7
+
8
+ def initialize(issue_tracker_config, branches_config, branch_info_file, git_config, opts={})
9
+ @issue_tracker = IssueTracker::Base.new(issue_tracker_config)
10
+ @collection = Data::Base.new(branches_config, branch_info_file, ShadowGit.new(git_config)).merged_branches
11
+ end
12
+
13
+ def status(filename=nil)
14
+ filename = File.dirname(__FILE__) + '/merge_status.html'
15
+ @branches = branches
16
+
17
+ template = ERB.new File.read(File.dirname(__FILE__) + '/merge_status.html.erb')
18
+ html = template.result(binding)
19
+ File.open(filename, 'w') do |f|
20
+ f.puts html
21
+ end
22
+ `open #{filename}`
23
+ end
24
+
25
+ def branches
26
+ g = ReleaseGraph.build(collection.current_branches, issue_tracker)
27
+
28
+ branch_hash = {}
29
+ collection.current_branches.each_with_index do |branch, i|
30
+ connected_branches = g.connected_branches(branch.ref)
31
+ connected_stories = g.connected_stories(branch.ref)
32
+ connected_releases = g.connected_releases(branch.ref)
33
+ add_stories(connected_stories)
34
+ add_releases(connected_releases)
35
+ sub_g = ReleaseGraph.build(connected_branches, issue_tracker)
36
+ graph_file = sub_g.output("/tmp/graph-#{i}.png")
37
+
38
+ branch_hash[branch] =
39
+ Hash.new.tap do |hash|
40
+ hash[:name] = branch.ref
41
+ hash[:branch_url] = collection.branch_link(branch)
42
+ hash[:branch_can_ship?] = collection.can_ship?(branch)
43
+ hash[:connected_branches] = connected_branches
44
+ hash[:image] = graph_file
45
+ hash[:my_stories] = branch.stories.to_a
46
+ hash[:stories] = connected_stories
47
+ hash[:releases] = connected_releases
48
+ end
49
+ end
50
+
51
+ mark_as_shippable(branch_hash)
52
+ branch_hash
53
+ end
54
+
55
+ private
56
+
57
+ def add_stories(story_list)
58
+ @stories ||= {}
59
+ story_list.each do |story_id|
60
+ @stories[story_id] ||= story_info_hash(story_id)
61
+ end
62
+ end
63
+
64
+ def add_releases(release_list)
65
+ @releases ||= {}
66
+ release_list.each do |release_key|
67
+ @releases[release_key] ||= {stories: issue_tracker.stories_for_release(release_key).map(&:to_s)}
68
+ end
69
+ end
70
+
71
+ def mark_as_shippable(branches)
72
+ branches.each do |_, b|
73
+ b[:shippable?] = b[:branch_can_ship?] &&
74
+ unshippable_stories(b[:stories]).empty? &&
75
+ unshippable_releases(b[:releases]).empty?
76
+ end
77
+
78
+ branches.each do |_, b|
79
+ b[:shippable?] &= b[:connected_branches].all? do |other_branch|
80
+ branches[other_branch][:shippable?]
81
+ end
82
+ end
83
+ end
84
+
85
+ def unshippable_releases(arr)
86
+ arr.select do |release_key|
87
+ !unshippable_stories(@releases[release_key][:stories]).empty?
88
+ end
89
+
90
+ end
91
+
92
+ def unshippable_stories(arr)
93
+ arr.select { |story| !@stories[story][:can_ship?] }
94
+ end
95
+
96
+ def story_info_hash(story_id)
97
+ {
98
+ id: story_id,
99
+ url: issue_tracker.story_link(story_id),
100
+ title: issue_tracker.story_title(story_id),
101
+ can_ship?: issue_tracker.story_deployable?(story_id),
102
+ release_keys: issue_tracker.release_keys(story_id)
103
+ }
104
+ end
105
+
106
+ end
107
+ end
108
+ end