flash_flow 1.0.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +81 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +152 -0
  8. data/Rakefile +10 -0
  9. data/bin/flash_flow +23 -0
  10. data/flash_flow.gemspec +28 -0
  11. data/flash_flow.yml.erb.example +83 -0
  12. data/lib/flash_flow.rb +7 -0
  13. data/lib/flash_flow/branch_merger.rb +52 -0
  14. data/lib/flash_flow/cmd_runner.rb +37 -0
  15. data/lib/flash_flow/config.rb +71 -0
  16. data/lib/flash_flow/data.rb +6 -0
  17. data/lib/flash_flow/data/base.rb +58 -0
  18. data/lib/flash_flow/data/branch.rb +131 -0
  19. data/lib/flash_flow/data/collection.rb +181 -0
  20. data/lib/flash_flow/data/github.rb +129 -0
  21. data/lib/flash_flow/data/store.rb +33 -0
  22. data/lib/flash_flow/deploy.rb +184 -0
  23. data/lib/flash_flow/git.rb +248 -0
  24. data/lib/flash_flow/install.rb +19 -0
  25. data/lib/flash_flow/issue_tracker.rb +52 -0
  26. data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
  27. data/lib/flash_flow/lock.rb +25 -0
  28. data/lib/flash_flow/lock/github.rb +91 -0
  29. data/lib/flash_flow/notifier.rb +24 -0
  30. data/lib/flash_flow/notifier/hipchat.rb +36 -0
  31. data/lib/flash_flow/options.rb +36 -0
  32. data/lib/flash_flow/time_helper.rb +11 -0
  33. data/lib/flash_flow/version.rb +3 -0
  34. data/test/lib/data/test_base.rb +10 -0
  35. data/test/lib/data/test_branch.rb +203 -0
  36. data/test/lib/data/test_collection.rb +238 -0
  37. data/test/lib/data/test_github.rb +23 -0
  38. data/test/lib/data/test_store.rb +53 -0
  39. data/test/lib/issue_tracker/test_pivotal.rb +221 -0
  40. data/test/lib/lock/test_github.rb +70 -0
  41. data/test/lib/test_branch_merger.rb +76 -0
  42. data/test/lib/test_config.rb +84 -0
  43. data/test/lib/test_deploy.rb +175 -0
  44. data/test/lib/test_git.rb +73 -0
  45. data/test/lib/test_issue_tracker.rb +43 -0
  46. data/test/lib/test_notifier.rb +33 -0
  47. data/test/minitest_helper.rb +38 -0
  48. metadata +217 -0
@@ -0,0 +1,52 @@
1
+ module FlashFlow
2
+ class BranchMerger
3
+
4
+ attr_reader :conflict_sha, :resolutions
5
+
6
+ def initialize(git, branch)
7
+ @git = git
8
+ @branch = branch
9
+ end
10
+
11
+ def do_merge(rerere_forget)
12
+ return :deleted if sha.nil?
13
+
14
+ @git.run("merge --no-ff #{@branch.remote}/#{@branch.ref}")
15
+
16
+ if @git.last_success? || try_rerere(rerere_forget)
17
+ return :success
18
+ else
19
+ @conflict_sha = merge_rollback
20
+ return :conflict
21
+ end
22
+ end
23
+
24
+ def sha
25
+ @sha if defined?(@sha)
26
+ @sha = get_sha
27
+ end
28
+
29
+ private
30
+
31
+ def try_rerere(rerere_forget)
32
+ if rerere_forget
33
+ @git.run('rerere forget')
34
+ false
35
+ else
36
+ @resolutions = @git.rerere_resolve!
37
+ end
38
+ end
39
+
40
+ def get_sha
41
+ @git.run("rev-parse #{@branch.remote}/#{@branch.ref}")
42
+ @git.last_stdout.strip if @git.last_success?
43
+ end
44
+
45
+ def merge_rollback
46
+ @git.run("reset --hard HEAD")
47
+ @git.run("rev-parse HEAD")
48
+ @git.last_stdout.strip
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,37 @@
1
+ require 'logger'
2
+ require 'open3'
3
+
4
+ module FlashFlow
5
+ class CmdRunner
6
+ attr_reader :dry_run, :dir, :last_command, :last_stderr, :last_stdout
7
+
8
+ def initialize(opts={})
9
+ @dir = opts[:dir] || '.'
10
+ @dry_run = opts[:dry_run]
11
+ @logger = opts[:logger] || Logger.new('/dev/null')
12
+ end
13
+
14
+ def run(cmd)
15
+ @last_command = cmd
16
+ if dry_run
17
+ puts "#{dir}$ #{cmd}"
18
+ ''
19
+ else
20
+ Dir.chdir(dir) do
21
+ Open3.popen3(cmd) do |_, stdout, stderr, wait_thr|
22
+ @last_stdout = stdout.read
23
+ @last_stderr = stderr.read
24
+ @success = wait_thr.value.success?
25
+ end
26
+ end
27
+ @logger.debug("#{dir}$ #{cmd}")
28
+ last_stdout.split("\n").each { |line| @logger.debug(line) }
29
+ last_stderr.split("\n").each { |line| @logger.debug(line) }
30
+ end
31
+ end
32
+
33
+ def last_success?
34
+ @success
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ require 'logger'
2
+ require 'singleton'
3
+ require 'yaml'
4
+ require 'erb'
5
+
6
+ module FlashFlow
7
+ class Config
8
+
9
+ class AlreadyConfigured < StandardError ; end
10
+ class NotYetConfigured < StandardError ; end
11
+ class IncompleteConfiguration < StandardError ; end
12
+
13
+ include Singleton
14
+ class << self
15
+ private :instance
16
+ end
17
+
18
+ ATTRIBUTES = [
19
+ :git, :branch_info_file, :log_file, :notifier, :issue_tracker, :lock, :branches
20
+ ]
21
+
22
+ attr_reader *ATTRIBUTES
23
+ attr_reader :logger
24
+
25
+ def self.configuration
26
+ raise NotYetConfigured unless instance.instance_variable_get(:@configured)
27
+ instance
28
+ end
29
+
30
+ def self.configure!(config_file)
31
+ raise AlreadyConfigured if instance.instance_variable_get(:@configured)
32
+
33
+ template = ERB.new File.read(config_file)
34
+ yaml = YAML.load template.result(binding)
35
+ config = defaults.merge(symbolize_keys!(yaml))
36
+
37
+ missing_attrs = []
38
+ ATTRIBUTES.each do |attr_name|
39
+ missing_attrs << attr_name unless config.has_key?(attr_name)
40
+ instance.instance_variable_set("@#{attr_name}", config[attr_name])
41
+ end
42
+
43
+ instance.instance_variable_set(:@logger, Logger.new(instance.log_file))
44
+
45
+ raise IncompleteConfiguration.new("Missing attributes:\n #{missing_attrs.join("\n ")}") unless missing_attrs.empty?
46
+
47
+ instance.instance_variable_set(:@configured, true)
48
+ end
49
+
50
+ def self.defaults
51
+ {
52
+ branch_info_file: 'README.rdoc',
53
+ log_file: 'log/flash_flow.log',
54
+ notifier: nil,
55
+ issue_tracker: nil,
56
+ lock: nil,
57
+ branches: nil,
58
+ }
59
+ end
60
+
61
+ def self.symbolize_keys!(hash)
62
+ hash.keys.each do |k|
63
+ unless k.is_a?(Symbol)
64
+ hash[k.to_sym] = hash[k]
65
+ hash.delete(k)
66
+ end
67
+ end
68
+ hash
69
+ end
70
+ end
71
+ 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,58 @@
1
+ require 'json'
2
+ require 'flash_flow/version'
3
+ require 'flash_flow/data/branch'
4
+ require 'flash_flow/data/collection'
5
+ require 'flash_flow/data/store'
6
+
7
+ module FlashFlow
8
+ module Data
9
+ class Base
10
+ extend Forwardable
11
+
12
+ def_delegators :@collection, :add_story, :mergeable, :mark_deleted, :mark_success,
13
+ :mark_failure, :remove_from_merge, :add_to_merge, :failures, :set_resolutions
14
+
15
+ def initialize(branch_config, filename, git, opts={})
16
+ @git = git
17
+ @store = Store.new(filename, git, opts)
18
+ @collection = initialize_collection(branch_config, git.remotes_hash)
19
+ end
20
+
21
+ def initialize_collection(branch_config, remotes)
22
+ Collection.fetch(remotes, branch_config) ||
23
+ Collection.from_hash(remotes, backwards_compatible_store['branches'])
24
+ end
25
+
26
+ def version
27
+ backwards_compatible_store['version']
28
+ end
29
+
30
+ def save!
31
+ @store.write(to_hash)
32
+ end
33
+
34
+ def to_hash
35
+ {
36
+ 'version' => FlashFlow::VERSION,
37
+ 'branches' => merged_branches.to_hash
38
+ }
39
+ end
40
+
41
+ def merged_branches
42
+ @collection.reverse_merge(Collection.from_hash({}, backwards_compatible_store['branches']))
43
+ end
44
+
45
+ def backwards_compatible_store
46
+ @backwards_compatible_store ||= begin
47
+ hash = @store.get
48
+ hash.has_key?('branches') ? hash : { 'branches' => hash }
49
+ end
50
+ end
51
+
52
+ def saved_branches
53
+ Collection.from_hash(@git.remotes, backwards_compatible_store['branches']).to_a
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,131 @@
1
+ require 'json'
2
+
3
+ module FlashFlow
4
+ module Data
5
+
6
+ class Branch
7
+ attr_accessor :remote, :remote_url, :ref, :sha, :status, :resolutions, :stories, :metadata, :updated_at, :created_at
8
+
9
+ def initialize(_remote, _remote_url, _ref)
10
+ @remote = _remote
11
+ @remote_url = _remote_url
12
+ @ref = _ref
13
+ @resolutions = {}
14
+ @stories = []
15
+ @updated_at = Time.now
16
+ @created_at = Time.now
17
+ end
18
+
19
+ def self.from_hash(hash)
20
+ branch = new(hash['remote'], hash['remote_url'], hash['ref'])
21
+ branch.sha = hash['sha']
22
+ branch.status = hash['status']
23
+ branch.resolutions = hash['resolutions']
24
+ branch.stories = hash['stories']
25
+ branch.metadata = hash['metadata']
26
+ branch.updated_at = massage_time(hash['updated_at'])
27
+ branch.created_at = massage_time(hash['created_at'])
28
+ branch
29
+ end
30
+
31
+ def self.massage_time(time)
32
+ case time
33
+ when Time
34
+ time
35
+ when NilClass
36
+ Time.now
37
+ else
38
+ Time.parse(time)
39
+ end
40
+ end
41
+
42
+ def ==(other)
43
+ other.remote_url == remote_url && other.remote == remote && other.ref == ref
44
+ end
45
+
46
+ def to_hash
47
+ {
48
+ 'remote' => remote,
49
+ 'remote_url' => remote_url,
50
+ 'ref' => ref,
51
+ 'sha' => sha,
52
+ 'status' => status,
53
+ 'resolutions' => resolutions,
54
+ 'stories' => stories,
55
+ 'metadata' => metadata,
56
+ 'updated_at' => updated_at,
57
+ 'created_at' => created_at,
58
+ }
59
+ end
60
+ alias :to_h :to_hash
61
+
62
+ def to_json(_)
63
+ JSON.pretty_generate(to_hash)
64
+ end
65
+
66
+ def merge(other)
67
+ unless other.nil?
68
+ self.sha = other.sha
69
+ self.status = other.status
70
+ self.resolutions = other.resolutions
71
+ self.stories = self.stories.to_a | other.stories.to_a
72
+ self.updated_at = Time.now
73
+ self.created_at = [(self.created_at || Time.now), (other.created_at || Time.now)].min
74
+ end
75
+
76
+ self
77
+ end
78
+
79
+ def add_metadata(data)
80
+ self.metadata ||= {}
81
+ self.metadata.merge!(data)
82
+ end
83
+
84
+ def set_resolutions(_resolutions)
85
+ self.resolutions = _resolutions
86
+ end
87
+
88
+ def success!
89
+ self.status = 'success'
90
+ end
91
+
92
+ def success?
93
+ self.status == 'success'
94
+ end
95
+
96
+ def fail!(conflict_sha=nil)
97
+ add_metadata('conflict_sha' => conflict_sha) if conflict_sha
98
+ self.status = 'fail'
99
+ end
100
+
101
+ def fail?
102
+ self.status == 'fail'
103
+ end
104
+
105
+ def removed!
106
+ self.status = 'removed'
107
+ end
108
+
109
+ def removed?
110
+ self.status == 'removed'
111
+ end
112
+
113
+ def deleted!
114
+ self.status = 'deleted'
115
+ end
116
+
117
+ def deleted?
118
+ self.status == 'deleted'
119
+ end
120
+
121
+ def unknown!
122
+ self.status = nil
123
+ end
124
+
125
+ def unknown?
126
+ self.status.nil?
127
+ end
128
+ end
129
+
130
+ end
131
+ end
@@ -0,0 +1,181 @@
1
+ require 'flash_flow/data/branch'
2
+ require 'flash_flow/data/github'
3
+
4
+ module FlashFlow
5
+ module Data
6
+
7
+ class Collection
8
+
9
+ attr_accessor :branches, :remotes
10
+
11
+ def initialize(remotes, config=nil)
12
+ @remotes = remotes
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(remotes, config=nil)
22
+ collection = new(remotes, config)
23
+ collection.fetch
24
+ collection
25
+ end
26
+
27
+ def self.from_hash(remotes, hash)
28
+ collection = new(remotes)
29
+ collection.branches = branches_from_hash(hash.dup)
30
+ collection
31
+ end
32
+
33
+ def self.branches_from_hash(hash)
34
+ hash.each do |key, val|
35
+ hash[key] = val.is_a?(Branch) ? val : Branch.from_hash(val)
36
+ end
37
+ end
38
+
39
+ def get(remote_url, ref)
40
+ @branches[key(remote_url, ref)]
41
+ end
42
+
43
+ def to_hash
44
+ {}.tap do |hash|
45
+ @branches.each do |key, val|
46
+ hash[key] = val.to_hash
47
+ end
48
+ end
49
+ end
50
+ alias :to_h :to_hash
51
+
52
+ def reverse_merge(old)
53
+ merged_branches = @branches.dup
54
+
55
+ merged_branches.each do |_, info|
56
+ info.updated_at = Time.now
57
+ info.created_at ||= Time.now
58
+ end
59
+
60
+ old.branches.each do |full_ref, info|
61
+ if merged_branches.has_key?(full_ref)
62
+ branch = merged_branches[full_ref]
63
+
64
+ branch.created_at = info.created_at
65
+ branch.resolutions = branch.resolutions.to_h.merge(info.resolutions.to_h)
66
+ branch.stories = info.stories.to_a | merged_branches[full_ref].stories.to_a
67
+ else
68
+ merged_branches[full_ref] = info
69
+ merged_branches[full_ref].status = nil
70
+ end
71
+ end
72
+
73
+ self.class.from_hash(remotes, merged_branches)
74
+ end
75
+
76
+ def to_a
77
+ @branches.values
78
+ end
79
+
80
+ def each
81
+ to_a.each
82
+ end
83
+
84
+ def mergeable
85
+ to_a.select { |branch| branch.success? || branch.fail? || branch.unknown? }
86
+ end
87
+
88
+ def failures
89
+ @branches.select { |_, v| v.fail? }
90
+ end
91
+
92
+ def fetch
93
+ return unless @collection_instance.respond_to?(:fetch)
94
+
95
+ @collection_instance.fetch.each do |b|
96
+ update_or_add(b)
97
+ end
98
+ end
99
+
100
+ def add_to_merge(remote, ref)
101
+ branch = record(remote, nil, ref)
102
+ @collection_instance.add_to_merge(branch) if @collection_instance.respond_to?(:add_to_merge)
103
+ branch
104
+ end
105
+
106
+ def remove_from_merge(remote, ref)
107
+ branch = record(remote, nil, ref)
108
+ branch.removed!
109
+ @collection_instance.remove_from_merge(branch) if @collection_instance.respond_to?(:remove_from_merge)
110
+ branch
111
+ end
112
+
113
+ def mark_failure(branch, conflict_sha=nil)
114
+ update_or_add(branch)
115
+ branch.fail!(conflict_sha)
116
+ @collection_instance.mark_failure(branch) if @collection_instance.respond_to?(:mark_failure)
117
+ branch
118
+ end
119
+
120
+ def mark_deleted(branch)
121
+ update_or_add(branch)
122
+ branch.deleted!
123
+ @collection_instance.mark_deleted(branch) if @collection_instance.respond_to?(:mark_deleted)
124
+ branch
125
+ end
126
+
127
+ def mark_success(branch)
128
+ update_or_add(branch)
129
+ branch.success!
130
+ @collection_instance.mark_success(branch) if @collection_instance.respond_to?(:mark_success)
131
+ branch
132
+ end
133
+
134
+ def add_story(remote, ref, story_id)
135
+ branch = get(url_from_remote(remote), ref)
136
+ branch.stories ||= []
137
+ branch.stories << story_id
138
+
139
+ @collection_instance.add_story(branch, story_id) if @collection_instance.respond_to?(:add_story)
140
+ branch
141
+ end
142
+
143
+ def set_resolutions(branch, resolutions)
144
+ update_or_add(branch)
145
+ branch.set_resolutions(resolutions)
146
+ @collection_instance.set_resolutions(branch) if @collection_instance.respond_to?(:set_resolutions)
147
+ branch
148
+ end
149
+
150
+ private
151
+
152
+ def key(remote_url, ref)
153
+ "#{remote_url}/#{ref}"
154
+ end
155
+
156
+ def remote_from_url(url)
157
+ remotes.detect { |_, url_val| url_val == url }.first
158
+ end
159
+
160
+ def url_from_remote(remote)
161
+ remotes[remote]
162
+ end
163
+
164
+ def fixup(branch)
165
+ branch.remote ||= remote_from_url(branch.remote_url)
166
+ branch.remote_url ||= url_from_remote(branch.remote)
167
+ end
168
+
169
+ def update_or_add(branch)
170
+ fixup(branch)
171
+ old_branch = @branches[key(branch.remote_url, branch.ref)]
172
+ @branches[key(branch.remote_url, branch.ref)] = old_branch.nil? ? branch : old_branch.merge(branch)
173
+ end
174
+
175
+ def record(remote, remote_url, ref)
176
+ update_or_add(Branch.new(remote, remote_url, ref))
177
+ end
178
+
179
+ end
180
+ end
181
+ end