daq_flow 1.0.4
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 +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
|