flash_flow 1.0.0

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