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.
- 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
|