flash_flow 1.3.2.1 → 1.4.0

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