flash_flow 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +22 -0
- data/README.md +152 -0
- data/Rakefile +10 -0
- data/bin/flash_flow +23 -0
- data/flash_flow.gemspec +28 -0
- data/flash_flow.yml.erb.example +83 -0
- data/lib/flash_flow.rb +7 -0
- data/lib/flash_flow/branch_merger.rb +52 -0
- data/lib/flash_flow/cmd_runner.rb +37 -0
- data/lib/flash_flow/config.rb +71 -0
- data/lib/flash_flow/data.rb +6 -0
- data/lib/flash_flow/data/base.rb +58 -0
- data/lib/flash_flow/data/branch.rb +131 -0
- data/lib/flash_flow/data/collection.rb +181 -0
- data/lib/flash_flow/data/github.rb +129 -0
- data/lib/flash_flow/data/store.rb +33 -0
- data/lib/flash_flow/deploy.rb +184 -0
- data/lib/flash_flow/git.rb +248 -0
- data/lib/flash_flow/install.rb +19 -0
- data/lib/flash_flow/issue_tracker.rb +52 -0
- data/lib/flash_flow/issue_tracker/pivotal.rb +160 -0
- data/lib/flash_flow/lock.rb +25 -0
- data/lib/flash_flow/lock/github.rb +91 -0
- data/lib/flash_flow/notifier.rb +24 -0
- data/lib/flash_flow/notifier/hipchat.rb +36 -0
- data/lib/flash_flow/options.rb +36 -0
- data/lib/flash_flow/time_helper.rb +11 -0
- data/lib/flash_flow/version.rb +3 -0
- data/test/lib/data/test_base.rb +10 -0
- data/test/lib/data/test_branch.rb +203 -0
- data/test/lib/data/test_collection.rb +238 -0
- data/test/lib/data/test_github.rb +23 -0
- data/test/lib/data/test_store.rb +53 -0
- data/test/lib/issue_tracker/test_pivotal.rb +221 -0
- data/test/lib/lock/test_github.rb +70 -0
- data/test/lib/test_branch_merger.rb +76 -0
- data/test/lib/test_config.rb +84 -0
- data/test/lib/test_deploy.rb +175 -0
- data/test/lib/test_git.rb +73 -0
- data/test/lib/test_issue_tracker.rb +43 -0
- data/test/lib/test_notifier.rb +33 -0
- data/test/minitest_helper.rb +38 -0
- 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,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
|