monkey_butler 1.2.2

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