flash_flow 1.2.1 → 1.2.2.a

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.
@@ -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