daq_flow 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
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