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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +62 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +165 -0
  8. data/Rakefile +10 -0
  9. data/bin/daq_flow +23 -0
  10. data/flash_flow.gemspec +28 -0
  11. data/flash_flow.yml.erb.example +42 -0
  12. data/lib/flash_flow.rb +7 -0
  13. data/lib/flash_flow/branch_merger.rb +55 -0
  14. data/lib/flash_flow/cmd_runner.rb +54 -0
  15. data/lib/flash_flow/config.rb +84 -0
  16. data/lib/flash_flow/data.rb +6 -0
  17. data/lib/flash_flow/data/base.rb +89 -0
  18. data/lib/flash_flow/data/bitbucket.rb +152 -0
  19. data/lib/flash_flow/data/branch.rb +124 -0
  20. data/lib/flash_flow/data/collection.rb +211 -0
  21. data/lib/flash_flow/data/github.rb +140 -0
  22. data/lib/flash_flow/data/store.rb +44 -0
  23. data/lib/flash_flow/git.rb +267 -0
  24. data/lib/flash_flow/install.rb +19 -0
  25. data/lib/flash_flow/lock.rb +23 -0
  26. data/lib/flash_flow/merge.rb +6 -0
  27. data/lib/flash_flow/merge/acceptance.rb +154 -0
  28. data/lib/flash_flow/merge/base.rb +116 -0
  29. data/lib/flash_flow/merge_order.rb +27 -0
  30. data/lib/flash_flow/notifier.rb +23 -0
  31. data/lib/flash_flow/options.rb +34 -0
  32. data/lib/flash_flow/resolve.rb +143 -0
  33. data/lib/flash_flow/shadow_repo.rb +44 -0
  34. data/lib/flash_flow/time_helper.rb +32 -0
  35. data/lib/flash_flow/version.rb +4 -0
  36. data/log/.keep +0 -0
  37. data/test/lib/data/test_base.rb +10 -0
  38. data/test/lib/data/test_branch.rb +206 -0
  39. data/test/lib/data/test_collection.rb +308 -0
  40. data/test/lib/data/test_store.rb +70 -0
  41. data/test/lib/lock/test_github.rb +74 -0
  42. data/test/lib/merge/test_acceptance.rb +230 -0
  43. data/test/lib/test_branch_merger.rb +78 -0
  44. data/test/lib/test_config.rb +63 -0
  45. data/test/lib/test_git.rb +73 -0
  46. data/test/lib/test_merge_order.rb +71 -0
  47. data/test/lib/test_notifier.rb +33 -0
  48. data/test/lib/test_resolve.rb +69 -0
  49. data/test/minitest_helper.rb +41 -0
  50. data/update_gem.sh +5 -0
  51. 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,6 @@
1
+ require 'flash_flow/data/base'
2
+
3
+ module FlashFlow
4
+ module Data
5
+ end
6
+ 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