flash_flow 1.0.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.
- 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
|