flash_flow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +10 -0
- data/bin/flash_flow +23 -0
- data/flash_flow.gemspec +28 -0
- data/flash_flow.yml.erb.example +83 -0
- data/lib/flash_flow.rb +7 -0
- data/lib/flash_flow/branch_merger.rb +52 -0
- data/lib/flash_flow/cmd_runner.rb +37 -0
- data/lib/flash_flow/config.rb +71 -0
- data/lib/flash_flow/data.rb +6 -0
- data/lib/flash_flow/data/base.rb +58 -0
- data/lib/flash_flow/data/branch.rb +131 -0
- data/lib/flash_flow/data/collection.rb +181 -0
- data/lib/flash_flow/data/github.rb +129 -0
- data/lib/flash_flow/data/store.rb +33 -0
- data/lib/flash_flow/deploy.rb +184 -0
- data/lib/flash_flow/git.rb +248 -0
- data/lib/flash_flow/install.rb +19 -0
- data/lib/flash_flow/issue_tracker.rb +52 -0
- data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
- data/lib/flash_flow/lock.rb +25 -0
- data/lib/flash_flow/lock/github.rb +91 -0
- data/lib/flash_flow/notifier.rb +24 -0
- data/lib/flash_flow/notifier/hipchat.rb +36 -0
- data/lib/flash_flow/options.rb +36 -0
- data/lib/flash_flow/time_helper.rb +11 -0
- data/lib/flash_flow/version.rb +3 -0
- data/test/lib/data/test_base.rb +10 -0
- data/test/lib/data/test_branch.rb +203 -0
- data/test/lib/data/test_collection.rb +238 -0
- data/test/lib/data/test_github.rb +23 -0
- data/test/lib/data/test_store.rb +53 -0
- data/test/lib/issue_tracker/test_pivotal.rb +221 -0
- data/test/lib/lock/test_github.rb +70 -0
- data/test/lib/test_branch_merger.rb +76 -0
- data/test/lib/test_config.rb +84 -0
- data/test/lib/test_deploy.rb +175 -0
- data/test/lib/test_git.rb +73 -0
- data/test/lib/test_issue_tracker.rb +43 -0
- data/test/lib/test_notifier.rb +33 -0
- data/test/minitest_helper.rb +38 -0
- metadata +217 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'flash_flow/data/branch'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
class Github
|
7
|
+
|
8
|
+
attr_accessor :repo, :unmergeable_label
|
9
|
+
|
10
|
+
def initialize(config={})
|
11
|
+
initialize_connection!(config['token'])
|
12
|
+
@repo = config['repo']
|
13
|
+
@master_branch = config['master_branch'] || master
|
14
|
+
@unmergeable_label = config['unmergeable_label'] || 'unmergeable'
|
15
|
+
@do_not_merge_label = config['do_not_merge_label'] || 'do not merge'
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_connection!(token)
|
19
|
+
if token.nil?
|
20
|
+
raise RuntimeError.new("Github token must be set in your flash_flow config file.")
|
21
|
+
end
|
22
|
+
octokit.configure do |c|
|
23
|
+
c.access_token = token
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_from_merge(branch)
|
28
|
+
pr = pr_for(branch)
|
29
|
+
if pr && @do_not_merge_label
|
30
|
+
add_label(pr.number, @do_not_merge_label)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def fetch
|
35
|
+
pull_requests.map do |pr|
|
36
|
+
Branch.from_hash(
|
37
|
+
'remote_url' => pr.head.repo.ssh_url,
|
38
|
+
'ref' => pr.head.ref,
|
39
|
+
'status' => status_from_labels(pr),
|
40
|
+
'metadata' => metadata(pr)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_to_merge(branch)
|
46
|
+
pr = pr_for(branch)
|
47
|
+
|
48
|
+
pr ||= create_pr(branch.ref, branch.ref, branch.ref)
|
49
|
+
branch.add_metadata(metadata(pr))
|
50
|
+
|
51
|
+
if pr && @do_not_merge_label
|
52
|
+
remove_label(pr.number, @do_not_merge_label)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def mark_success(branch)
|
57
|
+
remove_label(branch.metadata['pr_number'], @unmergeable_label)
|
58
|
+
end
|
59
|
+
|
60
|
+
def mark_failure(branch)
|
61
|
+
add_label(branch.metadata['pr_number'], @unmergeable_label)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def status_from_labels(pull_request)
|
67
|
+
case
|
68
|
+
when has_label?(pull_request.number, @do_not_merge_label)
|
69
|
+
'removed'
|
70
|
+
when has_label?(pull_request.number, @unmergeable_label)
|
71
|
+
'fail'
|
72
|
+
else
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def pr_for(branch)
|
78
|
+
pull_requests.detect { |p| branch.remote_url == p.head.repo.ssh_url && branch.ref == p.head.ref }
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_pr(pr_number)
|
82
|
+
octokit.update_pull_request(repo, pr_number, {})
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_pr(branch, title, body)
|
86
|
+
pr = octokit.create_pull_request(repo, @master_branch, branch, title, body)
|
87
|
+
pull_requests << pr
|
88
|
+
pr
|
89
|
+
end
|
90
|
+
|
91
|
+
def pull_requests
|
92
|
+
@pull_requests ||= octokit.pull_requests(repo).sort_by(&:updated_at)
|
93
|
+
end
|
94
|
+
|
95
|
+
def remove_label(pull_request_number, label)
|
96
|
+
if has_label?(pull_request_number, label)
|
97
|
+
octokit.remove_label(repo, pull_request_number, label)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_label(pull_request_number, label)
|
102
|
+
unless has_label?(pull_request_number, label)
|
103
|
+
octokit.add_labels_to_an_issue(repo, pull_request_number, [label])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def has_label?(pull_request_number, label_name)
|
108
|
+
labels(pull_request_number).detect { |label| label.name == label_name }
|
109
|
+
end
|
110
|
+
|
111
|
+
def labels(pull_request_number)
|
112
|
+
@labels ||= {}
|
113
|
+
@labels[pull_request_number] ||= octokit.labels_for_issue(repo, pull_request_number)
|
114
|
+
end
|
115
|
+
|
116
|
+
def metadata(pr)
|
117
|
+
{
|
118
|
+
'pr_number' => pr.number,
|
119
|
+
'user_url' => pr.user.html_url,
|
120
|
+
'repo_url' => pr.head.repo.html_url
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
def octokit
|
125
|
+
Octokit
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'flash_flow/data'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
class Store
|
7
|
+
def initialize(filename, git, opts={})
|
8
|
+
@filename = filename
|
9
|
+
@git = git
|
10
|
+
@logger = opts[:logger] || Logger.new('/dev/null')
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
file_contents = @git.read_file_from_merge_branch(@filename)
|
15
|
+
JSON.parse(file_contents)
|
16
|
+
|
17
|
+
rescue JSON::ParserError, Errno::ENOENT
|
18
|
+
@logger.error "Unable to read branch info from file: #{@filename}"
|
19
|
+
{}
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(branches, file=nil)
|
23
|
+
@git.in_temp_merge_branch do
|
24
|
+
file ||= File.open(@filename, 'w')
|
25
|
+
file.puts JSON.pretty_generate(branches)
|
26
|
+
file.close
|
27
|
+
|
28
|
+
@git.add_and_commit(@filename, 'Branch Info', add: { force: true })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,184 @@
|
|
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
|
+
|
9
|
+
module FlashFlow
|
10
|
+
class Deploy
|
11
|
+
|
12
|
+
class OutOfSyncWithRemote < RuntimeError ; end
|
13
|
+
|
14
|
+
def initialize(opts={})
|
15
|
+
@do_not_merge = opts[:do_not_merge]
|
16
|
+
@force = opts[:force]
|
17
|
+
@rerere_forget = opts[:rerere_forget]
|
18
|
+
@stories = [opts[:stories]].flatten.compact
|
19
|
+
|
20
|
+
@git = Git.new(Config.configuration.git, logger)
|
21
|
+
@lock = Lock::Base.new(Config.configuration.lock)
|
22
|
+
@notifier = Notifier::Base.new(Config.configuration.notifier)
|
23
|
+
@data = Data::Base.new(Config.configuration.branches, Config.configuration.branch_info_file, @git, logger: logger)
|
24
|
+
end
|
25
|
+
|
26
|
+
def logger
|
27
|
+
@logger ||= FlashFlow::Config.configuration.logger
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
check_version
|
32
|
+
check_repo
|
33
|
+
puts "Building #{@git.merge_branch}... Log can be found in #{FlashFlow::Config.configuration.log_file}"
|
34
|
+
logger.info "\n\n### Beginning #{@git.merge_branch} merge ###\n\n"
|
35
|
+
|
36
|
+
fetch(@git.merge_remote)
|
37
|
+
@git.in_original_merge_branch do
|
38
|
+
@git.initialize_rerere
|
39
|
+
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
@lock.with_lock do
|
43
|
+
open_pull_request
|
44
|
+
|
45
|
+
@git.reset_temp_merge_branch
|
46
|
+
@git.in_temp_merge_branch do
|
47
|
+
merge_branches
|
48
|
+
commit_branch_info
|
49
|
+
commit_rerere
|
50
|
+
end
|
51
|
+
|
52
|
+
@git.copy_temp_to_merge_branch
|
53
|
+
@git.delete_temp_merge_branch
|
54
|
+
@git.push_merge_branch
|
55
|
+
end
|
56
|
+
|
57
|
+
print_errors
|
58
|
+
logger.info "### Finished #{@git.merge_branch} merge ###"
|
59
|
+
rescue Lock::Error, OutOfSyncWithRemote => e
|
60
|
+
puts 'Failure!'
|
61
|
+
puts e.message
|
62
|
+
ensure
|
63
|
+
@git.run("checkout #{@git.working_branch}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_repo
|
68
|
+
if @git.staged_and_working_dir_files.any?
|
69
|
+
raise RuntimeError.new('You have changes in your working directory. Please stash and try again')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_version
|
74
|
+
data_version = @data.version
|
75
|
+
return if data_version.nil?
|
76
|
+
|
77
|
+
written_version = data_version.split(".").map(&:to_i)
|
78
|
+
running_version = FlashFlow::VERSION.split(".").map(&:to_i)
|
79
|
+
|
80
|
+
unless written_version[0] < running_version[0] ||
|
81
|
+
(written_version[0] == running_version[0] && written_version[1] <= running_version[1]) # Ignore the point release number
|
82
|
+
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.")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def commit_branch_info
|
87
|
+
@stories.each do |story_id|
|
88
|
+
@data.add_story(@git.merge_remote, @git.working_branch, story_id)
|
89
|
+
end
|
90
|
+
@data.save!
|
91
|
+
end
|
92
|
+
|
93
|
+
def commit_rerere
|
94
|
+
current_branches = @data.merged_branches.to_a.select { |branch| !@git.master_branch_contains?(branch.sha) && (Time.now - branch.updated_at < two_weeks) }
|
95
|
+
current_rereres = current_branches.map { |branch| branch.resolutions.to_h.values }.flatten
|
96
|
+
|
97
|
+
@git.commit_rerere(current_rereres)
|
98
|
+
end
|
99
|
+
|
100
|
+
def two_weeks
|
101
|
+
60 * 60 * 24 * 14
|
102
|
+
end
|
103
|
+
|
104
|
+
def merge_branches
|
105
|
+
@data.mergeable.each do |branch|
|
106
|
+
remote = @git.fetch_remote_for_url(branch.remote_url)
|
107
|
+
if remote.nil?
|
108
|
+
raise RuntimeError.new("No remote found for #{branch.remote_url}. Please run 'git remote add *your_remote_name* #{branch.remote_url}' and try again.")
|
109
|
+
end
|
110
|
+
|
111
|
+
fetch(branch.remote)
|
112
|
+
git_merge(branch, branch.ref == @git.working_branch)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def git_merge(branch, is_working_branch)
|
117
|
+
merger = BranchMerger.new(@git, branch)
|
118
|
+
forget_rerere = is_working_branch && @rerere_forget
|
119
|
+
|
120
|
+
case merger.do_merge(forget_rerere)
|
121
|
+
when :deleted
|
122
|
+
@data.mark_deleted(branch)
|
123
|
+
@notifier.deleted_branch(branch) unless is_working_branch
|
124
|
+
|
125
|
+
when :success
|
126
|
+
branch.sha = merger.sha
|
127
|
+
@data.mark_success(branch)
|
128
|
+
@data.set_resolutions(branch, merger.resolutions)
|
129
|
+
|
130
|
+
when :conflict
|
131
|
+
@data.mark_failure(branch, merger.conflict_sha)
|
132
|
+
@notifier.merge_conflict(branch) unless is_working_branch
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def open_pull_request
|
137
|
+
return false if [@git.master_branch, @git.merge_branch].include?(@git.working_branch)
|
138
|
+
|
139
|
+
# TODO - This should use the actual remote for the branch we're on
|
140
|
+
@git.push(@git.working_branch, force: @force)
|
141
|
+
raise OutOfSyncWithRemote.new("Your branch is out of sync with the remote. If you want to force push, run 'flash_flow -f'") unless @git.last_success?
|
142
|
+
|
143
|
+
# TODO - This should use the actual remote for the branch we're on
|
144
|
+
if @do_not_merge
|
145
|
+
@data.remove_from_merge(@git.merge_remote, @git.working_branch)
|
146
|
+
else
|
147
|
+
@data.add_to_merge(@git.merge_remote, @git.working_branch)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def print_errors
|
152
|
+
puts format_errors
|
153
|
+
end
|
154
|
+
|
155
|
+
def format_errors
|
156
|
+
errors = []
|
157
|
+
branch_not_merged = nil
|
158
|
+
@data.failures.each do |full_ref, failure|
|
159
|
+
if failure.ref == @git.working_branch
|
160
|
+
branch_not_merged = "\nERROR: Your branch did not merge to #{@git.merge_branch}. Run the following commands to fix the merge conflict and then re-run this script:\n\n git checkout #{failure.metadata['conflict_sha']}\n git merge #{@git.working_branch}\n # Resolve the conflicts\n git add <conflicted files>\n git commit --no-edit"
|
161
|
+
else
|
162
|
+
errors << "WARNING: Unable to merge branch #{failure.remote}/#{failure.ref} to #{@git.merge_branch} due to conflicts."
|
163
|
+
end
|
164
|
+
end
|
165
|
+
errors << branch_not_merged if branch_not_merged
|
166
|
+
|
167
|
+
if errors.empty?
|
168
|
+
"Success!"
|
169
|
+
else
|
170
|
+
errors.join("\n")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def fetch(remote)
|
177
|
+
@fetched_remotes ||= {}
|
178
|
+
unless @fetched_remotes[remote]
|
179
|
+
@git.fetch(remote)
|
180
|
+
@fetched_remotes[remote] = true
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'flash_flow/cmd_runner'
|
2
|
+
|
3
|
+
module FlashFlow
|
4
|
+
class Git
|
5
|
+
ATTRIBUTES = [:merge_remote, :merge_branch, :master_branch, :use_rerere]
|
6
|
+
attr_reader *ATTRIBUTES
|
7
|
+
attr_reader :working_branch
|
8
|
+
|
9
|
+
UNMERGED_STATUSES = %w{DD AU UD UA DU AA UU}
|
10
|
+
|
11
|
+
def initialize(config, logger=nil)
|
12
|
+
@cmd_runner = CmdRunner.new(logger: logger)
|
13
|
+
|
14
|
+
ATTRIBUTES.each do |attr|
|
15
|
+
unless config.has_key?(attr.to_s)
|
16
|
+
raise RuntimeError.new("git configuration missing. Required config parameters: #{ATTRIBUTES}")
|
17
|
+
end
|
18
|
+
|
19
|
+
instance_variable_set("@#{attr}", config[attr.to_s])
|
20
|
+
end
|
21
|
+
|
22
|
+
@working_branch = current_branch
|
23
|
+
end
|
24
|
+
|
25
|
+
def last_stdout
|
26
|
+
@cmd_runner.last_stdout
|
27
|
+
end
|
28
|
+
|
29
|
+
def last_command
|
30
|
+
@cmd_runner.last_command
|
31
|
+
end
|
32
|
+
|
33
|
+
def last_success?
|
34
|
+
@cmd_runner.last_success?
|
35
|
+
end
|
36
|
+
|
37
|
+
def run(cmd)
|
38
|
+
@cmd_runner.run("git #{cmd}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_and_commit(files, message, opts={})
|
42
|
+
files = [files].flatten
|
43
|
+
run("add #{'-f ' if opts[:add] && opts[:add][:force]}#{files.join(' ')}")
|
44
|
+
run("commit -m '#{message}'")
|
45
|
+
end
|
46
|
+
|
47
|
+
def push(branch, options)
|
48
|
+
run("push #{'-f' if options[:force]} #{merge_remote} #{branch}")
|
49
|
+
end
|
50
|
+
|
51
|
+
def merge(branch)
|
52
|
+
run("merge #{branch}")
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch(remote)
|
56
|
+
run("fetch #{remote}")
|
57
|
+
end
|
58
|
+
|
59
|
+
def master_branch_contains?(ref)
|
60
|
+
run("branch --contains #{ref}")
|
61
|
+
last_stdout.split("\n").detect { |str| str[2..-1] == master_branch }
|
62
|
+
end
|
63
|
+
|
64
|
+
def in_original_merge_branch
|
65
|
+
begin
|
66
|
+
starting_branch = current_branch
|
67
|
+
run("checkout #{merge_remote}/#{merge_branch}")
|
68
|
+
|
69
|
+
yield
|
70
|
+
ensure
|
71
|
+
run("checkout #{starting_branch}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def read_file_from_merge_branch(filename)
|
76
|
+
run("show #{merge_remote}/#{merge_branch}:#{filename}")
|
77
|
+
last_stdout
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize_rerere
|
81
|
+
return unless use_rerere
|
82
|
+
|
83
|
+
@cmd_runner.run('mkdir .git/rr-cache')
|
84
|
+
@cmd_runner.run('cp -R rr-cache/* .git/rr-cache/')
|
85
|
+
end
|
86
|
+
|
87
|
+
def commit_rerere(current_rereres)
|
88
|
+
return unless use_rerere
|
89
|
+
@cmd_runner.run('mkdir rr-cache')
|
90
|
+
@cmd_runner.run('rm -rf rr-cache/*')
|
91
|
+
current_rereres.each do |rerere|
|
92
|
+
@cmd_runner.run("cp -R .git/rr-cache/#{rerere} rr-cache/")
|
93
|
+
end
|
94
|
+
|
95
|
+
run('add rr-cache/')
|
96
|
+
run("commit -m 'Update rr-cache'")
|
97
|
+
end
|
98
|
+
|
99
|
+
def rerere_resolve!
|
100
|
+
return false unless use_rerere
|
101
|
+
|
102
|
+
merging_files = staged_and_working_dir_files.select { |s| UNMERGED_STATUSES.include?(s[0..1]) }.map { |s| s[3..-1] }
|
103
|
+
|
104
|
+
conflicts = merging_files.map do |file|
|
105
|
+
File.open(file) { |f| f.grep(/>>>>/) }
|
106
|
+
end
|
107
|
+
|
108
|
+
if conflicts.all? { |c| c.empty? }
|
109
|
+
run("add #{merging_files.join(" ")}")
|
110
|
+
run('commit --no-edit')
|
111
|
+
|
112
|
+
resolutions(merging_files)
|
113
|
+
else
|
114
|
+
false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def resolutions(files)
|
119
|
+
{}.tap do |hash|
|
120
|
+
files.map do |file|
|
121
|
+
hash[file] = resolution_candidates(file)
|
122
|
+
end.flatten
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# git rerere doesn't give you a deterministic way to determine which resolution was used
|
127
|
+
def resolution_candidates(file)
|
128
|
+
@cmd_runner.run("diff -q --from-file #{file} .git/rr-cache/*/postimage")
|
129
|
+
different_files = split_diff_lines(@cmd_runner.last_stdout)
|
130
|
+
|
131
|
+
@cmd_runner.run('ls -la .git/rr-cache/*/postimage')
|
132
|
+
all_files = split_diff_lines(@cmd_runner.last_stdout)
|
133
|
+
|
134
|
+
all_files - different_files
|
135
|
+
end
|
136
|
+
|
137
|
+
def split_diff_lines(arr)
|
138
|
+
arr.split("\n").map { |s| s.split(".git/rr-cache/").last.split("/postimage").first }
|
139
|
+
end
|
140
|
+
|
141
|
+
def remotes
|
142
|
+
run('remote -v')
|
143
|
+
last_stdout.split("\n")
|
144
|
+
end
|
145
|
+
|
146
|
+
def remotes_hash
|
147
|
+
return @remotes_hash if @remotes_hash
|
148
|
+
|
149
|
+
@remotes_hash = {}
|
150
|
+
remotes.each do |r|
|
151
|
+
name = r.split[0]
|
152
|
+
url = r.split[1]
|
153
|
+
@remotes_hash[name] ||= url
|
154
|
+
end
|
155
|
+
@remotes_hash
|
156
|
+
end
|
157
|
+
|
158
|
+
def fetch_remote_for_url(url)
|
159
|
+
fetch_remotes = remotes.grep(Regexp.new(url)).grep(/ \(fetch\)/)
|
160
|
+
fetch_remotes.map { |remote| remote.to_s.split("\t").first }.first
|
161
|
+
end
|
162
|
+
|
163
|
+
def staged_and_working_dir_files
|
164
|
+
run("status --porcelain")
|
165
|
+
last_stdout.split("\n").reject { |line| line[0..1] == '??' }
|
166
|
+
end
|
167
|
+
|
168
|
+
def current_branch
|
169
|
+
run("rev-parse --abbrev-ref HEAD")
|
170
|
+
last_stdout.strip
|
171
|
+
end
|
172
|
+
|
173
|
+
def most_recent_commit
|
174
|
+
run("show -s --format=%cd head")
|
175
|
+
end
|
176
|
+
|
177
|
+
def reset_temp_merge_branch
|
178
|
+
in_branch(master_branch) do
|
179
|
+
run("fetch #{merge_remote}")
|
180
|
+
run("branch -D #{temp_merge_branch}")
|
181
|
+
run("checkout -b #{temp_merge_branch}")
|
182
|
+
run("reset --hard #{merge_remote}/#{master_branch}")
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def push_merge_branch
|
187
|
+
run("push -f #{merge_remote} #{merge_branch}")
|
188
|
+
end
|
189
|
+
|
190
|
+
def copy_temp_to_merge_branch
|
191
|
+
run("checkout #{temp_merge_branch}")
|
192
|
+
run("merge --strategy=ours --no-edit #{merge_branch}")
|
193
|
+
run("checkout #{merge_branch}")
|
194
|
+
run("merge #{temp_merge_branch}")
|
195
|
+
|
196
|
+
squash_commits
|
197
|
+
end
|
198
|
+
|
199
|
+
def commit_message(log)
|
200
|
+
"Flash Flow run from branch: #{working_branch}\n\n#{log}".gsub(/'/, '')
|
201
|
+
end
|
202
|
+
|
203
|
+
def delete_temp_merge_branch
|
204
|
+
in_merge_branch do
|
205
|
+
run("branch -d #{temp_merge_branch}")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def in_temp_merge_branch(&block)
|
210
|
+
in_branch(temp_merge_branch, &block)
|
211
|
+
end
|
212
|
+
|
213
|
+
def in_merge_branch(&block)
|
214
|
+
in_branch(merge_branch, &block)
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def squash_commits
|
220
|
+
# There are three commits created by flash flow that we don't need in the message
|
221
|
+
run("log #{merge_remote}/#{merge_branch}..#{merge_branch}~3")
|
222
|
+
log = last_stdout
|
223
|
+
|
224
|
+
# Get all the files that differ between existing acceptance and new acceptance
|
225
|
+
run("diff --name-only #{merge_remote}/#{merge_branch} #{merge_branch}")
|
226
|
+
files = last_stdout.split("\n")
|
227
|
+
run("reset #{merge_remote}/#{merge_branch}")
|
228
|
+
run("add #{files.map { |f| "'#{f}'" }.join(" ")}")
|
229
|
+
|
230
|
+
run("commit -m '#{commit_message(log)}'")
|
231
|
+
end
|
232
|
+
|
233
|
+
def temp_merge_branch
|
234
|
+
"flash_flow/#{merge_branch}"
|
235
|
+
end
|
236
|
+
|
237
|
+
def in_branch(branch)
|
238
|
+
begin
|
239
|
+
starting_branch = current_branch
|
240
|
+
run("checkout #{branch}")
|
241
|
+
|
242
|
+
yield
|
243
|
+
ensure
|
244
|
+
run("checkout #{starting_branch}")
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|