monkey_butler 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.gitignore +3 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +28 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +122 -0
  8. data/Guardfile +8 -0
  9. data/LICENSE +202 -0
  10. data/README.md +221 -0
  11. data/Rakefile +16 -0
  12. data/bin/mb +6 -0
  13. data/lib/monkey_butler.rb +1 -0
  14. data/lib/monkey_butler/actions.rb +38 -0
  15. data/lib/monkey_butler/cli.rb +354 -0
  16. data/lib/monkey_butler/databases/abstract_database.rb +49 -0
  17. data/lib/monkey_butler/databases/cassandra_database.rb +69 -0
  18. data/lib/monkey_butler/databases/sqlite_database.rb +105 -0
  19. data/lib/monkey_butler/migrations.rb +52 -0
  20. data/lib/monkey_butler/project.rb +90 -0
  21. data/lib/monkey_butler/targets/base.rb +85 -0
  22. data/lib/monkey_butler/targets/cassandra/cassandra_target.rb +72 -0
  23. data/lib/monkey_butler/targets/cassandra/create_schema_migrations.cql.erb +16 -0
  24. data/lib/monkey_butler/targets/cassandra/migration.cql.erb +0 -0
  25. data/lib/monkey_butler/targets/cocoapods/cocoapods_target.rb +43 -0
  26. data/lib/monkey_butler/targets/cocoapods/podspec.erb +12 -0
  27. data/lib/monkey_butler/targets/maven/maven_target.rb +60 -0
  28. data/lib/monkey_butler/targets/maven/project/.gitignore +6 -0
  29. data/lib/monkey_butler/targets/maven/project/MonkeyButler.iml +19 -0
  30. data/lib/monkey_butler/targets/maven/project/build.gradle +54 -0
  31. data/lib/monkey_butler/targets/sqlite/create_monkey_butler_tables.sql.erb +15 -0
  32. data/lib/monkey_butler/targets/sqlite/migration.sql.erb +11 -0
  33. data/lib/monkey_butler/targets/sqlite/sqlite_target.rb +91 -0
  34. data/lib/monkey_butler/templates/Gemfile.erb +4 -0
  35. data/lib/monkey_butler/templates/gitignore.erb +1 -0
  36. data/lib/monkey_butler/util.rb +71 -0
  37. data/lib/monkey_butler/version.rb +3 -0
  38. data/logo.jpg +0 -0
  39. data/monkey_butler.gemspec +33 -0
  40. data/spec/cli_spec.rb +700 -0
  41. data/spec/databases/cassandra_database_spec.rb +241 -0
  42. data/spec/databases/sqlite_database_spec.rb +181 -0
  43. data/spec/migrations_spec.rb +4 -0
  44. data/spec/project_spec.rb +128 -0
  45. data/spec/sandbox/cassandra/.gitignore +2 -0
  46. data/spec/sandbox/cassandra/.monkey_butler.yml +7 -0
  47. data/spec/sandbox/cassandra/migrations/20140523123443021_create_sandbox.cql.sql +14 -0
  48. data/spec/sandbox/cassandra/sandbox.cql +0 -0
  49. data/spec/sandbox/sqlite/.gitignore +2 -0
  50. data/spec/sandbox/sqlite/.monkey_butler.yml +7 -0
  51. data/spec/sandbox/sqlite/migrations/20140523123443021_create_sandbox.sql +14 -0
  52. data/spec/sandbox/sqlite/sandbox.sql +0 -0
  53. data/spec/spec_helper.rb +103 -0
  54. data/spec/targets/cassandra_target_spec.rb +191 -0
  55. data/spec/targets/cocoapods_target_spec.rb +197 -0
  56. data/spec/targets/maven_target_spec.rb +156 -0
  57. data/spec/targets/sqlite_target_spec.rb +103 -0
  58. data/spec/util_spec.rb +13 -0
  59. metadata +260 -0
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ desc 'Builds and installs the monkey_butler Gem'
8
+ task :install do
9
+ lib = File.expand_path('../lib', __FILE__)
10
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
11
+ require 'monkey_butler/version'
12
+
13
+ system("gem build monkey_butler.gemspec && gem install monkey_butler-#{MonkeyButler::VERSION}.gem")
14
+ end
15
+
16
+ task :default => :spec
data/bin/mb ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'monkey_butler'
4
+ require 'monkey_butler/cli'
5
+
6
+ MonkeyButler::CLI.start(ARGV)
@@ -0,0 +1 @@
1
+ require 'monkey_butler/cli'
@@ -0,0 +1,38 @@
1
+ module MonkeyButler
2
+ module Actions
3
+ # Run a command in git.
4
+ #
5
+ # git :init
6
+ # git add: "this.file that.rb"
7
+ # git add: "onefile.rb", rm: "badfile.cxx"
8
+ def git(commands={})
9
+ if commands.is_a?(Symbol)
10
+ run "git #{commands}"
11
+ else
12
+ commands.each do |cmd, options|
13
+ run "git #{cmd} #{options}"
14
+ end
15
+ end
16
+ end
17
+
18
+ def git_add(*paths)
19
+ inside(destination_root) do
20
+ git add: paths.flatten.join(' ')
21
+ end
22
+ end
23
+
24
+ def truncate_database
25
+ say_status :truncate, database.to_s, :yellow
26
+ database.drop
27
+ end
28
+
29
+ def bundle
30
+ inside(destination_root) { run "bundle" }
31
+ end
32
+
33
+ def unique_tag_for_version(version)
34
+ return version if options['pretend']
35
+ MonkeyButler::Util.unique_tag_for_version(version)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,354 @@
1
+ require 'thor'
2
+ require "open3"
3
+ require 'monkey_butler/project'
4
+ require 'monkey_butler/actions'
5
+ require 'monkey_butler/databases/abstract_database'
6
+ require 'monkey_butler/migrations'
7
+ require 'monkey_butler/util'
8
+ require 'monkey_butler/targets/base'
9
+
10
+ module MonkeyButler
11
+ class CLI < Thor
12
+ include Thor::Actions
13
+ include MonkeyButler::Actions
14
+
15
+ add_runtime_options!
16
+
17
+ # Configures root path for resources (e.g. templates)
18
+ def self.source_root
19
+ File.dirname(__FILE__)
20
+ end
21
+
22
+ attr_reader :project
23
+
24
+ desc 'init [PATH]', 'Initializes a new repository into PATH'
25
+ method_option :name, type: :string, aliases: '-n', desc: "Specify project name"
26
+ method_option :database, type: :string, aliases: '-d', desc: "Specify database path or URL."
27
+ method_option :targets, type: :array, aliases: '-g', default: [], desc: "Specify default code targets."
28
+ method_option :bundler, type: :boolean, aliases: '-b', default: false, desc: "Use Bundler to import MonkeyButler into project."
29
+ method_option :config, type: :hash, aliases: '-c', default: {}, desc: "Specify config variables."
30
+ def init(path)
31
+ if File.exists?(path)
32
+ raise Error, "Cannot create repository: regular file exists at path '#{path}'" unless File.directory?(path)
33
+ raise Error, "Cannot create repository into non-empty path '#{path}'" if File.directory?(path) && Dir.entries(path) != %w{. ..}
34
+ end
35
+ self.destination_root = File.expand_path(path)
36
+ empty_directory('.')
37
+ inside(destination_root) { git init: '-q' }
38
+
39
+ # hydrate the project
40
+ project_name = options['name'] || File.basename(path)
41
+ sanitized_options = options.reject { |k,v| %w{bundler pretend database}.include?(k) }
42
+ sanitized_options[:name] = project_name
43
+ sanitized_options[:database_url] = options[:database] || "sqlite:#{project_name}.sqlite"
44
+ @project = MonkeyButler::Project.set(sanitized_options)
45
+
46
+ # generate_gitignore
47
+ template('templates/gitignore.erb', ".gitignore")
48
+ git_add '.gitignore'
49
+
50
+ # generate_config
51
+ create_file '.monkey_butler.yml', YAML.dump(sanitized_options)
52
+ git_add '.monkey_butler.yml'
53
+
54
+ # generate_gemfile
55
+ if options['bundler']
56
+ template('templates/Gemfile.erb', "Gemfile")
57
+ end
58
+
59
+ # init_targets
60
+ project = MonkeyButler::Project.set(sanitized_options)
61
+ target_options = options.merge('name' => project_name)
62
+ MonkeyButler::Util.target_classes_named(options[:targets]) do |target_class|
63
+ say "Initializing target '#{target_class.name}'..."
64
+ invoke(target_class, :init, [], target_options)
65
+ end
66
+ project.save!(destination_root) unless options['pretend']
67
+ git_add '.monkey_butler.yml'
68
+
69
+ # Run after targets in case they modify the Gemfile
70
+ # run_bundler
71
+ if options['bundler']
72
+ git_add "Gemfile"
73
+ bundle
74
+ git_add "Gemfile.lock"
75
+ end
76
+
77
+ # touch_database
78
+ create_file(project.schema_path)
79
+ git_add project.schema_path
80
+
81
+ # init_database_adapter
82
+ empty_directory('migrations')
83
+ inside do
84
+ invoke(project.database_target_class, :init, [], target_options)
85
+ end
86
+ end
87
+
88
+ desc "dump", "Dump project schema from a database"
89
+ def dump
90
+ @project = MonkeyButler::Project.load
91
+ invoke(project.database_target_class, :dump, [], options)
92
+ end
93
+
94
+ desc "load", "Load project schema into a database"
95
+ def load
96
+ @project = MonkeyButler::Project.load
97
+ invoke(project.database_target_class, :load, [], options)
98
+ end
99
+
100
+ desc "drop", "Drop the schema currently loaded into a database"
101
+ def drop
102
+ @project = MonkeyButler::Project.load
103
+ invoke(project.database_target_class, :drop, [], options)
104
+ end
105
+
106
+ desc "new NAME", "Create a new migration"
107
+ def new(name)
108
+ @project = MonkeyButler::Project.load
109
+ empty_directory('migrations')
110
+ invoke(project.database_target_class, :new, [name], options)
111
+ end
112
+
113
+ desc "status", "Display current schema version and any pending migrations"
114
+ method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
115
+ def status
116
+ project = MonkeyButler::Project.load
117
+ migrations = MonkeyButler::Migrations.new(project.migrations_path, database)
118
+
119
+ if database.migrations_table?
120
+ say "Current version: #{migrations.current_version}"
121
+ pending_count = migrations.pending.size
122
+ version = (pending_count == 1) ? "version" : "versions"
123
+ say "The database at '#{database}' is #{pending_count} #{version} behind #{migrations.latest_version}" unless migrations.up_to_date?
124
+ else
125
+ say "New database"
126
+ say "The database at '#{database}' does not have a 'schema_migrations' table."
127
+ end
128
+
129
+ if migrations.up_to_date?
130
+ say "Database is up to date."
131
+ return
132
+ end
133
+
134
+ say
135
+ say "Migrations to be applied"
136
+ with_padding do
137
+ say %q{(use "mb migrate" to apply)}
138
+ say
139
+ with_padding do
140
+ migrations.pending do |version, path|
141
+ say "pending migration: #{path}", :green
142
+ end
143
+ end
144
+ say
145
+ end
146
+ end
147
+
148
+ desc "migrate [VERSION]", "Apply pending migrations to a database"
149
+ method_option :database, type: :string, aliases: '-d', desc: "Set target DATABASE"
150
+ method_option :dump, type: :boolean, aliases: '-D', desc: "Dump schema after migrate"
151
+ def migrate(version = nil)
152
+ project = MonkeyButler::Project.load
153
+
154
+ if migrations.up_to_date?
155
+ say "Database is up to date."
156
+ return
157
+ end
158
+
159
+ target_version = version || migrations.latest_version
160
+ if database.migrations_table?
161
+ say "Migrating from #{database.current_version} to #{target_version}"
162
+ else
163
+ say "Migrating new database to #{target_version}"
164
+ end
165
+ say
166
+
167
+ with_padding do
168
+ say "Migrating database..."
169
+ say
170
+ with_padding do
171
+ migrations.pending do |version, path|
172
+ say "applying migration: #{path}", :green
173
+ begin
174
+ database.execute_migration(File.read(path))
175
+ database.insert_version(version)
176
+ rescue project.database_class.exception_class => exception
177
+ fail Error, "Failed loading migration: #{exception}"
178
+ end
179
+ end
180
+ end
181
+ say
182
+ end
183
+
184
+ say "Migration to version #{target_version} complete."
185
+
186
+ if options['dump']
187
+ say
188
+ invoke :dump, [], options
189
+ end
190
+ end
191
+
192
+ desc "validate", "Validate that schema loads and all migrations are linearly applicable"
193
+ def validate
194
+ project = MonkeyButler::Project.load
195
+
196
+ say "Validating project configuration..."
197
+ say_status :git, "configuration", (project.git_url.empty? ? :red : :green)
198
+ if project.git_url.empty?
199
+ fail Error, "Invalid configuration: git does not have a remote named 'origin'."
200
+ end
201
+ say
202
+
203
+ invoke(project.database_target_class, :validate, [], options)
204
+
205
+ say "Validating schema loads..."
206
+ truncate_database
207
+ load
208
+ say
209
+
210
+ say "Validating migrations apply..."
211
+ truncate_database
212
+ migrate
213
+ say
214
+
215
+ say "Validating targets..."
216
+ target_names = options['targets'] || project.targets
217
+ MonkeyButler::Util.target_classes_named(target_names) do |target_class|
218
+ with_padding do
219
+ say_status :validate, target_class.name
220
+ invoke_with_padding(target_class, :validate, [], options)
221
+ end
222
+ end
223
+ say
224
+
225
+ say "Validation successful."
226
+ end
227
+
228
+ desc "generate", "Generate platform specific migration implementations"
229
+ method_option :targets, type: :array, aliases: '-t', desc: "Generate only the specified targets."
230
+ def generate
231
+ project = MonkeyButler::Project.load
232
+ invoke(project.database_target_class, :generate, [], options)
233
+ target_names = options['targets'] || project.targets
234
+ MonkeyButler::Util.target_classes_named(target_names) do |target_class|
235
+ say "Invoking target '#{target_class.name}'..."
236
+ invoke(target_class, :generate, [], options)
237
+ end
238
+ end
239
+
240
+ desc "package", "Package a release by validating, generating, and tagging a version."
241
+ method_option :diff, type: :boolean, desc: "Show Git diff after packaging"
242
+ method_option :commit, type: :boolean, desc: "Commit package artifacts after build."
243
+ def package
244
+ validate
245
+ say
246
+ generate
247
+ say
248
+
249
+ git_add '.'
250
+ git :status unless options['quiet']
251
+
252
+ show_diff = options['diff'] != false && (options['diff'] || ask("Review package diff?", limited_to: %w{y n}) == 'y')
253
+ git diff: '--cached' if show_diff
254
+
255
+ commit = options['commit'] != false && (options['commit'] || ask("Commit package artifacts?", limited_to: %w{y n}) == 'y')
256
+ if commit
257
+ tag = unique_tag_for_version(migrations.latest_version)
258
+ git commit: "#{options['quiet'] && '-q '}-m 'Packaging release #{tag}' ."
259
+ git tag: "#{tag}"
260
+ else
261
+ say "Package artifacts were built but not committed. Re-run `mb package` when ready to complete build."
262
+ end
263
+ end
264
+
265
+ desc "push", "Push a release to Git, CocoaPods, Maven, etc."
266
+ def push
267
+ # Verify that the tag exists
268
+ git tag: "-l #{migrations.latest_version}"
269
+ unless $?.exitstatus.zero?
270
+ fail Error, "Could not find tag #{migrations.latest_version}. Did you forget to run `mb package`?"
271
+ end
272
+ push_options = []
273
+ push_options << '--force' if options['force']
274
+ branch_name = project.git_current_branch
275
+ run "git config branch.`git symbolic-ref --short HEAD`.merge", verbose: false
276
+ unless $?.exitstatus.zero?
277
+ say_status :git, "no merge branch detected: setting upstream during push", :yellow
278
+ push_options << "--set-upstream origin #{branch_name}"
279
+ end
280
+ push_options << "origin #{branch_name}"
281
+ push_options << "--tags"
282
+
283
+ git push: push_options.join(' ')
284
+ unless $?.exitstatus.zero?
285
+ fail Error, "git push failed."
286
+ end
287
+
288
+ # Give the targets a chance to push
289
+ target_names = options['targets'] || project.targets
290
+ MonkeyButler::Util.target_classes_named(target_names) do |target_class|
291
+ say "Invoking target '#{target_class.name}'..."
292
+ invoke(target_class, :push, [], options)
293
+ end
294
+ end
295
+
296
+ desc "config", "Get and set configuration options."
297
+ def config(key = nil, value = nil)
298
+ if key && value
299
+ project.config[key] = value
300
+ project.save!(Dir.pwd)
301
+ elsif key
302
+ value = project.config[key]
303
+ if value
304
+ say "#{key}=#{value}"
305
+ else
306
+ say "No value for key '#{key}'"
307
+ end
308
+ else
309
+ project.config.each { |key, value| say "#{key}=#{value}" }
310
+ end
311
+ end
312
+
313
+ # Hook into the command execution for dynamic task configuration
314
+ def self.start(given_args = ARGV, config = {})
315
+ if File.exists?(Dir.pwd + '/.monkey_butler.yml')
316
+ project = MonkeyButler::Project.load
317
+ project.database_target_class.register_with_cli(self)
318
+ end
319
+ super
320
+ end
321
+
322
+ private
323
+ def unique_tag_for_version(version)
324
+ return version if options['pretend']
325
+
326
+ revision = nil
327
+ tag = nil
328
+ begin
329
+ tag = [version, revision].compact.join('.')
330
+ existing_tag = run "git tag -l #{tag}", capture: true
331
+ break if existing_tag == ""
332
+ revision = revision.to_i + 1
333
+ end while true
334
+ tag
335
+ end
336
+
337
+ private
338
+ def bundle
339
+ inside(destination_root) { run "bundle" }
340
+ end
341
+
342
+ def project
343
+ @project ||= MonkeyButler::Project.load
344
+ end
345
+
346
+ def database
347
+ @database ||= project.database_class.new((options[:database] && URI(options[:database])) || project.database_url)
348
+ end
349
+
350
+ def migrations
351
+ @migrations ||= MonkeyButler::Migrations.new(project.migrations_path, database)
352
+ end
353
+ end
354
+ end
@@ -0,0 +1,49 @@
1
+ module MonkeyButler
2
+ module Databases
3
+ class AbstractDatabase
4
+ class << self
5
+ def migration_ext
6
+ raise NotImplementedError, "Required method not implemented."
7
+ end
8
+ end
9
+
10
+ def migrations_table?
11
+ raise NotImplementedError, "Required method not implemented."
12
+ end
13
+
14
+ def origin_version
15
+ raise NotImplementedError, "Required method not implemented."
16
+ end
17
+
18
+ def current_version
19
+ raise NotImplementedError, "Required method not implemented."
20
+ end
21
+
22
+ def all_versions
23
+ raise NotImplementedError, "Required method not implemented."
24
+ end
25
+
26
+ def insert_version(version)
27
+ raise NotImplementedError, "Required method not implemented."
28
+ end
29
+
30
+ def execute_migration(content)
31
+ raise NotImplementedError, "Required method not implemented."
32
+ end
33
+
34
+ def drop
35
+ raise NotImplementedError, "Required method not implemented."
36
+ end
37
+
38
+ attr_reader :url
39
+
40
+ def initialize(url)
41
+ @url = url
42
+ end
43
+
44
+ def to_s
45
+ url.to_s
46
+ end
47
+ end
48
+ end
49
+ end