flash_flow 1.4.6 → 1.4.7

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