flash_flow 1.3.2.1 → 1.4.0

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.
@@ -0,0 +1,81 @@
1
+ require 'logger'
2
+
3
+ require 'flash_flow/git'
4
+ require 'flash_flow/data'
5
+ require 'flash_flow/lock'
6
+ require 'flash_flow/notifier'
7
+ require 'flash_flow/branch_merger'
8
+ require 'flash_flow/merge_order'
9
+ require 'flash_flow/shadow_repo'
10
+
11
+ module FlashFlow
12
+ module Merge
13
+ class Base
14
+
15
+ class VersionError < RuntimeError; end
16
+ class OutOfSyncWithRemote < RuntimeError; end
17
+ class UnmergeableBranch < RuntimeError; end
18
+
19
+ def initialize(opts={})
20
+ @local_git = Git.new(Config.configuration.git, logger)
21
+ @git = ShadowGit.new(Config.configuration.git, logger)
22
+ @lock = Lock::Base.new(Config.configuration.lock)
23
+ @notifier = Notifier::Base.new(Config.configuration.notifier)
24
+ end
25
+
26
+ def logger
27
+ @logger ||= FlashFlow::Config.configuration.logger
28
+ end
29
+
30
+ def check_repo
31
+ if @local_git.staged_and_working_dir_files.any?
32
+ raise RuntimeError.new('You have changes in your working directory. Please stash and try again')
33
+ end
34
+ end
35
+
36
+ def check_version
37
+ data_version = @data.version
38
+ return if data_version.nil?
39
+
40
+ written_version = data_version.split(".").map(&:to_i)
41
+ running_version = FlashFlow::VERSION.split(".").map(&:to_i)
42
+
43
+ unless written_version[0] < running_version[0] ||
44
+ (written_version[0] == running_version[0] && written_version[1] <= running_version[1]) # Ignore the point release number
45
+ raise RuntimeError.new("Your version of flash flow (#{FlashFlow::VERSION}) is behind the version that was last used (#{data_version}) by a member of your team. Please upgrade to at least #{written_version[0]}.#{written_version[1]}.0 and try again.")
46
+ end
47
+ end
48
+
49
+ def merge_branches(branches)
50
+ ordered_branches = MergeOrder.new(@git, branches).get_order
51
+ ordered_branches.each_with_index do |branch, index|
52
+ branch.merge_order = index + 1
53
+
54
+ remote = @git.fetch_remote_for_url(branch.remote_url)
55
+ if remote.nil?
56
+ raise RuntimeError.new("No remote found for #{branch.remote_url}. Please run 'git remote add *your_remote_name* #{branch.remote_url}' and try again.")
57
+ end
58
+
59
+ @git.fetch(branch.remote)
60
+ merger = git_merge(branch)
61
+
62
+ yield(branch, merger)
63
+ end
64
+ end
65
+
66
+ def git_merge(branch)
67
+ merger = BranchMerger.new(@git, branch)
68
+ forget_rerere = is_working_branch(branch) && @rerere_forget
69
+
70
+ merger.do_merge(forget_rerere)
71
+
72
+ merger
73
+ end
74
+
75
+ def is_working_branch(branch)
76
+ branch.ref == @git.working_branch
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,126 @@
1
+ require 'flash_flow/merge/base'
2
+
3
+ module FlashFlow
4
+ module Merge
5
+ class Master < Base
6
+
7
+ class PendingReleaseError < RuntimeError; end
8
+ class NothingToMergeError < RuntimeError; end
9
+ class GitPushFailure < RuntimeError; end
10
+
11
+ def initialize(opts={})
12
+ super(opts)
13
+
14
+ @release_branches = parse_branches(opts[:release_branches])
15
+
16
+ @data = Data::Base.new({}, Config.configuration.branch_info_file, @git, logger: logger)
17
+ end
18
+
19
+ def run
20
+ begin
21
+ check_version
22
+ check_branches
23
+ puts "Merging these branches into #{@git.release_branch}:\n #{@release_branches.map(&:ref).join("\n ")}"
24
+ logger.info "\n\n### Beginning #{@local_git.merge_branch} merge ###\n\n"
25
+
26
+ mergers, errors = [], []
27
+
28
+ @lock.with_lock do
29
+ release = @data.releases.detect { |r| r['status'] == 'Pending' }
30
+ if release
31
+ raise PendingReleaseError.new("There is already a pending release: #{release}")
32
+ elsif @git.branch_exists?("#{@git.merge_remote}/#{@git.release_branch}") &&
33
+ !@git.branch_contains?(@git.master_branch, @git.get_sha("#{@git.merge_remote}/#{@git.release_branch}"))
34
+ raise PendingReleaseError.new("The release branch '#{@git.release_branch}' has commits that are not in master")
35
+ end
36
+
37
+ @git.fetch(@git.merge_remote)
38
+ @git.in_original_merge_branch do
39
+ @git.initialize_rerere
40
+ end
41
+
42
+ @git.reset_temp_merge_branch
43
+ @git.in_temp_merge_branch do
44
+ merge_branches(@release_branches) do |branch, merger|
45
+ mergers << [branch, merger]
46
+ end
47
+ end
48
+
49
+ errors = mergers.select { |m| m.last.result != :success }
50
+
51
+ if errors.empty?
52
+ unless @git.push("#{@git.temp_merge_branch}:#{@git.release_branch}", true)
53
+ raise GitPushFailure.new("Unable to push to #{@git.release_branch}. See log for details.")
54
+ end
55
+
56
+ release_sha = @git.get_sha(@git.temp_merge_branch)
57
+
58
+ @data.releases.unshift({ created_at: Time.now, sha: release_sha, status: 'Pending' })
59
+
60
+ @git.in_temp_merge_branch do
61
+ @git.run("reset --hard #{@git.merge_remote}/#{@git.merge_branch}")
62
+ end
63
+ @git.in_merge_branch do
64
+ @git.run("reset --hard #{@git.merge_remote}/#{@git.merge_branch}")
65
+ end
66
+
67
+ @data.save!
68
+
69
+ @git.copy_temp_to_branch(@git.merge_branch, 'Release data updated [ci skip]')
70
+ @git.delete_temp_merge_branch
71
+ @git.push(@git.merge_branch, false)
72
+ end
73
+ end
74
+
75
+ if errors.empty?
76
+ puts 'Success!'
77
+ else
78
+ raise UnmergeableBranch.new("The following branches didn't merge successfully:\n #{errors.map { |e| e.first.ref }.join("\n ")}")
79
+ end
80
+
81
+ logger.info "### Finished #{@git.release_branch} merge ###"
82
+ rescue Lock::Error, OutOfSyncWithRemote, UnmergeableBranch, GitPushFailure, NothingToMergeError, VersionError, PendingReleaseError => e
83
+ puts 'Failure!'
84
+ puts e.message
85
+ ensure
86
+ @local_git.run("checkout #{@local_git.working_branch}")
87
+ end
88
+ end
89
+
90
+ def branch_data
91
+
92
+ end
93
+
94
+ def parse_branches(user_branches)
95
+ branch_list = user_branches == ['ready'] ? shippable_branch_names : [user_branches].flatten.compact
96
+
97
+ branch_list.map { |b| Data::Branch.new('origin', @git.remotes_hash['origin'], b) }
98
+ end
99
+
100
+ def check_branches
101
+ raise NothingToMergeError.new("Nothing to merge") if @release_branches.empty?
102
+
103
+ requested_not_ready_branches = (@release_branches.map(&:ref) - shippable_branch_names)
104
+ raise RuntimeError.new("The following branches are not ready to ship:\n#{requested_not_ready_branches.join("\n")}") unless requested_not_ready_branches.empty?
105
+ end
106
+
107
+ def shippable_branch_names
108
+ @shippable_branch_names ||= begin
109
+ status = Merge::Status.new(Config.configuration.issue_tracker, Config.configuration.branches, Config.configuration.branch_info_file, Config.configuration.git, logger: logger)
110
+
111
+ all_branches = status.branches
112
+ all_branches.values.select { |b| b[:shippable?] }.map { |b| b[:name] }
113
+ end
114
+ end
115
+
116
+ def commit_message
117
+ message =<<-EOS
118
+ Flash Flow merged these branches:
119
+ #{@release_branches.map(&:ref).join("\n")}
120
+ EOS
121
+ message.gsub(/'/, '')
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -1,7 +1,7 @@
1
1
  require 'graphviz'
2
2
 
3
3
  module FlashFlow
4
- module MergeMaster
4
+ module Merge
5
5
  class ReleaseGraph
6
6
  attr_accessor :branches, :issue_tracker
7
7
 
@@ -1,13 +1,13 @@
1
- require 'flash_flow/merge_master/release_graph'
1
+ require 'flash_flow/merge/release_graph'
2
2
 
3
3
  module FlashFlow
4
- module MergeMaster
4
+ module Merge
5
5
  class Status
6
6
  attr_reader :issue_tracker, :collection, :stories, :releases
7
7
 
8
8
  def initialize(issue_tracker_config, branches_config, branch_info_file, git_config, opts={})
9
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
10
+ @collection = Data::Base.new(branches_config, branch_info_file, ShadowGit.new(git_config)).collection
11
11
  end
12
12
 
13
13
  def status
@@ -20,8 +20,7 @@ module FlashFlow
20
20
  private
21
21
 
22
22
  def current_sha(branch)
23
- @git.run("rev-parse #{branch.remote}/#{branch.ref}")
24
- @git.last_stdout.strip if @git.last_success?
23
+ @git.get_sha("#{branch.remote}/#{branch.ref}")
25
24
  end
26
25
 
27
26
  end
@@ -14,14 +14,12 @@ module FlashFlow
14
14
  user_name = branch.metadata['user_url'].split('/').last
15
15
  user_url_link = %{<a href="#{branch.metadata['user_url']}">#{user_name}</a>}
16
16
  ref_link = %{<a href="#{branch.metadata['repo_url']}/tree/#{branch.ref}">#{branch.ref}</a>}
17
- rescue => e
18
- puts "An error occurred in the hipchat notifier: #{e.message}."
19
- user_url_link = 'Unknown'
20
- ref_link = branch.ref
21
- end
22
17
 
23
- message = %{#{user_url_link}'s branch (#{ref_link}) did not merge successfully}
24
- @client[@room].send("FlashFlow", message)
18
+ message = %{#{user_url_link}'s branch (#{ref_link}) did not merge successfully}
19
+ @client[@room].send("FlashFlow", message)
20
+ rescue HipChat::UnknownResponseCode => e
21
+ puts e.message
22
+ end
25
23
  end
26
24
 
27
25
  private
@@ -0,0 +1,20 @@
1
+ require 'flash_flow/release/percy_client'
2
+
3
+ module FlashFlow
4
+ module Release
5
+ class Base
6
+ def initialize(config=nil)
7
+ release_class_name = config && config['class'] && config['class']['name']
8
+ return unless release_class_name
9
+
10
+ @release_class = Object.const_get(release_class_name)
11
+ @release = @release_class.new(config['class'])
12
+ end
13
+
14
+ def find_latest_by_sha(sha)
15
+ @release.find_latest_by_sha(sha) if @release.respond_to?(:find_latest_by_sha)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,67 @@
1
+ require 'percy'
2
+
3
+ module FlashFlow
4
+ module Release
5
+ class PercyClient
6
+
7
+ def initialize(config={})
8
+ @client = initialize_connection!(config['token'])
9
+ end
10
+
11
+ def find_latest_by_sha(sha)
12
+ response = get_builds
13
+ commit = find_commit_by_sha(response, sha)
14
+ build = find_build_by_commit_id(response, commit['id'])
15
+
16
+ { url: build['web-url'], approved: !build['approved-at'].nil? }
17
+ end
18
+
19
+ private
20
+
21
+ def initialize_connection!(token)
22
+ if token.nil?
23
+ raise RuntimeError.new("Percy token must be set in your flash flow config.")
24
+ end
25
+
26
+ Percy.client.config.access_token = token
27
+ percy_client
28
+ end
29
+
30
+ def percy_client
31
+ Percy.client
32
+ end
33
+
34
+ def get_builds
35
+ @client.get("#{Percy.config.api_url}/repos/#{Percy.config.repo}/builds/")
36
+ end
37
+
38
+ def find_commit_by_sha(response, sha)
39
+ commits_data(response).detect { |h| h.dig('attributes', 'sha') == sha }
40
+ end
41
+
42
+ def find_build_by_commit_id(response, commit_id)
43
+ builds = builds_collection(response)
44
+ return if builds.nil?
45
+
46
+ latest_build = builds
47
+ .select { |b| b.dig('relationships', 'commit', 'data', 'id') == commit_id }
48
+ .sort_by { |b| DateTime.parse(b.dig('attributes', 'created-at')) }.last
49
+
50
+ latest_build['attributes']
51
+ end
52
+
53
+ def builds_collection(response)
54
+ response['data'].select do |h|
55
+ h['type'] == 'builds' &&
56
+ h.dig('attributes', 'web-url') &&
57
+ h.dig('relationships', 'commit', 'data', 'type') == 'commits'
58
+ end
59
+ end
60
+
61
+ def commits_data(response)
62
+ response['included'].select { |data| data['type'] == 'commits' }
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -13,6 +13,7 @@ module FlashFlow
13
13
 
14
14
  run("clean -x -f")
15
15
  fetch(merge_remote)
16
+ run("remote prune #{merge_remote}")
16
17
  run("reset --hard HEAD")
17
18
  end
18
19
 
@@ -1,3 +1,3 @@
1
1
  module FlashFlow
2
- VERSION = "1.3.2.1"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -0,0 +1,191 @@
1
+ require 'minitest_helper'
2
+ require 'minitest/stub_any_instance'
3
+
4
+ module FlashFlow
5
+ module Merge
6
+ class TestAcceptance < Minitest::Test
7
+
8
+ def setup
9
+ reset_config!
10
+ config!(git:
11
+ {
12
+ 'merge_branch' => 'test_acceptance',
13
+ 'merge_remote' => 'test_remote',
14
+ 'master_branch' => 'test_master',
15
+ 'remotes' => ['fake_origin'],
16
+ 'use_rerere' => true
17
+ },
18
+ branches: {}
19
+ )
20
+
21
+ @branch = Data::Branch.from_hash({'ref' => 'pushing_branch', 'remote' => 'origin', 'status' => 'fail', 'stories' => []})
22
+ @deploy = Acceptance.new
23
+ end
24
+
25
+ def test_version_is_nil
26
+ with_versions('1.1.1', nil) do
27
+ assert_nil(@deploy.check_version)
28
+ end
29
+ end
30
+
31
+ def test_check_version_greater
32
+ with_versions('2.0.0', '1.1.1') do
33
+ assert_nil(@deploy.check_version)
34
+ end
35
+
36
+ with_versions('1.2.0', '1.1.1') do
37
+ assert_nil(@deploy.check_version)
38
+ end
39
+ end
40
+
41
+ def test_check_version_less_raises
42
+ with_versions('1.1.1', '2.1.0') do
43
+ assert_raises(RuntimeError) { @deploy.check_version }
44
+ end
45
+
46
+ with_versions('1.2.0', '2.1.0') do
47
+ assert_raises(RuntimeError) { @deploy.check_version }
48
+ end
49
+ end
50
+
51
+ def test_check_version_equal
52
+
53
+ end
54
+
55
+ def test_print_errors_with_no_errors
56
+ data.expect(:failures, [])
57
+ assert_equal(@deploy.format_errors, 'Success!')
58
+ end
59
+
60
+ def test_print_errors_when_current_branch_cant_merge
61
+ data.expect(:failures, [@branch])
62
+ @branch.fail!('some_random_sha')
63
+
64
+ current_branch_error = "ERROR: Your branch did not merge to test_acceptance. Run 'flash_flow --resolve', fix the merge conflict(s) and then re-run this script\n"
65
+
66
+ @deploy.instance_variable_get('@local_git'.to_sym).stub(:working_branch, 'pushing_branch') do
67
+ assert_equal(current_branch_error, @deploy.format_errors)
68
+ end
69
+ end
70
+
71
+ def test_print_errors_when_another_branch_cant_merge
72
+ data.expect(:failures, [@branch])
73
+
74
+ other_branch_error = "WARNING: Unable to merge branch origin/pushing_branch to test_acceptance due to conflicts."
75
+
76
+ assert_equal(@deploy.format_errors, other_branch_error)
77
+ end
78
+
79
+ def test_check_out_to_working_branch
80
+ @deploy.stub(:check_repo, true) do
81
+ @deploy.stub(:check_version, true) do
82
+ Lock::Base.stub_any_instance(:with_lock, -> { raise Lock::Error }) do
83
+ assert_output(/Failure!/) { @deploy.run }
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ def test_deleted_branch
90
+ data.expect(:mark_deleted, true, [@branch])
91
+
92
+ notifier.expect(:deleted_branch, true, [@branch])
93
+
94
+ merger.expect(:result, :deleted)
95
+
96
+ @deploy.process_result(@branch, merger)
97
+
98
+ notifier.verify
99
+ data.verify
100
+ merger.verify
101
+ end
102
+
103
+ def test_merge_conflict
104
+ data.expect(:mark_failure, true, [@branch, nil])
105
+
106
+ notifier.expect(:merge_conflict, true, [@branch])
107
+
108
+ merger.expect(:result, :conflict)
109
+
110
+ @deploy.process_result(@branch, merger)
111
+
112
+ notifier.verify
113
+ data.verify
114
+ merger.verify
115
+ end
116
+
117
+ def test_successful_merge
118
+ data.expect(:mark_success, true, [@branch])
119
+ data.expect(:set_resolutions, true, [@branch, {'filename' => ["resolution_sha"]}])
120
+
121
+ merger.
122
+ expect(:result, :success).
123
+ expect(:sha, 'sha').
124
+ expect(:resolutions, {'filename' => ["resolution_sha"]})
125
+
126
+ @deploy.process_result(@branch, merger)
127
+
128
+ data.verify
129
+ merger.verify
130
+ assert_equal(@branch.sha, 'sha')
131
+ end
132
+
133
+ def test_ignore_pushing_master_or_acceptance
134
+ ['test_master', 'test_acceptance'].each do |branch|
135
+ @deploy.instance_variable_get('@local_git'.to_sym).stub(:working_branch, branch) do
136
+ refute(@deploy.open_pull_request)
137
+ end
138
+ end
139
+ end
140
+
141
+ def test_commit_message_ordering
142
+ data
143
+ .expect(:successes, ['data_successes'])
144
+ .expect(:successes, sample_branches)
145
+ .expect(:failures, [])
146
+ .expect(:removals, [])
147
+
148
+ expected_message = sample_branches.sort_by(&:merge_order).map(&:ref).join("\n")
149
+ assert_output(/#{expected_message}/) { print @deploy.commit_message }
150
+ data.verify
151
+ end
152
+
153
+ private
154
+
155
+ def with_versions(current, written)
156
+ original_version = FlashFlow::VERSION
157
+ FlashFlow.send(:remove_const, :VERSION)
158
+ FlashFlow.const_set(:VERSION, current)
159
+ data.expect(:version, written)
160
+ yield
161
+ data.verify
162
+ FlashFlow.send(:remove_const, :VERSION)
163
+ FlashFlow.const_set(:VERSION, original_version)
164
+ end
165
+
166
+ def merger
167
+ @merger ||= Minitest::Mock.new
168
+ end
169
+
170
+ def notifier
171
+ return @notifier if @notifier
172
+
173
+ @notifier = Minitest::Mock.new
174
+ @deploy.instance_variable_set('@notifier'.to_sym, @notifier)
175
+ end
176
+
177
+ def data
178
+ return @data if @data
179
+
180
+ @data = Minitest::Mock.new
181
+ @deploy.instance_variable_set('@data'.to_sym, @data)
182
+ end
183
+
184
+ def sample_branches
185
+ @sample_branches ||= [Data::Branch.from_hash({'ref' => 'branch3', 'merge_order' => 2}),
186
+ Data::Branch.from_hash({'ref' => 'branch1', 'merge_order' => 3}),
187
+ Data::Branch.from_hash({'ref' => 'branch2', 'merge_order' => 1})]
188
+ end
189
+ end
190
+ end
191
+ end