daq_flow 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +62 -0
- data/LICENSE.txt +22 -0
- data/README.md +165 -0
- data/Rakefile +10 -0
- data/bin/daq_flow +23 -0
- data/flash_flow.gemspec +28 -0
- data/flash_flow.yml.erb.example +42 -0
- data/lib/flash_flow.rb +7 -0
- data/lib/flash_flow/branch_merger.rb +55 -0
- data/lib/flash_flow/cmd_runner.rb +54 -0
- data/lib/flash_flow/config.rb +84 -0
- data/lib/flash_flow/data.rb +6 -0
- data/lib/flash_flow/data/base.rb +89 -0
- data/lib/flash_flow/data/bitbucket.rb +152 -0
- data/lib/flash_flow/data/branch.rb +124 -0
- data/lib/flash_flow/data/collection.rb +211 -0
- data/lib/flash_flow/data/github.rb +140 -0
- data/lib/flash_flow/data/store.rb +44 -0
- data/lib/flash_flow/git.rb +267 -0
- data/lib/flash_flow/install.rb +19 -0
- data/lib/flash_flow/lock.rb +23 -0
- data/lib/flash_flow/merge.rb +6 -0
- data/lib/flash_flow/merge/acceptance.rb +154 -0
- data/lib/flash_flow/merge/base.rb +116 -0
- data/lib/flash_flow/merge_order.rb +27 -0
- data/lib/flash_flow/notifier.rb +23 -0
- data/lib/flash_flow/options.rb +34 -0
- data/lib/flash_flow/resolve.rb +143 -0
- data/lib/flash_flow/shadow_repo.rb +44 -0
- data/lib/flash_flow/time_helper.rb +32 -0
- data/lib/flash_flow/version.rb +4 -0
- data/log/.keep +0 -0
- data/test/lib/data/test_base.rb +10 -0
- data/test/lib/data/test_branch.rb +206 -0
- data/test/lib/data/test_collection.rb +308 -0
- data/test/lib/data/test_store.rb +70 -0
- data/test/lib/lock/test_github.rb +74 -0
- data/test/lib/merge/test_acceptance.rb +230 -0
- data/test/lib/test_branch_merger.rb +78 -0
- data/test/lib/test_config.rb +63 -0
- data/test/lib/test_git.rb +73 -0
- data/test/lib/test_merge_order.rb +71 -0
- data/test/lib/test_notifier.rb +33 -0
- data/test/lib/test_resolve.rb +69 -0
- data/test/minitest_helper.rb +41 -0
- data/update_gem.sh +5 -0
- metadata +192 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'flash_flow/data/branch'
|
2
|
+
require 'flash_flow/data/bitbucket'
|
3
|
+
require 'flash_flow/data/github'
|
4
|
+
|
5
|
+
module FlashFlow
|
6
|
+
module Data
|
7
|
+
|
8
|
+
class Collection
|
9
|
+
|
10
|
+
attr_accessor :branches
|
11
|
+
|
12
|
+
def initialize(config=nil)
|
13
|
+
@branches = {}
|
14
|
+
|
15
|
+
if config && config['class'] && config['class']['name']
|
16
|
+
collection_class = Object.const_get(config['class']['name'])
|
17
|
+
@collection_instance = collection_class.new(config['class'])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.fetch(config=nil)
|
22
|
+
collection = new(config)
|
23
|
+
collection.fetch
|
24
|
+
collection
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.from_hash(hash, collection_instance=nil)
|
28
|
+
collection = new
|
29
|
+
collection.branches = branches_from_hash(hash)
|
30
|
+
collection.instance_variable_set(:@collection_instance, collection_instance)
|
31
|
+
collection
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.branches_from_hash(hash)
|
35
|
+
{}.tap do |new_branches|
|
36
|
+
hash.each do |_, val|
|
37
|
+
branch = val.is_a?(Branch) ? val : Branch.from_hash(val)
|
38
|
+
new_branches[branch.ref] = branch
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.key(ref)
|
44
|
+
ref
|
45
|
+
end
|
46
|
+
|
47
|
+
def get(ref)
|
48
|
+
@branches[key(ref)]
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_hash
|
52
|
+
{}.tap do |hash|
|
53
|
+
@branches.each do |key, val|
|
54
|
+
hash[key] = val.to_hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
alias :to_h :to_hash
|
59
|
+
|
60
|
+
def reverse_merge(old)
|
61
|
+
merged_branches = @branches.dup
|
62
|
+
|
63
|
+
merged_branches.each do |_, info|
|
64
|
+
info.updated_at = Time.now
|
65
|
+
info.created_at ||= Time.now
|
66
|
+
end
|
67
|
+
|
68
|
+
old.branches.each do |full_ref, info|
|
69
|
+
if merged_branches.has_key?(full_ref)
|
70
|
+
branch = merged_branches[full_ref]
|
71
|
+
|
72
|
+
branch.created_at = info.created_at
|
73
|
+
branch.resolutions = info.resolutions.to_h.merge(branch.resolutions.to_h)
|
74
|
+
branch.stories = info.stories.to_a | merged_branches[full_ref].stories.to_a
|
75
|
+
branch.merge_order ||= info.merge_order
|
76
|
+
if branch.fail?
|
77
|
+
branch.conflict_sha ||= info.conflict_sha
|
78
|
+
end
|
79
|
+
else
|
80
|
+
merged_branches[full_ref] = info
|
81
|
+
merged_branches[full_ref].status = nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
self.class.from_hash(merged_branches, @collection_instance)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_a
|
89
|
+
@branches.values
|
90
|
+
end
|
91
|
+
|
92
|
+
def each
|
93
|
+
to_a.each
|
94
|
+
end
|
95
|
+
|
96
|
+
def current_branches
|
97
|
+
to_a.select { |branch| branch.current_record }
|
98
|
+
end
|
99
|
+
|
100
|
+
def mergeable
|
101
|
+
current_branches.select { |branch| (branch.success? || branch.fail? || branch.unknown?) }
|
102
|
+
end
|
103
|
+
|
104
|
+
def failures
|
105
|
+
current_branches.select { |branch| branch.fail? }
|
106
|
+
end
|
107
|
+
|
108
|
+
def successes
|
109
|
+
current_branches.select { |branch| branch.success? }
|
110
|
+
end
|
111
|
+
|
112
|
+
def removals
|
113
|
+
to_a.select { |branch| branch.removed? }
|
114
|
+
end
|
115
|
+
|
116
|
+
def fetch
|
117
|
+
return unless @collection_instance.respond_to?(:fetch)
|
118
|
+
|
119
|
+
@collection_instance.fetch.each do |b|
|
120
|
+
update_or_add(b)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def mark_all_as_current
|
125
|
+
@branches.each do |_, branch|
|
126
|
+
branch.current_record = true
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def add_to_merge(ref)
|
131
|
+
branch = record(ref)
|
132
|
+
branch.current_record = true
|
133
|
+
@collection_instance.add_to_merge(branch) if @collection_instance.respond_to?(:add_to_merge)
|
134
|
+
branch
|
135
|
+
end
|
136
|
+
|
137
|
+
def remove_from_merge(ref)
|
138
|
+
branch = record(ref)
|
139
|
+
branch.current_record = true
|
140
|
+
branch.removed!
|
141
|
+
@collection_instance.remove_from_merge(branch) if @collection_instance.respond_to?(:remove_from_merge)
|
142
|
+
branch
|
143
|
+
end
|
144
|
+
|
145
|
+
def mark_failure(branch, conflict_sha=nil)
|
146
|
+
update_or_add(branch)
|
147
|
+
branch.fail!(conflict_sha)
|
148
|
+
@collection_instance.mark_failure(branch) if @collection_instance.respond_to?(:mark_failure)
|
149
|
+
branch
|
150
|
+
end
|
151
|
+
|
152
|
+
def mark_deleted(branch)
|
153
|
+
update_or_add(branch)
|
154
|
+
branch.deleted!
|
155
|
+
@collection_instance.mark_deleted(branch) if @collection_instance.respond_to?(:mark_deleted)
|
156
|
+
branch
|
157
|
+
end
|
158
|
+
|
159
|
+
def mark_success(branch)
|
160
|
+
update_or_add(branch)
|
161
|
+
branch.success!
|
162
|
+
@collection_instance.mark_success(branch) if @collection_instance.respond_to?(:mark_success)
|
163
|
+
branch
|
164
|
+
end
|
165
|
+
|
166
|
+
def add_story(ref, story_id)
|
167
|
+
branch = get(ref)
|
168
|
+
branch.stories ||= []
|
169
|
+
branch.stories << story_id
|
170
|
+
|
171
|
+
@collection_instance.add_story(branch, story_id) if @collection_instance.respond_to?(:add_story)
|
172
|
+
branch
|
173
|
+
end
|
174
|
+
|
175
|
+
def code_reviewed?(branch)
|
176
|
+
@collection_instance.respond_to?(:code_reviewed?) ? @collection_instance.code_reviewed?(branch) : true
|
177
|
+
end
|
178
|
+
|
179
|
+
def can_ship?(branch)
|
180
|
+
@collection_instance.respond_to?(:can_ship?) ? @collection_instance.can_ship?(branch) : true
|
181
|
+
end
|
182
|
+
|
183
|
+
def branch_link(branch)
|
184
|
+
@collection_instance.branch_link(branch) if @collection_instance.respond_to?(:branch_link)
|
185
|
+
end
|
186
|
+
|
187
|
+
def set_resolutions(branch, resolutions)
|
188
|
+
update_or_add(branch)
|
189
|
+
branch.set_resolutions(resolutions)
|
190
|
+
@collection_instance.set_resolutions(branch) if @collection_instance.respond_to?(:set_resolutions)
|
191
|
+
branch
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def key(ref)
|
197
|
+
self.class.key(ref)
|
198
|
+
end
|
199
|
+
|
200
|
+
def update_or_add(branch)
|
201
|
+
old_branch = @branches[key(branch.ref)]
|
202
|
+
@branches[key(branch.ref)] = old_branch.nil? ? branch : old_branch.merge(branch)
|
203
|
+
end
|
204
|
+
|
205
|
+
def record(ref)
|
206
|
+
update_or_add(Branch.new(ref))
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,140 @@
|
|
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
|
+
@code_reviewed_label = config['code_reviewed_label'] || 'code reviewed'
|
17
|
+
@shippable_label = config['shippable_label'] || 'shippable'
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize_connection!(token)
|
21
|
+
if token.nil?
|
22
|
+
raise RuntimeError.new("Github token must be set in your flash_flow config file.")
|
23
|
+
end
|
24
|
+
octokit.configure do |c|
|
25
|
+
c.access_token = token
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_from_merge(branch)
|
30
|
+
pr = pr_for(branch)
|
31
|
+
if pr && @do_not_merge_label
|
32
|
+
add_label(pr.number, @do_not_merge_label)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def fetch
|
37
|
+
pull_requests.map do |pr|
|
38
|
+
Branch.from_hash(
|
39
|
+
'ref' => pr.head.ref,
|
40
|
+
'status' => status_from_labels(pr),
|
41
|
+
'metadata' => metadata(pr),
|
42
|
+
'sha' => pr.head.sha
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_to_merge(branch)
|
48
|
+
pr = pr_for(branch)
|
49
|
+
|
50
|
+
pr ||= create_pr(branch.ref, branch.ref, branch.ref)
|
51
|
+
branch.add_metadata(metadata(pr))
|
52
|
+
|
53
|
+
if pr && @do_not_merge_label
|
54
|
+
remove_label(pr.number, @do_not_merge_label)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def mark_success(branch)
|
59
|
+
remove_label(branch.metadata['pr_number'], @unmergeable_label)
|
60
|
+
end
|
61
|
+
|
62
|
+
def mark_failure(branch)
|
63
|
+
add_label(branch.metadata['pr_number'], @unmergeable_label)
|
64
|
+
end
|
65
|
+
|
66
|
+
def code_reviewed?(branch)
|
67
|
+
has_label?(branch.metadata['pr_number'], @code_reviewed_label)
|
68
|
+
end
|
69
|
+
|
70
|
+
def can_ship?(branch)
|
71
|
+
has_label?(branch.metadata['pr_number'], @shippable_label)
|
72
|
+
end
|
73
|
+
|
74
|
+
def branch_link(branch)
|
75
|
+
branch.metadata['pr_url']
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def status_from_labels(pull_request)
|
81
|
+
case
|
82
|
+
when has_label?(pull_request.number, @do_not_merge_label)
|
83
|
+
'removed'
|
84
|
+
when has_label?(pull_request.number, @unmergeable_label)
|
85
|
+
'fail'
|
86
|
+
else
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def pr_for(branch)
|
92
|
+
pull_requests.detect { |p| branch.ref == p.head.ref }
|
93
|
+
end
|
94
|
+
|
95
|
+
def create_pr(branch, title, body)
|
96
|
+
pr = octokit.create_pull_request(repo, @master_branch, branch, title, body)
|
97
|
+
pull_requests << pr
|
98
|
+
pr
|
99
|
+
end
|
100
|
+
|
101
|
+
def pull_requests
|
102
|
+
@pull_requests ||= octokit.pull_requests(repo).sort_by(&:created_at)
|
103
|
+
end
|
104
|
+
|
105
|
+
def remove_label(pull_request_number, label)
|
106
|
+
if has_label?(pull_request_number, label)
|
107
|
+
octokit.remove_label(repo, pull_request_number, label)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def add_label(pull_request_number, label)
|
112
|
+
unless has_label?(pull_request_number, label)
|
113
|
+
octokit.add_labels_to_an_issue(repo, pull_request_number, [label])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def has_label?(pull_request_number, label_name)
|
118
|
+
!!labels(pull_request_number).detect { |label| label == label_name }
|
119
|
+
end
|
120
|
+
|
121
|
+
def labels(pull_request_number)
|
122
|
+
@labels ||= {}
|
123
|
+
@labels[pull_request_number] ||= octokit.labels_for_issue(repo, pull_request_number).map(&:name)
|
124
|
+
end
|
125
|
+
|
126
|
+
def metadata(pr)
|
127
|
+
{
|
128
|
+
'pr_number' => pr.number,
|
129
|
+
'pr_url' => pr.html_url,
|
130
|
+
'user_url' => pr.user.html_url,
|
131
|
+
'repo_url' => pr.head.repo.html_url
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
def octokit
|
136
|
+
Octokit
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,44 @@
|
|
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_dir do
|
24
|
+
file ||= File.open(@filename, 'w')
|
25
|
+
file.puts JSON.pretty_generate(sort_branches(branches))
|
26
|
+
file.close
|
27
|
+
end
|
28
|
+
|
29
|
+
@git.in_temp_merge_branch do
|
30
|
+
@git.add_and_commit(@filename, 'Branch Info', add: {force: true})
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def sort_branches(branches)
|
37
|
+
return branches unless branches.is_a?(Hash)
|
38
|
+
sorted_branches = {}
|
39
|
+
branches.keys.sort.each { |key| sorted_branches[key] = sort_branches(branches[key]) }
|
40
|
+
sorted_branches
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'flash_flow/cmd_runner'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
class Git
|
6
|
+
ATTRIBUTES = [:remote, :merge_branch, :master_branch, :release_branch, :use_rerere]
|
7
|
+
|
8
|
+
attr_reader *ATTRIBUTES
|
9
|
+
attr_reader :working_branch
|
10
|
+
|
11
|
+
UNMERGED_STATUSES = %w{DD AU UD UA DU AA UU}
|
12
|
+
|
13
|
+
def initialize(config, logger=nil)
|
14
|
+
@cmd_runner = CmdRunner.new(logger: logger)
|
15
|
+
|
16
|
+
config['release_branch'] ||= config['master_branch']
|
17
|
+
config['remote'] ||= config['merge_remote'] # For backwards compatibility
|
18
|
+
|
19
|
+
ATTRIBUTES.each do |attr|
|
20
|
+
unless config.has_key?(attr.to_s)
|
21
|
+
raise RuntimeError.new("git configuration missing. Required config parameters: #{ATTRIBUTES}")
|
22
|
+
end
|
23
|
+
|
24
|
+
instance_variable_set("@#{attr}", config[attr.to_s])
|
25
|
+
end
|
26
|
+
|
27
|
+
@working_branch = current_branch
|
28
|
+
end
|
29
|
+
|
30
|
+
def in_dir
|
31
|
+
Dir.chdir(@cmd_runner.dir) do
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def last_stdout
|
37
|
+
@cmd_runner.last_stdout
|
38
|
+
end
|
39
|
+
|
40
|
+
def last_command
|
41
|
+
@cmd_runner.last_command
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_success?
|
45
|
+
@cmd_runner.last_success?
|
46
|
+
end
|
47
|
+
|
48
|
+
def run(cmd, opts={})
|
49
|
+
@cmd_runner.run("git #{cmd}", opts)
|
50
|
+
end
|
51
|
+
|
52
|
+
def working_dir
|
53
|
+
@cmd_runner.dir
|
54
|
+
end
|
55
|
+
|
56
|
+
def add_and_commit(files, message, opts={})
|
57
|
+
files = [files].flatten
|
58
|
+
run("add #{'-f ' if opts[:add] && opts[:add][:force]}#{files.join(' ')}")
|
59
|
+
run("commit -m '#{message}'")
|
60
|
+
end
|
61
|
+
|
62
|
+
def merge(branch)
|
63
|
+
run("merge #{branch}")
|
64
|
+
end
|
65
|
+
|
66
|
+
def branch_contains?(branch, ref)
|
67
|
+
run("branch -a --contains #{ref}", log: CmdRunner::LOG_CMD)
|
68
|
+
last_stdout.split("\n").detect { |str| str[2..-1] == branch }
|
69
|
+
end
|
70
|
+
|
71
|
+
def master_branch_contains?(sha)
|
72
|
+
branch_contains?("remotes/#{remote}/#{master_branch}", sha)
|
73
|
+
end
|
74
|
+
|
75
|
+
def in_original_merge_branch
|
76
|
+
in_branch("#{remote}/#{merge_branch}") { yield }
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_file_from_merge_branch(filename)
|
80
|
+
run("show #{remote}/#{merge_branch}:#{filename}", log: CmdRunner::LOG_CMD)
|
81
|
+
last_stdout
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize_rerere(copy_from_dir=nil)
|
85
|
+
return unless use_rerere
|
86
|
+
|
87
|
+
@cmd_runner.run('mkdir .git/rr-cache')
|
88
|
+
@cmd_runner.run('cp -R rr-cache/* .git/rr-cache/')
|
89
|
+
@cmd_runner.run("cp -R #{File.join(copy_from_dir, '.git/rr-cache/*')} .git/rr-cache/") if copy_from_dir
|
90
|
+
end
|
91
|
+
|
92
|
+
def commit_rerere(current_rereres)
|
93
|
+
return unless use_rerere
|
94
|
+
@cmd_runner.run('mkdir rr-cache')
|
95
|
+
@cmd_runner.run('rm -rf rr-cache/*')
|
96
|
+
current_rereres.each do |rerere|
|
97
|
+
@cmd_runner.run("cp -R .git/rr-cache/#{rerere} rr-cache/")
|
98
|
+
end
|
99
|
+
|
100
|
+
run('add rr-cache/')
|
101
|
+
run("commit -m 'Update rr-cache'")
|
102
|
+
end
|
103
|
+
|
104
|
+
def rerere_resolve!
|
105
|
+
return false unless use_rerere
|
106
|
+
|
107
|
+
if unresolved_conflicts.empty?
|
108
|
+
merging_files = staged_and_working_dir_files.select { |s| UNMERGED_STATUSES.include?(s[0..1]) }.map { |s| s[3..-1] }
|
109
|
+
conflicts = conflicted_files
|
110
|
+
|
111
|
+
run("add #{merging_files.join(" ")}")
|
112
|
+
run('commit --no-edit')
|
113
|
+
|
114
|
+
resolutions(conflicts)
|
115
|
+
else
|
116
|
+
false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def unresolved_conflicts
|
121
|
+
in_dir do
|
122
|
+
conflicted_files.map do |file|
|
123
|
+
File.open(file) { |f| f.grep(/>>>>/) }.empty? ? nil : file
|
124
|
+
end.compact
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def resolutions(files)
|
129
|
+
{}.tap do |hash|
|
130
|
+
files.map do |file|
|
131
|
+
hash[file] = resolution_candidates(file)
|
132
|
+
end.flatten
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# git rerere doesn't give you a deterministic way to determine which resolution was used
|
137
|
+
def resolution_candidates(file)
|
138
|
+
@cmd_runner.run("diff -q --from-file #{file} .git/rr-cache/*/postimage*", log: CmdRunner::LOG_CMD)
|
139
|
+
different_files = split_diff_lines(@cmd_runner.last_stdout)
|
140
|
+
|
141
|
+
@cmd_runner.run('ls -la .git/rr-cache/*/postimage*', log: CmdRunner::LOG_CMD)
|
142
|
+
all_files = split_diff_lines(@cmd_runner.last_stdout)
|
143
|
+
|
144
|
+
all_files - different_files
|
145
|
+
end
|
146
|
+
|
147
|
+
def split_diff_lines(arr)
|
148
|
+
arr.split("\n").map { |s| s.split(".git/rr-cache/").last.split("/postimage").first }
|
149
|
+
end
|
150
|
+
|
151
|
+
def staged_and_working_dir_files
|
152
|
+
run("status --porcelain")
|
153
|
+
last_stdout.split("\n").reject { |line| line[0..1] == '??' }
|
154
|
+
end
|
155
|
+
|
156
|
+
def conflicted_files
|
157
|
+
run("diff --name-only --diff-filter=U")
|
158
|
+
last_stdout.split("\n")
|
159
|
+
end
|
160
|
+
|
161
|
+
def current_branch
|
162
|
+
run("rev-parse --abbrev-ref HEAD")
|
163
|
+
last_stdout.strip
|
164
|
+
end
|
165
|
+
|
166
|
+
def most_recent_commit
|
167
|
+
run("show -s --format=%cd head")
|
168
|
+
end
|
169
|
+
|
170
|
+
def reset_temp_merge_branch
|
171
|
+
in_branch(master_branch) do
|
172
|
+
run("fetch #{remote}")
|
173
|
+
run("branch -D #{temp_merge_branch}")
|
174
|
+
run("checkout -b #{temp_merge_branch}")
|
175
|
+
run("reset --hard #{remote}/#{master_branch}")
|
176
|
+
run("clean -x -f -d")
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def push(branch, force=false)
|
181
|
+
run("push #{'-f' if force} #{remote} #{branch}:#{branch}")
|
182
|
+
end
|
183
|
+
|
184
|
+
def copy_temp_to_branch(branch, squash_message = nil)
|
185
|
+
run("checkout #{temp_merge_branch}")
|
186
|
+
run("merge --strategy=ours --no-edit #{branch}")
|
187
|
+
run("checkout #{branch}")
|
188
|
+
run("merge #{temp_merge_branch}")
|
189
|
+
|
190
|
+
squash_commits(branch, squash_message) if squash_message
|
191
|
+
end
|
192
|
+
|
193
|
+
def delete_temp_merge_branch
|
194
|
+
in_branch(master_branch) do
|
195
|
+
run("branch -d #{temp_merge_branch}")
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def in_temp_merge_branch(&block)
|
200
|
+
in_branch(temp_merge_branch, &block)
|
201
|
+
end
|
202
|
+
|
203
|
+
def in_merge_branch(&block)
|
204
|
+
in_branch(merge_branch, &block)
|
205
|
+
end
|
206
|
+
|
207
|
+
def in_branch(branch)
|
208
|
+
begin
|
209
|
+
starting_branch = current_branch
|
210
|
+
run("checkout #{branch}")
|
211
|
+
|
212
|
+
yield
|
213
|
+
ensure
|
214
|
+
run("checkout #{starting_branch}")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def temp_merge_branch
|
219
|
+
"flash_flow-#{merge_branch}"
|
220
|
+
end
|
221
|
+
|
222
|
+
def get_sha(branch, opts={})
|
223
|
+
if opts[:short]
|
224
|
+
run("rev-parse --short #{branch}")
|
225
|
+
else
|
226
|
+
run("rev-parse #{branch}")
|
227
|
+
end
|
228
|
+
last_stdout.strip if last_success?
|
229
|
+
end
|
230
|
+
|
231
|
+
def branch_exists?(branch)
|
232
|
+
run("rev-parse --verify #{branch}")
|
233
|
+
last_success?
|
234
|
+
end
|
235
|
+
|
236
|
+
def ahead_of_master?(branch)
|
237
|
+
branch_exists?(branch) && !master_branch_contains?(get_sha(branch))
|
238
|
+
end
|
239
|
+
|
240
|
+
def version
|
241
|
+
run('--version')
|
242
|
+
semver_regex = Regexp.new('.*(\d+\.\d+\.\d+).*')
|
243
|
+
running_version = last_stdout.strip
|
244
|
+
if semver = semver_regex.match(running_version)
|
245
|
+
semver[1]
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
private
|
250
|
+
|
251
|
+
def squash_commits(branch, commit_message)
|
252
|
+
unless branch_exists?("#{remote}/#{branch}")
|
253
|
+
run("push #{remote} #{master_branch}:#{branch}")
|
254
|
+
end
|
255
|
+
|
256
|
+
# Get all the files that differ between existing acceptance and new acceptance
|
257
|
+
run("diff --name-only #{remote}/#{branch} #{branch}")
|
258
|
+
files = last_stdout.split("\n")
|
259
|
+
run("reset #{remote}/#{branch}")
|
260
|
+
|
261
|
+
run("add -f #{files.map { |f| "\"#{Shellwords.escape(f)}\"" }.join(" ")}")
|
262
|
+
|
263
|
+
run("commit -m '#{commit_message}'")
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
end
|