engineyard 0.5.3 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/lib/engineyard/cli.rb +71 -44
  2. data/lib/engineyard/cli/recipes.rb +17 -17
  3. data/lib/engineyard/cli/ui.rb +4 -8
  4. data/lib/engineyard/cli/web.rb +14 -15
  5. data/lib/engineyard/model/environment.rb +1 -1
  6. data/lib/engineyard/model/instance.rb +16 -4
  7. data/lib/engineyard/thor.rb +24 -18
  8. data/lib/engineyard/vendor/thor.rb +270 -0
  9. data/lib/engineyard/vendor/thor/actions.rb +297 -0
  10. data/lib/engineyard/vendor/thor/actions/create_file.rb +105 -0
  11. data/lib/engineyard/vendor/thor/actions/directory.rb +93 -0
  12. data/lib/engineyard/vendor/thor/actions/empty_directory.rb +134 -0
  13. data/lib/engineyard/vendor/thor/actions/file_manipulation.rb +229 -0
  14. data/lib/engineyard/vendor/thor/actions/inject_into_file.rb +104 -0
  15. data/lib/engineyard/vendor/thor/base.rb +540 -0
  16. data/lib/engineyard/vendor/thor/core_ext/file_binary_read.rb +9 -0
  17. data/lib/engineyard/vendor/thor/core_ext/hash_with_indifferent_access.rb +75 -0
  18. data/lib/engineyard/vendor/thor/core_ext/ordered_hash.rb +100 -0
  19. data/lib/engineyard/vendor/thor/error.rb +30 -0
  20. data/lib/engineyard/vendor/thor/group.rb +271 -0
  21. data/lib/engineyard/vendor/thor/invocation.rb +180 -0
  22. data/lib/engineyard/vendor/thor/parser.rb +4 -0
  23. data/lib/engineyard/vendor/thor/parser/argument.rb +67 -0
  24. data/lib/engineyard/vendor/thor/parser/arguments.rb +161 -0
  25. data/lib/engineyard/vendor/thor/parser/option.rb +128 -0
  26. data/lib/engineyard/vendor/thor/parser/options.rb +164 -0
  27. data/lib/engineyard/vendor/thor/rake_compat.rb +66 -0
  28. data/lib/engineyard/vendor/thor/runner.rb +314 -0
  29. data/lib/engineyard/vendor/thor/shell.rb +83 -0
  30. data/lib/engineyard/vendor/thor/shell/basic.rb +268 -0
  31. data/lib/engineyard/vendor/thor/shell/color.rb +108 -0
  32. data/lib/engineyard/vendor/thor/task.rb +102 -0
  33. data/lib/engineyard/vendor/thor/util.rb +229 -0
  34. data/lib/engineyard/vendor/thor/version.rb +3 -0
  35. data/lib/engineyard/version.rb +1 -1
  36. data/spec/ey/deploy_spec.rb +24 -5
  37. data/spec/ey/ey_spec.rb +1 -1
  38. data/spec/ey/rollback_spec.rb +7 -0
  39. data/spec/spec_helper.rb +13 -0
  40. data/spec/support/git_repo.rb +10 -7
  41. data/spec/support/helpers.rb +2 -2
  42. metadata +30 -4
  43. data/lib/engineyard/cli/thor_fixes.rb +0 -26
@@ -0,0 +1,102 @@
1
+ class Thor
2
+ class Task < Struct.new(:name, :description, :long_description, :usage, :options)
3
+ FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
4
+
5
+ # A dynamic task that handles method missing scenarios.
6
+ class Dynamic < Task
7
+ def initialize(name, options=nil)
8
+ super(name.to_s, "A dynamically-generated task", name.to_s, name.to_s, options)
9
+ end
10
+
11
+ def run(instance, args=[])
12
+ if (instance.methods & [name.to_s, name.to_sym]).empty?
13
+ super
14
+ else
15
+ instance.class.handle_no_task_error(name)
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(name, description, long_description, usage, options=nil)
21
+ super(name.to_s, description, long_description, usage, options || {})
22
+ end
23
+
24
+ def initialize_copy(other) #:nodoc:
25
+ super(other)
26
+ self.options = other.options.dup if other.options
27
+ end
28
+
29
+ # By default, a task invokes a method in the thor class. You can change this
30
+ # implementation to create custom tasks.
31
+ def run(instance, args=[])
32
+ public_method?(instance) ?
33
+ instance.send(name, *args) : instance.class.handle_no_task_error(name)
34
+ rescue ArgumentError => e
35
+ handle_argument_error?(instance, e, caller) ?
36
+ instance.class.handle_argument_error(self, e) : (raise e)
37
+ rescue NoMethodError => e
38
+ handle_no_method_error?(instance, e, caller) ?
39
+ instance.class.handle_no_task_error(name) : (raise e)
40
+ end
41
+
42
+ # Returns the formatted usage by injecting given required arguments
43
+ # and required options into the given usage.
44
+ def formatted_usage(klass, namespace=true)
45
+ namespace = klass.namespace unless namespace == false
46
+
47
+ # Add namespace
48
+ formatted = if namespace
49
+ "#{namespace.gsub(/^(default|thor:runner:)/,'')}:"
50
+ else
51
+ ""
52
+ end
53
+
54
+ # Add usage with required arguments
55
+ formatted << if klass && !klass.arguments.empty?
56
+ usage.to_s.gsub(/^#{name}/) do |match|
57
+ match << " " << klass.arguments.map{ |a| a.usage }.compact.join(' ')
58
+ end
59
+ else
60
+ usage.to_s
61
+ end
62
+
63
+ # Add required options
64
+ formatted << " #{required_options}"
65
+
66
+ # Strip and go!
67
+ formatted.strip
68
+ end
69
+
70
+ protected
71
+
72
+ def not_debugging?(instance)
73
+ !(instance.class.respond_to?(:debugging) && instance.class.debugging)
74
+ end
75
+
76
+ def required_options
77
+ @required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ")
78
+ end
79
+
80
+ # Given a target, checks if this class name is not a private/protected method.
81
+ def public_method?(instance) #:nodoc:
82
+ collection = instance.private_methods + instance.protected_methods
83
+ (collection & [name.to_s, name.to_sym]).empty?
84
+ end
85
+
86
+ def sans_backtrace(backtrace, caller) #:nodoc:
87
+ saned = backtrace.reject { |frame| frame =~ FILE_REGEXP }
88
+ saned -= caller
89
+ end
90
+
91
+ def handle_argument_error?(instance, error, caller)
92
+ not_debugging?(instance) && error.message =~ /wrong number of arguments/ &&
93
+ sans_backtrace(error.backtrace, caller).empty?
94
+ end
95
+
96
+ def handle_no_method_error?(instance, error, caller)
97
+ not_debugging?(instance) &&
98
+ error.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,229 @@
1
+ require 'rbconfig'
2
+
3
+ class Thor
4
+ module Sandbox #:nodoc:
5
+ end
6
+
7
+ # This module holds several utilities:
8
+ #
9
+ # 1) Methods to convert thor namespaces to constants and vice-versa.
10
+ #
11
+ # Thor::Utils.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz"
12
+ #
13
+ # 2) Loading thor files and sandboxing:
14
+ #
15
+ # Thor::Utils.load_thorfile("~/.thor/foo")
16
+ #
17
+ module Util
18
+
19
+ # Receives a namespace and search for it in the Thor::Base subclasses.
20
+ #
21
+ # ==== Parameters
22
+ # namespace<String>:: The namespace to search for.
23
+ #
24
+ def self.find_by_namespace(namespace)
25
+ namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/
26
+ Thor::Base.subclasses.find { |klass| klass.namespace == namespace }
27
+ end
28
+
29
+ # Receives a constant and converts it to a Thor namespace. Since Thor tasks
30
+ # can be added to a sandbox, this method is also responsable for removing
31
+ # the sandbox namespace.
32
+ #
33
+ # This method should not be used in general because it's used to deal with
34
+ # older versions of Thor. On current versions, if you need to get the
35
+ # namespace from a class, just call namespace on it.
36
+ #
37
+ # ==== Parameters
38
+ # constant<Object>:: The constant to be converted to the thor path.
39
+ #
40
+ # ==== Returns
41
+ # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz"
42
+ #
43
+ def self.namespace_from_thor_class(constant)
44
+ constant = constant.to_s.gsub(/^Thor::Sandbox::/, "")
45
+ constant = snake_case(constant).squeeze(":")
46
+ constant
47
+ end
48
+
49
+ # Given the contents, evaluate it inside the sandbox and returns the
50
+ # namespaces defined in the sandbox.
51
+ #
52
+ # ==== Parameters
53
+ # contents<String>
54
+ #
55
+ # ==== Returns
56
+ # Array[Object]
57
+ #
58
+ def self.namespaces_in_content(contents, file=__FILE__)
59
+ old_constants = Thor::Base.subclasses.dup
60
+ Thor::Base.subclasses.clear
61
+
62
+ load_thorfile(file, contents)
63
+
64
+ new_constants = Thor::Base.subclasses.dup
65
+ Thor::Base.subclasses.replace(old_constants)
66
+
67
+ new_constants.map!{ |c| c.namespace }
68
+ new_constants.compact!
69
+ new_constants
70
+ end
71
+
72
+ # Returns the thor classes declared inside the given class.
73
+ #
74
+ def self.thor_classes_in(klass)
75
+ stringfied_constants = klass.constants.map { |c| c.to_s }
76
+ Thor::Base.subclasses.select do |subclass|
77
+ next unless subclass.name
78
+ stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", ''))
79
+ end
80
+ end
81
+
82
+ # Receives a string and convert it to snake case. SnakeCase returns snake_case.
83
+ #
84
+ # ==== Parameters
85
+ # String
86
+ #
87
+ # ==== Returns
88
+ # String
89
+ #
90
+ def self.snake_case(str)
91
+ return str.downcase if str =~ /^[A-Z_]+$/
92
+ str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/
93
+ return $+.downcase
94
+ end
95
+
96
+ # Receives a string and convert it to camel case. camel_case returns CamelCase.
97
+ #
98
+ # ==== Parameters
99
+ # String
100
+ #
101
+ # ==== Returns
102
+ # String
103
+ #
104
+ def self.camel_case(str)
105
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
106
+ str.split('_').map { |i| i.capitalize }.join
107
+ end
108
+
109
+ # Receives a namespace and tries to retrieve a Thor or Thor::Group class
110
+ # from it. It first searches for a class using the all the given namespace,
111
+ # if it's not found, removes the highest entry and searches for the class
112
+ # again. If found, returns the highest entry as the class name.
113
+ #
114
+ # ==== Examples
115
+ #
116
+ # class Foo::Bar < Thor
117
+ # def baz
118
+ # end
119
+ # end
120
+ #
121
+ # class Baz::Foo < Thor::Group
122
+ # end
123
+ #
124
+ # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task
125
+ # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil
126
+ # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz"
127
+ #
128
+ # ==== Parameters
129
+ # namespace<String>
130
+ #
131
+ def self.find_class_and_task_by_namespace(namespace, fallback = true)
132
+ if namespace.include?(?:) # look for a namespaced task
133
+ pieces = namespace.split(":")
134
+ task = pieces.pop
135
+ klass = Thor::Util.find_by_namespace(pieces.join(":"))
136
+ end
137
+ unless klass # look for a Thor::Group with the right name
138
+ klass, task = Thor::Util.find_by_namespace(namespace), nil
139
+ end
140
+ if !klass && fallback # try a task in the default namespace
141
+ task = namespace
142
+ klass = Thor::Util.find_by_namespace('')
143
+ end
144
+ return klass, task
145
+ end
146
+
147
+ # Receives a path and load the thor file in the path. The file is evaluated
148
+ # inside the sandbox to avoid namespacing conflicts.
149
+ #
150
+ def self.load_thorfile(path, content=nil, debug=false)
151
+ content ||= File.binread(path)
152
+
153
+ begin
154
+ Thor::Sandbox.class_eval(content, path)
155
+ rescue Exception => e
156
+ $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}"
157
+ if debug
158
+ $stderr.puts *e.backtrace
159
+ else
160
+ $stderr.puts e.backtrace.first
161
+ end
162
+ end
163
+ end
164
+
165
+ def self.user_home
166
+ @@user_home ||= if ENV["HOME"]
167
+ ENV["HOME"]
168
+ elsif ENV["USERPROFILE"]
169
+ ENV["USERPROFILE"]
170
+ elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"]
171
+ File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"])
172
+ elsif ENV["APPDATA"]
173
+ ENV["APPDATA"]
174
+ else
175
+ begin
176
+ File.expand_path("~")
177
+ rescue
178
+ if File::ALT_SEPARATOR
179
+ "C:/"
180
+ else
181
+ "/"
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # Returns the root where thor files are located, dependending on the OS.
188
+ #
189
+ def self.thor_root
190
+ File.join(user_home, ".thor").gsub(/\\/, '/')
191
+ end
192
+
193
+ # Returns the files in the thor root. On Windows thor_root will be something
194
+ # like this:
195
+ #
196
+ # C:\Documents and Settings\james\.thor
197
+ #
198
+ # If we don't #gsub the \ character, Dir.glob will fail.
199
+ #
200
+ def self.thor_root_glob
201
+ files = Dir["#{thor_root}/*"]
202
+
203
+ files.map! do |file|
204
+ File.directory?(file) ? File.join(file, "main.thor") : file
205
+ end
206
+ end
207
+
208
+ # Where to look for Thor files.
209
+ #
210
+ def self.globs_for(path)
211
+ ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"]
212
+ end
213
+
214
+ # Return the path to the ruby interpreter taking into account multiple
215
+ # installations and windows extensions.
216
+ #
217
+ def self.ruby_command
218
+ @ruby_command ||= begin
219
+ ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name'])
220
+ ruby << Config::CONFIG['EXEEXT']
221
+
222
+ # escape string in case path to ruby executable contain spaces.
223
+ ruby.sub!(/.*\s.*/m, '"\&"')
224
+ ruby
225
+ end
226
+ end
227
+
228
+ end
229
+ end
@@ -0,0 +1,3 @@
1
+ class Thor
2
+ VERSION = "0.13.6".freeze
3
+ end
@@ -1,3 +1,3 @@
1
1
  module EY
2
- VERSION = '0.5.3'
2
+ VERSION = '0.5.4'
3
3
  end
@@ -197,9 +197,17 @@ describe "ey deploy" do
197
197
  context "specifying the application" do
198
198
  before(:all) do
199
199
  api_scenario "one app, one environment"
200
+ end
201
+
202
+ before(:each) do
203
+ @_deploy_spec_start_dir = Dir.getwd
200
204
  Dir.chdir(File.expand_path("~"))
201
205
  end
202
206
 
207
+ after(:each) do
208
+ Dir.chdir(@_deploy_spec_start_dir)
209
+ end
210
+
203
211
  it "allows you to specify an app when not in a directory" do
204
212
  ey "deploy --app rails232app --ref master"
205
213
  @ssh_commands.last.should match(/--app rails232app/)
@@ -212,10 +220,21 @@ describe "ey deploy" do
212
220
  end
213
221
  end
214
222
 
215
- it "passes along the repository URL to eysd" do
216
- api_scenario "one app, one environment", "user@git.host:path/to/repo.git"
217
- ey "deploy"
218
- @ssh_commands.last.should =~ /--repo user@git.host:path\/to\/repo.git/
219
- end
223
+ context "sending necessary information" do
224
+ use_git_repo("deploy test")
220
225
 
226
+ before(:all) do
227
+ api_scenario "one app, one environment", "user@git.host:path/to/repo.git"
228
+ ey "deploy"
229
+ @deploy_command = @ssh_commands.find {|c| c =~ /eysd deploy/ }
230
+ end
231
+
232
+ it "passes along the repository URL to eysd" do
233
+ @deploy_command.should =~ /--repo user@git.host:path\/to\/repo.git/
234
+ end
235
+
236
+ it "passes along the web server stack to eysd" do
237
+ @deploy_command.should =~ /--stack nginx_mongrel/
238
+ end
239
+ end
221
240
  end
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe "ey" do
4
4
  context "run without arguments" do
5
5
  it "prints usage information" do
6
- ey.should include("Commands:")
6
+ ey.should include("Usage:")
7
7
  end
8
8
  end
9
9
 
@@ -17,4 +17,11 @@ describe "ey rollback" do
17
17
 
18
18
  it_should_behave_like "it takes an environment name"
19
19
  it_should_behave_like "it invokes eysd"
20
+
21
+ it "passes along the web server stack to eysd" do
22
+ api_scenario "one app, one environment"
23
+ ey "rollback"
24
+ @ssh_commands.last.should =~ /--stack nginx_mongrel/
25
+ end
26
+
20
27
  end
@@ -14,6 +14,19 @@ end
14
14
  require 'fakeweb'
15
15
  require 'fakeweb_matcher'
16
16
  require 'fakefs/safe'
17
+ module FakeFS
18
+ def self.activated?
19
+ Object.const_get(:Dir) == FakeFS::Dir
20
+ end
21
+
22
+ def self.without
23
+ was_on = activated?
24
+ deactivate!
25
+ yield
26
+ activate! if was_on
27
+ end
28
+ end
29
+
17
30
  require 'json'
18
31
 
19
32
  # Engineyard gem
@@ -3,19 +3,22 @@ module Spec
3
3
  def define_git_repo(name, &setup)
4
4
  # EY's ivars don't get cleared between examples, so we can keep
5
5
  # a git repo around longer (and thus make our tests faster)
6
- EY.define_git_repo(name, &setup)
6
+ FakeFS.without { EY.define_git_repo(name, &setup) }
7
7
  end
8
8
 
9
9
  def use_git_repo(repo_name)
10
- before(:each) do
11
- @_original_wd ||= []
12
- @_original_wd << Dir.getwd
13
- Dir.chdir(EY.git_repo_dir(repo_name))
10
+ before(:all) do
11
+ FakeFS.without do
12
+ @_original_wd ||= []
13
+ @_original_wd << Dir.getwd
14
+ Dir.chdir(EY.git_repo_dir(repo_name))
15
+ end
14
16
  end
15
17
 
16
- after(:each) do
17
- Dir.chdir(@_original_wd.pop)
18
+ after(:all) do
19
+ FakeFS.without { Dir.chdir(@_original_wd.pop) }
18
20
  end
19
21
  end
22
+
20
23
  end
21
24
  end