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,54 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
class CmdRunner
|
6
|
+
LOG_NONE = :log_none
|
7
|
+
LOG_CMD = :log_cmd
|
8
|
+
|
9
|
+
attr_reader :dry_run, :last_command, :last_stderr, :last_stdout
|
10
|
+
attr_accessor :dir
|
11
|
+
|
12
|
+
def initialize(opts={})
|
13
|
+
@dir = opts[:dir] || `pwd`.strip
|
14
|
+
@dry_run = opts[:dry_run]
|
15
|
+
@logger = opts[:logger] || Logger.new('/dev/null')
|
16
|
+
end
|
17
|
+
|
18
|
+
def run(cmd, opts={})
|
19
|
+
@last_command = cmd
|
20
|
+
if dry_run
|
21
|
+
puts "#{dir}$ #{cmd}"
|
22
|
+
''
|
23
|
+
else
|
24
|
+
Dir.chdir(dir) do
|
25
|
+
Open3.popen3(cmd) do |_, stdout, stderr, wait_thr|
|
26
|
+
@last_stdout = stdout.read
|
27
|
+
@last_stderr = stderr.read
|
28
|
+
@success = wait_thr.value.success?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
log(cmd, opts[:log])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def last_success?
|
36
|
+
@success
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def log(cmd, log_what)
|
42
|
+
log_what = nil
|
43
|
+
if log_what == LOG_NONE
|
44
|
+
# Do nothing
|
45
|
+
else
|
46
|
+
@logger.debug("#{dir}$ #{cmd}")
|
47
|
+
unless log_what == LOG_CMD
|
48
|
+
last_stdout.split("\n").each { |line| @logger.debug(line) }
|
49
|
+
last_stderr.split("\n").each { |line| @logger.debug(line) }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'singleton'
|
3
|
+
require 'yaml'
|
4
|
+
require 'erb'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module FlashFlow
|
8
|
+
class Config
|
9
|
+
|
10
|
+
class AlreadyConfigured < StandardError ; end
|
11
|
+
class NotYetConfigured < StandardError ; end
|
12
|
+
class IncompleteConfiguration < StandardError ; end
|
13
|
+
|
14
|
+
include Singleton
|
15
|
+
class << self
|
16
|
+
private :instance
|
17
|
+
end
|
18
|
+
|
19
|
+
ATTRIBUTES = [
|
20
|
+
:git, :branch_info_file, :log_file, :notifier, :lock, :branches
|
21
|
+
]
|
22
|
+
|
23
|
+
attr_reader *ATTRIBUTES
|
24
|
+
attr_reader :logger
|
25
|
+
|
26
|
+
def self.configuration
|
27
|
+
raise NotYetConfigured unless instance.instance_variable_get(:@configured)
|
28
|
+
instance
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.configure!(config_file)
|
32
|
+
raise AlreadyConfigured if instance.instance_variable_get(:@configured)
|
33
|
+
|
34
|
+
template = ERB.new File.read(config_file)
|
35
|
+
yaml = YAML.load template.result(binding)
|
36
|
+
config = defaults.merge(symbolize_keys!(yaml))
|
37
|
+
|
38
|
+
missing_attrs = []
|
39
|
+
ATTRIBUTES.each do |attr_name|
|
40
|
+
missing_attrs << attr_name unless config.has_key?(attr_name)
|
41
|
+
instance.instance_variable_set("@#{attr_name}", config[attr_name])
|
42
|
+
end
|
43
|
+
|
44
|
+
instance.instance_variable_set(:@logger, get_logger(instance.log_file))
|
45
|
+
|
46
|
+
raise IncompleteConfiguration.new("Missing attributes:\n #{missing_attrs.join("\n ")}") unless missing_attrs.empty?
|
47
|
+
|
48
|
+
instance.instance_variable_set(:@configured, true)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.get_logger(log_file)
|
52
|
+
if log_file.to_s.empty?
|
53
|
+
log_file = '/dev/null'
|
54
|
+
elsif log_file.to_s.upcase == 'STDOUT'
|
55
|
+
log_file = STDOUT
|
56
|
+
else
|
57
|
+
dir = File.dirname(log_file)
|
58
|
+
FileUtils.mkdir_p(dir)
|
59
|
+
end
|
60
|
+
Logger.new(log_file)
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.defaults
|
65
|
+
{
|
66
|
+
branch_info_file: 'README.rdoc',
|
67
|
+
log_file: 'log/flash_flow.log',
|
68
|
+
notifier: nil,
|
69
|
+
lock: nil,
|
70
|
+
branches: nil
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.symbolize_keys!(hash)
|
75
|
+
hash.keys.each do |k|
|
76
|
+
unless k.is_a?(Symbol)
|
77
|
+
hash[k.to_sym] = hash[k]
|
78
|
+
hash.delete(k)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
hash
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'flash_flow/time_helper'
|
3
|
+
require 'flash_flow/version'
|
4
|
+
require 'flash_flow/data/branch'
|
5
|
+
require 'flash_flow/data/collection'
|
6
|
+
require 'flash_flow/data/store'
|
7
|
+
|
8
|
+
module FlashFlow
|
9
|
+
module Data
|
10
|
+
class Base
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def_delegators :@collection, :add_story, :mergeable, :mark_deleted, :mark_success, :mark_failure,
|
14
|
+
:remove_from_merge, :add_to_merge, :failures, :successes, :removals, :set_resolutions,
|
15
|
+
:to_a, :code_reviewed?, :branch_link
|
16
|
+
|
17
|
+
attr_reader :collection
|
18
|
+
|
19
|
+
def initialize(branch_config, filename, git, opts={})
|
20
|
+
@git = git
|
21
|
+
@store = Store.new(filename, git, opts)
|
22
|
+
@collection = initialize_collection(branch_config)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize_collection(branch_config)
|
26
|
+
stored_collection = Collection.from_hash(stored_branches)
|
27
|
+
|
28
|
+
if branch_config && !branch_config.empty?
|
29
|
+
collection = Collection.fetch(branch_config)
|
30
|
+
# Order matters. We are marking the PRs as current, not the branches stored in the json
|
31
|
+
collection.mark_all_as_current
|
32
|
+
collection = collection.reverse_merge(stored_collection)
|
33
|
+
|
34
|
+
else
|
35
|
+
collection = stored_collection
|
36
|
+
collection.mark_all_as_current
|
37
|
+
end
|
38
|
+
|
39
|
+
collection.branches.delete_if { |k, v| TimeHelper.massage_time(v.updated_at) < Time.now - TimeHelper.two_weeks }
|
40
|
+
|
41
|
+
collection
|
42
|
+
end
|
43
|
+
|
44
|
+
def version
|
45
|
+
stored_data['version']
|
46
|
+
end
|
47
|
+
|
48
|
+
def save!
|
49
|
+
@store.write(to_hash)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_hash
|
53
|
+
{
|
54
|
+
'version' => FlashFlow::VERSION,
|
55
|
+
'branches' => @collection.to_hash,
|
56
|
+
'releases' => releases
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def stored_branches
|
61
|
+
@stored_branches ||= stored_data['branches'] || {}
|
62
|
+
end
|
63
|
+
|
64
|
+
def releases
|
65
|
+
@releases ||= stored_data['releases'] || []
|
66
|
+
end
|
67
|
+
|
68
|
+
def merged_branches
|
69
|
+
@collection.reverse_merge(Collection.from_hash({}, stored_branches))
|
70
|
+
end
|
71
|
+
|
72
|
+
def stored_data
|
73
|
+
@stored_data ||= @store.get
|
74
|
+
end
|
75
|
+
|
76
|
+
def saved_branches
|
77
|
+
Collection.from_hash(stored_branches).to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
def pending_release
|
81
|
+
releases.detect { |r| r['status'] == 'Pending' }
|
82
|
+
end
|
83
|
+
|
84
|
+
def ready_to_merge_release
|
85
|
+
releases.detect { |r| r['status'] == 'Ready to merge' }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'tinybucket'
|
2
|
+
require 'flash_flow/data/branch'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
class Bitbucket
|
7
|
+
|
8
|
+
attr_accessor :repo, :unmergeable_label
|
9
|
+
|
10
|
+
def initialize(config={})
|
11
|
+
initialize_connection!(config['oauth_token'], config['oauth_secret'])
|
12
|
+
@repo_owner = config['repo_owner']
|
13
|
+
@repo = config['repo']
|
14
|
+
@master_branch = config['master_branch'] || 'master'
|
15
|
+
@unmergeable_label = config['unmergeable_label'] || 'Flash Flow -- Unmergeable'
|
16
|
+
@do_not_merge_label = config['do_not_merge_label'] || 'Flash Flow -- Do Not Merge'
|
17
|
+
# @code_reviewed_label = config['code_reviewed_label'] || 'code reviewed'
|
18
|
+
# @shippable_label = config['shippable_label'] || 'shippable'
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize_connection!(oauth_token, oauth_secret)
|
22
|
+
if oauth_token.nil? || oauth_secret.nil?
|
23
|
+
raise RuntimeError.new("Oauth token and Oauth secret must be set in your flash_flow config file.")
|
24
|
+
end
|
25
|
+
Tinybucket.configure do |config|
|
26
|
+
config.oauth_token = oauth_token
|
27
|
+
config.oauth_secret = oauth_secret
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_from_merge(branch)
|
32
|
+
pr = pr_for(branch)
|
33
|
+
if pr && @do_not_merge_label
|
34
|
+
add_labeling(pr, @do_not_merge_label)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch
|
39
|
+
pull_requests.map do |pr|
|
40
|
+
Branch.from_hash(
|
41
|
+
'ref' => pr.source['branch']['name'],
|
42
|
+
'status' => status_from_labeling(pr),
|
43
|
+
'metadata' => metadata(pr),
|
44
|
+
'sha' => pr.source['commit']['hash']
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_to_merge(branch)
|
50
|
+
pr = pr_for(branch)
|
51
|
+
|
52
|
+
pr ||= create_pr(branch.ref, branch.ref, branch.ref)
|
53
|
+
branch.add_metadata(metadata(pr))
|
54
|
+
|
55
|
+
if pr && @do_not_merge_label
|
56
|
+
remove_labeling(pr, @do_not_merge_label)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def mark_success(branch)
|
61
|
+
remove_labeling(pr_for(branch), @unmergeable_label)
|
62
|
+
end
|
63
|
+
|
64
|
+
def mark_failure(branch)
|
65
|
+
add_labeling(pr_for(branch), @unmergeable_label)
|
66
|
+
end
|
67
|
+
|
68
|
+
def code_reviewed?(branch)
|
69
|
+
is_labeled?(pr_for(branch), @code_reviewed_label)
|
70
|
+
end
|
71
|
+
|
72
|
+
def can_ship?(branch)
|
73
|
+
is_labeled?(pr_for(branch), @shippable_label)
|
74
|
+
end
|
75
|
+
|
76
|
+
def branch_link(branch)
|
77
|
+
branch.metadata['pr_url']
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def status_from_labeling(pr)
|
83
|
+
case
|
84
|
+
when is_labeled?(pr, @do_not_merge_label)
|
85
|
+
'removed'
|
86
|
+
when is_labeled?(pr, @unmergeable_label)
|
87
|
+
'fail'
|
88
|
+
else
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def pr_for(branch)
|
94
|
+
pull_requests.detect { |pr| branch.ref == pr.source['branch']['name'] }
|
95
|
+
end
|
96
|
+
|
97
|
+
def create_pr(branch, title, body)
|
98
|
+
pr_resource = Tinybucket::Resource::PullRequests.new(repo_obj, [])
|
99
|
+
pr = pr_resource.create(source: { branch: { name: branch }}, title: title, description: body)
|
100
|
+
pull_requests << pr
|
101
|
+
pr
|
102
|
+
end
|
103
|
+
|
104
|
+
def pull_requests
|
105
|
+
@pull_requests ||= repo_obj.pull_requests.sort_by(&:created_on)
|
106
|
+
end
|
107
|
+
|
108
|
+
def labeling_string(label)
|
109
|
+
" --- #{label}"
|
110
|
+
end
|
111
|
+
|
112
|
+
def labeling_regex(label)
|
113
|
+
/#{Regexp.escape(labeling_string(label))}$/
|
114
|
+
end
|
115
|
+
|
116
|
+
def remove_labeling(pr, label)
|
117
|
+
if is_labeled?(pr, label)
|
118
|
+
pr.title.gsub!(labeling_regex(label), '')
|
119
|
+
pr.update
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def add_labeling(pr, label)
|
124
|
+
unless is_labeled?(pr, label)
|
125
|
+
pr.title += labeling_string(label)
|
126
|
+
pr.update
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def is_labeled?(pr, label)
|
131
|
+
pr.title =~ labeling_regex(label)
|
132
|
+
end
|
133
|
+
|
134
|
+
def metadata(pr)
|
135
|
+
{
|
136
|
+
'pr_number' => pr.id,
|
137
|
+
'pr_url' => pr.links['html']['href'],
|
138
|
+
'user_url' => pr.author['links']['html']['href'],
|
139
|
+
'repo_url' => repo_obj.links['html']['href']
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def repo_obj
|
144
|
+
@repo_obj ||= begin
|
145
|
+
repo = Tinybucket.new.repo(@repo_owner, @repo)
|
146
|
+
repo.load
|
147
|
+
repo
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'flash_flow/time_helper'
|
3
|
+
|
4
|
+
module FlashFlow
|
5
|
+
module Data
|
6
|
+
|
7
|
+
class Branch
|
8
|
+
attr_accessor :ref, :sha, :status, :resolutions, :stories, :conflict_sha, :metadata,
|
9
|
+
:current_record, :merge_order, :updated_at, :created_at
|
10
|
+
|
11
|
+
def initialize(_ref)
|
12
|
+
@ref = _ref
|
13
|
+
@resolutions = {}
|
14
|
+
@stories = []
|
15
|
+
@metadata = {}
|
16
|
+
@updated_at = Time.now
|
17
|
+
@created_at = Time.now
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.from_hash(hash)
|
21
|
+
branch = new(hash['ref'])
|
22
|
+
branch.sha = hash['sha']
|
23
|
+
branch.status = hash['status']
|
24
|
+
branch.merge_order = hash['merge_order']
|
25
|
+
branch.resolutions = hash['resolutions']
|
26
|
+
branch.stories = hash['stories']
|
27
|
+
branch.metadata = hash['metadata']
|
28
|
+
branch.conflict_sha = hash['conflict_sha'] || hash['metadata'].to_h['conflict_sha']
|
29
|
+
branch.updated_at = TimeHelper.massage_time(hash['updated_at'])
|
30
|
+
branch.created_at = TimeHelper.massage_time(hash['created_at'])
|
31
|
+
branch
|
32
|
+
end
|
33
|
+
|
34
|
+
def ==(other)
|
35
|
+
other.ref == ref
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_hash
|
39
|
+
{
|
40
|
+
'ref' => ref,
|
41
|
+
'sha' => sha,
|
42
|
+
'status' => status,
|
43
|
+
'merge_order' => merge_order,
|
44
|
+
'resolutions' => resolutions,
|
45
|
+
'stories' => stories,
|
46
|
+
'conflict_sha' => conflict_sha,
|
47
|
+
'metadata' => metadata,
|
48
|
+
'updated_at' => updated_at,
|
49
|
+
'created_at' => created_at,
|
50
|
+
}
|
51
|
+
end
|
52
|
+
alias :to_h :to_hash
|
53
|
+
|
54
|
+
def to_json(_)
|
55
|
+
JSON.pretty_generate(to_hash)
|
56
|
+
end
|
57
|
+
|
58
|
+
def merge(other)
|
59
|
+
unless other.nil?
|
60
|
+
self.sha = other.sha
|
61
|
+
self.status = other.status
|
62
|
+
self.merge_order = other.merge_order
|
63
|
+
self.resolutions = other.resolutions
|
64
|
+
self.stories = self.stories.to_a | other.stories.to_a
|
65
|
+
self.updated_at = Time.now
|
66
|
+
self.created_at = [(self.created_at || Time.now), (other.created_at || Time.now)].min
|
67
|
+
end
|
68
|
+
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_metadata(data)
|
73
|
+
self.metadata ||= {}
|
74
|
+
self.metadata.merge!(data)
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_resolutions(_resolutions)
|
78
|
+
self.resolutions = _resolutions
|
79
|
+
end
|
80
|
+
|
81
|
+
def success!
|
82
|
+
self.status = 'success'
|
83
|
+
end
|
84
|
+
|
85
|
+
def success?
|
86
|
+
self.status == 'success'
|
87
|
+
end
|
88
|
+
|
89
|
+
def fail!(conflict_sha=nil)
|
90
|
+
self.conflict_sha = conflict_sha
|
91
|
+
self.status = 'fail'
|
92
|
+
end
|
93
|
+
|
94
|
+
def fail?
|
95
|
+
self.status == 'fail'
|
96
|
+
end
|
97
|
+
|
98
|
+
def removed!
|
99
|
+
self.status = 'removed'
|
100
|
+
end
|
101
|
+
|
102
|
+
def removed?
|
103
|
+
self.status == 'removed'
|
104
|
+
end
|
105
|
+
|
106
|
+
def deleted!
|
107
|
+
self.status = 'deleted'
|
108
|
+
end
|
109
|
+
|
110
|
+
def deleted?
|
111
|
+
self.status == 'deleted'
|
112
|
+
end
|
113
|
+
|
114
|
+
def unknown!
|
115
|
+
self.status = nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def unknown?
|
119
|
+
self.status.nil?
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|