flash_flow 1.4.6 → 1.4.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4b4faf52faa0345d39f5a6b125d897a33ef97dc6
4
- data.tar.gz: 96e3e0f1160587d5bbd972a4357f82c92807704d
3
+ metadata.gz: 15f833dbdf036b8b927f9aa237a16eeb7517dddd
4
+ data.tar.gz: 23a4cfc9d4c72be08b0d62033135761ad2b1b312
5
5
  SHA512:
6
- metadata.gz: d513f6f38ac8bd6e8e44e2e380975f7fafd9c1f28c60999b1d91d33312daff171fd97e855a3d9fdcfa5f1190dad7ae4f3caab9b5336d7531d1dd96b13a0b372d
7
- data.tar.gz: 5767d8b4b74060742e2a50b50aaa7b782c7858fec8badbfc4d6c2e05402052b3f285c0ca13029a4f6920b28a82b1ac194571e93f7d4c924f0bd67bdaab52bbb4
6
+ metadata.gz: 7c13b101fa30de5b08f047f5f595505b183d0e93a2ec79ffdef3537d751d0aa0503552c18533c07a3b5b15c63ca1f35e7464192eaec2e5fd193b439d9e51b82e
7
+ data.tar.gz: 9eff4cf8331cb77cc4c29db326f08e793c8ce2db05d3e2f5aa3b09502cff9bbde14e16ddb963986bf5f4bfb6889c7ea8489cc74d86d74968ef8eee57cecad947
@@ -1,12 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- flash_flow (1.4.6)
4
+ flash_flow (1.4.7)
5
5
  hipchat (~> 1.5)
6
6
  mail
7
7
  octokit (~> 4.1)
8
8
  percy-client
9
9
  pivotal-tracker (~> 0.5)
10
+ prawn
10
11
  ruby-graphviz (> 0)
11
12
 
12
13
  GEM
@@ -33,11 +34,13 @@ GEM
33
34
  domain_name (~> 0.5)
34
35
  httparty (0.14.0)
35
36
  multi_xml (>= 0.5.2)
36
- httpclient (2.8.0)
37
- mail (2.6.3)
38
- mime-types (>= 1.16, < 3)
39
- mime-types (2.99.2)
40
- mimemagic (0.3.1)
37
+ httpclient (2.8.2)
38
+ mail (2.6.4)
39
+ mime-types (>= 1.16, < 4)
40
+ mime-types (3.1)
41
+ mime-types-data (~> 3.2015)
42
+ mime-types-data (3.2016.0521)
43
+ mimemagic (0.3.2)
41
44
  mini_portile2 (2.1.0)
42
45
  minitest (5.3.5)
43
46
  minitest-stub_any_instance (1.0.1)
@@ -51,6 +54,7 @@ GEM
51
54
  nokogiri (~> 1.5)
52
55
  octokit (4.3.0)
53
56
  sawyer (~> 0.7.0, >= 0.5.3)
57
+ pdf-core (0.6.1)
54
58
  percy-client (1.6.0)
55
59
  faraday (>= 0.9)
56
60
  httpclient (>= 2.6)
@@ -61,6 +65,9 @@ GEM
61
65
  nokogiri-happymapper (>= 0.5.4)
62
66
  rest-client (>= 1.8.0)
63
67
  pkg-config (1.1.7)
68
+ prawn (2.1.0)
69
+ pdf-core (~> 0.6.1)
70
+ ttfunk (~> 1.4.0)
64
71
  rake (10.4.2)
65
72
  rest-client (2.0.0)
66
73
  http-cookie (>= 1.0.2, < 2.0)
@@ -72,6 +79,7 @@ GEM
72
79
  addressable (>= 2.3.5, < 2.5)
73
80
  faraday (~> 0.8, < 0.10)
74
81
  slop (3.6.0)
82
+ ttfunk (1.4.0)
75
83
  unf (0.1.4)
76
84
  unf_ext
77
85
  unf_ext (0.0.7.2)
@@ -32,6 +32,10 @@ case
32
32
  FlashFlow::Merge::Status.new(FlashFlow::Config.configuration.issue_tracker, FlashFlow::Config.configuration.branches, FlashFlow::Config.configuration.branch_info_file, FlashFlow::Config.configuration.git, logger: FlashFlow::Config.configuration.logger).status_html
33
33
  when options[:release_branches]
34
34
  FlashFlow::Merge::Release.new(options).run
35
+ when options[:gen_pdf_diffs]
36
+ FlashFlow::Release::Base.new(FlashFlow::Config.configuration.release).gen_pdf_diffs(*options[:gen_pdf_diffs])
37
+ when options[:merge_release]
38
+ FlashFlow::Merge::Master.new(options).run
35
39
  else
36
40
  FlashFlow::Merge::Acceptance.new(options).run
37
41
  FlashFlow::IssueTracker::Base.new(FlashFlow::Config.configuration.issue_tracker).stories_pushed
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'ruby-graphviz', "> 0"
24
24
  spec.add_dependency 'percy-client'
25
25
  spec.add_dependency 'mail'
26
+ spec.add_dependency 'prawn'
26
27
 
27
28
  spec.add_development_dependency "bundler", "~> 1.6"
28
29
  spec.add_development_dependency "rake", "> 0"
@@ -60,12 +60,12 @@ module FlashFlow
60
60
  end
61
61
 
62
62
  def branch_contains?(branch, ref)
63
- run("branch --contains #{ref}", log: CmdRunner::LOG_CMD)
63
+ run("branch -a --contains #{ref}", log: CmdRunner::LOG_CMD)
64
64
  last_stdout.split("\n").detect { |str| str[2..-1] == branch }
65
65
  end
66
66
 
67
- def master_branch_contains?(ref)
68
- branch_contains?(master_branch, ref)
67
+ def master_branch_contains?(sha)
68
+ branch_contains?("remotes/#{remote}/#{master_branch}", sha)
69
69
  end
70
70
 
71
71
  def in_original_merge_branch
@@ -175,13 +175,12 @@ module FlashFlow
175
175
  run("push #{'-f' if force} #{remote} #{branch}")
176
176
  end
177
177
 
178
- def copy_temp_to_branch(branch, squash_message = nil)
178
+ def copy_temp_to_branch(branch, squash_message = nil)
179
179
  run("checkout #{temp_merge_branch}")
180
180
  run("merge --strategy=ours --no-edit #{branch}")
181
181
  run("checkout #{branch}")
182
182
  run("merge #{temp_merge_branch}")
183
183
 
184
-
185
184
  squash_commits(branch, squash_message) if squash_message
186
185
  end
187
186
 
@@ -4,5 +4,6 @@ module FlashFlow
4
4
  end
5
5
 
6
6
  require 'flash_flow/merge/acceptance'
7
+ require 'flash_flow/merge/master'
7
8
  require 'flash_flow/merge/release'
8
9
  require 'flash_flow/merge/status'
@@ -15,6 +15,7 @@ module FlashFlow
15
15
  class VersionError < RuntimeError; end
16
16
  class OutOfSyncWithRemote < RuntimeError; end
17
17
  class UnmergeableBranch < RuntimeError; end
18
+ class NothingToMergeError < RuntimeError; end
18
19
 
19
20
  def initialize(opts={})
20
21
  @local_git = Git.new(Config.configuration.git, logger)
@@ -70,6 +71,30 @@ module FlashFlow
70
71
  branch.ref == @git.working_branch
71
72
  end
72
73
 
74
+ def pending_release
75
+ @data.releases.detect { |r| r['status'] == 'Pending' }
76
+ end
77
+
78
+ def release_ahead_of_master
79
+ @git.branch_exists?("#{@git.remote}/#{@git.release_branch}") &&
80
+ !@git.master_branch_contains?(@git.get_sha("#{@git.remote}/#{@git.release_branch}"))
81
+ end
82
+
83
+ def write_data(commit_msg)
84
+ @git.in_temp_merge_branch do
85
+ @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
86
+ end
87
+ @git.in_merge_branch do
88
+ @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
89
+ end
90
+
91
+ @data.save!
92
+
93
+ @git.copy_temp_to_branch(@git.merge_branch, commit_msg)
94
+ @git.delete_temp_merge_branch
95
+ @git.push(@git.merge_branch, false)
96
+ end
97
+
73
98
  end
74
99
  end
75
100
  end
@@ -0,0 +1,75 @@
1
+ require 'flash_flow/merge/base'
2
+
3
+ module FlashFlow
4
+ module Merge
5
+ class Master < Base
6
+
7
+ class GitPushFailure < RuntimeError; end
8
+
9
+ def initialize(opts={})
10
+ super(opts)
11
+
12
+ @data = Data::Base.new({}, Config.configuration.branch_info_file, @git, logger: logger)
13
+ end
14
+
15
+ def run
16
+ begin
17
+ check_version
18
+ puts "Merging #{@git.release_branch} into #{@git.master_branch}"
19
+ logger.info "\n\n### Beginning merge of #{@git.release_branch} into #{@git.master_branch} ###\n\n"
20
+
21
+ mergers, errors = [], []
22
+
23
+ @lock.with_lock do
24
+ release = pending_release
25
+ if !release
26
+ raise NothingToMergeError.new("There is no pending release.")
27
+ elsif !release_ahead_of_master
28
+ raise NothingToMergeError.new("The release branch '#{@git.release_branch}' has no commits that are not in master")
29
+ end
30
+
31
+ @git.in_original_merge_branch do
32
+ @git.initialize_rerere
33
+ end
34
+
35
+ @git.in_branch(@git.master_branch) do
36
+ @git.run("reset --hard origin/master")
37
+ merge_branches([Data::Branch.new(@git.release_branch)]) do |branch, merger|
38
+ mergers << [branch, merger]
39
+ end
40
+ end
41
+
42
+ errors = mergers.select { |m| m.last.result != :success }
43
+
44
+ if errors.empty?
45
+ unless @git.push("#{@git.master_branch}:#{@git.master_branch}", true)
46
+ raise GitPushFailure.new("Unable to push to #{@git.master_branch}. See log for details.")
47
+ end
48
+
49
+ released_sha = @git.get_sha(@git.master_branch)
50
+
51
+ release['status'] = 'Success'
52
+ release['released_sha'] = released_sha
53
+
54
+ write_data('Release merged [ci skip]')
55
+ end
56
+ end
57
+
58
+ if errors.empty?
59
+ puts 'Success!'
60
+ else
61
+ raise UnmergeableBranch.new("#{@git.release_branch} didn't merge successfully to #{@git.master_branch}:\n #{errors.map { |e| e.first.ref }.join("\n ")}")
62
+ end
63
+
64
+ logger.info "### Finished merge of #{@git.release_branch} into #{@git.master_branch} ###"
65
+ rescue Lock::Error, OutOfSyncWithRemote, UnmergeableBranch, GitPushFailure, NothingToMergeError, VersionError => e
66
+ puts 'Failure!'
67
+ puts e.message
68
+ ensure
69
+ @local_git.run("checkout #{@local_git.working_branch}")
70
+ end
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -5,7 +5,6 @@ module FlashFlow
5
5
  class Release < Base
6
6
 
7
7
  class PendingReleaseError < RuntimeError; end
8
- class NothingToMergeError < RuntimeError; end
9
8
  class GitPushFailure < RuntimeError; end
10
9
 
11
10
  def initialize(opts={})
@@ -26,11 +25,10 @@ module FlashFlow
26
25
  mergers, errors = [], []
27
26
 
28
27
  @lock.with_lock do
29
- release = @data.releases.detect { |r| r['status'] == 'Pending' }
28
+ release = pending_release
30
29
  if release
31
30
  raise PendingReleaseError.new("There is already a pending release: #{release}")
32
- elsif @git.branch_exists?("#{@git.remote}/#{@git.release_branch}") &&
33
- !@git.branch_contains?(@git.master_branch, @git.get_sha("#{@git.remote}/#{@git.release_branch}"))
31
+ elsif release_ahead_of_master
34
32
  raise PendingReleaseError.new("The release branch '#{@git.release_branch}' has commits that are not in master")
35
33
  end
36
34
 
@@ -56,18 +54,7 @@ module FlashFlow
56
54
 
57
55
  @data.releases.unshift({ created_at: Time.now, sha: release_sha, status: 'Pending' })
58
56
 
59
- @git.in_temp_merge_branch do
60
- @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
61
- end
62
- @git.in_merge_branch do
63
- @git.run("reset --hard #{@git.remote}/#{@git.merge_branch}")
64
- end
65
-
66
- @data.save!
67
-
68
- @git.copy_temp_to_branch(@git.merge_branch, 'Release data updated [ci skip]')
69
- @git.delete_temp_merge_branch
70
- @git.push(@git.merge_branch, false)
57
+ write_data('Release branch created [ci skip]')
71
58
  end
72
59
  end
73
60
 
@@ -5,8 +5,8 @@ module FlashFlow
5
5
  def self.parse
6
6
  options = {}
7
7
  opt_parser = OptionParser.new do |opts|
8
- opts.banner = "Usage: flash_flow [options]"
9
- opts.separator ""
8
+ opts.banner = 'Usage: flash_flow [options]'
9
+ opts.separator ''
10
10
 
11
11
  opts.on('--install', 'Copy flash_flow.yml.erb to your repo and exit') { |v| options[:install] = true }
12
12
  opts.on('-v', '--version', 'Print the current version of flash flow and exit') { |v| options[:version] = true }
@@ -25,8 +25,10 @@ module FlashFlow
25
25
  opts.on('--merge-status', 'Show status of all branches and their stories and exit') { |v| options[:merge_status] = true }
26
26
  opts.on('--merge-status-html', 'Show status of all branches and their stories in html format and exit') { |v| options[:merge_status_html] = true }
27
27
  opts.on('--make-release branch1,branch2', 'Comma-delimited list of branches to merge to the release branch. Run "--merge-release ready" to merge all ready to ship branches') { |v| options[:release_branches] = v.split(',') }
28
+ opts.on('--gen-pdf-diffs output_file,build_id,threshold', 'Generate a pdf file with screenshot differences for the specified (latest) build. output_file is required. build_id defaults to the latest build. threshold defaults to 0') { |v| options[:gen_pdf_diffs] = v.split(',') }
29
+ opts.on('--merge-release', 'Merge the release branch into the master branch and push') { |v| options[:merge_release] = true }
28
30
 
29
- opts.on_tail("-h", "--help", "Show this message") do
31
+ opts.on_tail('-h', '--help', 'Show this message') do
30
32
  puts opts
31
33
  exit
32
34
  end
@@ -19,6 +19,10 @@ module FlashFlow
19
19
  @release.send_release_email if @release.respond_to?(:send_release_email)
20
20
  end
21
21
 
22
+ def gen_pdf_diffs(output_file, threshold=0.0)
23
+ @release.gen_pdf_diffs(output_file, threshold) if @release.respond_to?(:gen_pdf_diffs)
24
+ end
25
+
22
26
  end
23
27
  end
24
28
  end
@@ -0,0 +1,159 @@
1
+ require 'prawn'
2
+ require 'open-uri'
3
+
4
+ module FlashFlow
5
+ module Release
6
+ class PdfDiffGenerator
7
+
8
+ NUM_COLUMNS = 3
9
+ SPACE_BETWEEN = 10
10
+
11
+ def generate(compare_data, filename, threshold, verbose=true)
12
+ info = collect_comparison_info(compare_data, threshold)
13
+ @orientation = :portrait
14
+ Prawn::Document.generate(filename, page_layout: @orientation, margin: [10, 10, 10, 10]) do |pdf|
15
+ set_dimensions(*pdf.bounds.top_right)
16
+ generate_title_page(pdf)
17
+ info.each do |comparison|
18
+ add_comparison_to_pdf(pdf, comparison)
19
+ end
20
+ pdf.number_pages('<page> of <total>', { start_count_at: 1, align: :right, size: 12 })
21
+ @num_pages = pdf.page_count
22
+ end
23
+ puts "Wrote #{@num_pages} pages to: #{filename}" if verbose
24
+ filename
25
+ end
26
+
27
+ private
28
+
29
+ ##########################
30
+ # #
31
+ # PDF Generation methods #
32
+ # #
33
+ ##########################
34
+
35
+ def set_dimensions(width, height)
36
+ @page_width = width
37
+ @page_height = height
38
+ @column_landscape = compute_column_width([width, height].max)
39
+ @column_portrait = compute_column_width([width, height].min)
40
+ end
41
+
42
+ def compute_column_width(page_width)
43
+ (page_width / NUM_COLUMNS) - (SPACE_BETWEEN * (NUM_COLUMNS - 1))
44
+ end
45
+
46
+ def generate_title_page(pdf)
47
+ pdf.text("Compliance Diffs Generated At: #{Time.now.to_s}")
48
+ end
49
+
50
+ def compute_scale_factor(column_width, page_height, info)
51
+ x_scale_factor = column_width / info[:width]
52
+ y_scale_factor = page_height / info[:height]
53
+ [x_scale_factor, y_scale_factor].min
54
+ end
55
+
56
+ def compute_scale_and_orientation(info)
57
+ scale_portrait = compute_scale_factor(@column_portrait, [@page_width, @page_height].max, info)
58
+ scale_landscape = compute_scale_factor(@column_landscape, [@page_width, @page_height].min, info)
59
+ if scale_portrait > scale_landscape
60
+ @orientation = :portrait
61
+ @column_width = @column_portrait
62
+ scale_portrait
63
+ else
64
+ @orientation = :landscape
65
+ @column_width = @column_landscape
66
+ scale_landscape
67
+ end
68
+ end
69
+
70
+ def add_comparison_to_pdf(pdf, comparison)
71
+ scale_factor = compute_scale_and_orientation(comparison['head-screenshot'])
72
+ options = { vposition: :top, scale: scale_factor }
73
+
74
+ pdf.start_new_page(layout: @orientation)
75
+
76
+ place_image(pdf, comparison.dig('head-screenshot', :url), options, 1)
77
+ place_image(pdf, comparison.dig('base-screenshot', :url), options, 2)
78
+ place_image(pdf, comparison.dig('base-screenshot', :url), options, 3)
79
+ place_image(pdf, comparison.dig('pdiff', :url), options, 3)
80
+ end
81
+
82
+ def place_image(pdf, url, options, column)
83
+ pdf.float do
84
+ options[:position] = (column - 1) * (@column_width + SPACE_BETWEEN)
85
+ pdf.image(open(url), options)
86
+ end
87
+ end
88
+
89
+ ####################################
90
+ # #
91
+ # Methods to traverse Percy output #
92
+ # #
93
+ ####################################
94
+
95
+ def collect_comparison_info(compare_info, threshold=0.0)
96
+ [].tap do |result|
97
+ compare_info['data'].each do |record|
98
+ if record['type'] == 'comparisons'
99
+ comparison_urls = get_comparison_info(record, compare_info)
100
+ result << comparison_urls if comparison_urls&.dig('diff-ratio').to_f > threshold
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def get_comparison_info(record, data)
107
+ { id: record['id'] }.tap do |h|
108
+ %w(head-screenshot base-screenshot pdiff).each do |attr|
109
+ info = record.dig('relationships', attr, 'data')
110
+ unless info.nil?
111
+ attr_record = lookup_record(info['id'], info['type'], 'included', data)
112
+ h[attr] = lookup_image_url(lookup_image_id(attr_record, attr), data)
113
+ h['diff-ratio'] = attr_record.dig('attributes', 'diff-ratio') if attr == 'pdiff'
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def lookup_image_id(record, attr)
120
+ if attr == 'pdiff'
121
+ record.dig('relationships', 'diff-image', 'data', 'id')
122
+ else
123
+ record.dig('relationships', 'image', 'data', 'id')
124
+ end
125
+ end
126
+
127
+ def lookup_image_url(id, data)
128
+ record = lookup_image(id, data)
129
+ unless record.nil?
130
+ { url: record.dig('attributes', 'url'),
131
+ width: record.dig('attributes', 'width'),
132
+ height: record.dig('attributes', 'height') }
133
+ end
134
+ end
135
+
136
+ def lookup_comparison(id, data)
137
+ lookup_record(id, 'comparisons', 'data', data)
138
+ end
139
+
140
+ def lookup_image(id, data)
141
+ lookup_record(id, 'images', 'included', data)
142
+ end
143
+
144
+ def lookup_screenshot(id, data)
145
+ lookup_record(id, 'screenshots', 'data', data)
146
+ end
147
+
148
+ def lookup_snapshot(id, data)
149
+ lookup_record(id, 'snapshots', 'data', data)
150
+ end
151
+
152
+ def lookup_record(id, kind, where, data)
153
+ data[where].detect { |item| item['type'] == kind && item['id'] == id }
154
+ end
155
+
156
+ end
157
+ end
158
+ end
159
+