Upgraded Rails and RSpec
[monkeycharger.git] / vendor / rails / railties / lib / rails_generator / commands.rb
blob98531d9771ed63827571618de5da74accc578b31
1 require 'delegate'
2 require 'optparse'
3 require 'fileutils'
4 require 'tempfile'
5 require 'erb'
7 module Rails
8   module Generator
9     module Commands
10       # Here's a convenient way to get a handle on generator commands.
11       # Command.instance('destroy', my_generator) instantiates a Destroy
12       # delegate of my_generator ready to do your dirty work.
13       def self.instance(command, generator)
14         const_get(command.to_s.camelize).new(generator)
15       end
17       # Even more convenient access to commands.  Include Commands in
18       # the generator Base class to get a nice #command instance method
19       # which returns a delegate for the requested command.
20       def self.included(base)
21         base.send(:define_method, :command) do |command|
22           Commands.instance(command, self)
23         end
24       end
27       # Generator commands delegate Rails::Generator::Base and implement
28       # a standard set of actions.  Their behavior is defined by the way
29       # they respond to these actions: Create brings life; Destroy brings
30       # death; List passively observes.
31       #
32       # Commands are invoked by replaying (or rewinding) the generator's
33       # manifest of actions.  See Rails::Generator::Manifest and
34       # Rails::Generator::Base#manifest method that generator subclasses
35       # are required to override.
36       #
37       # Commands allows generators to "plug in" invocation behavior, which
38       # corresponds to the GoF Strategy pattern.
39       class Base < DelegateClass(Rails::Generator::Base)
40         # Replay action manifest.  RewindBase subclass rewinds manifest.
41         def invoke!
42           manifest.replay(self)
43         end
45         def dependency(generator_name, args, runtime_options = {})
46           logger.dependency(generator_name) do
47             self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke!
48           end
49         end
51         # Does nothing for all commands except Create.
52         def class_collisions(*class_names)
53         end
55         # Does nothing for all commands except Create.
56         def readme(*args)
57         end
59         protected
60           def migration_directory(relative_path)
61             directory(@migration_directory = relative_path)
62           end
64           def existing_migrations(file_name)
65             Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/)
66           end
68           def migration_exists?(file_name)
69             not existing_migrations(file_name).empty?
70           end
72           def current_migration_number
73             Dir.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max, file_path|
74               n = File.basename(file_path).split('_', 2).first.to_i
75               if n > max then n else max end
76             end
77           end
79           def next_migration_number
80             current_migration_number + 1
81           end
83           def next_migration_string(padding = 3)
84             "%.#{padding}d" % next_migration_number
85           end
87           def gsub_file(relative_destination, regexp, *args, &block)
88             path = destination_path(relative_destination)
89             content = File.read(path).gsub(regexp, *args, &block)
90             File.open(path, 'wb') { |file| file.write(content) }
91           end
93         private
94           # Ask the user interactively whether to force collision.
95           def force_file_collision?(destination, src, dst, file_options = {}, &block)
96             $stdout.print "overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] "
97             case $stdin.gets.chomp
98               when /\Ad\z/i
99                 Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp|
100                   temp.write render_file(src, file_options, &block)
101                   temp.rewind
102                   $stdout.puts `#{diff_cmd} #{dst} #{temp.path}`
103                 end
104                 puts "retrying"
105                 raise 'retry diff'
106               when /\Aa\z/i
107                 $stdout.puts "forcing #{spec.name}"
108                 options[:collision] = :force
109               when /\Aq\z/i
110                 $stdout.puts "aborting #{spec.name}"
111                 raise SystemExit
112               when /\An\z/i then :skip
113               when /\Ay\z/i then :force
114               else
115                 $stdout.puts <<-HELP
116 Y - yes, overwrite
117 n - no, do not overwrite
118 a - all, overwrite this and all others
119 q - quit, abort
120 d - diff, show the differences between the old and the new
121 h - help, show this help
122 HELP
123                 raise 'retry'
124             end
125           rescue
126             retry
127           end
129           def diff_cmd
130             ENV['RAILS_DIFF'] || 'diff -u'
131           end
133           def render_template_part(template_options)
134             # Getting Sandbox to evaluate part template in it
135             part_binding = template_options[:sandbox].call.sandbox_binding
136             part_rel_path = template_options[:insert]
137             part_path = source_path(part_rel_path)
139             # Render inner template within Sandbox binding
140             rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding)
141             begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id])
142             end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id])
143             begin_mark + rendered_part + end_mark
144           end
146           def template_part_mark(name, id)
147             "<!--[#{name}:#{id}]-->\n"
148           end
149       end
151       # Base class for commands which handle generator actions in reverse, such as Destroy.
152       class RewindBase < Base
153         # Rewind action manifest.
154         def invoke!
155           manifest.rewind(self)
156         end
157       end
160       # Create is the premier generator command.  It copies files, creates
161       # directories, renders templates, and more.
162       class Create < Base
164         # Check whether the given class names are already taken by
165         # Ruby or Rails.  In the future, expand to check other namespaces
166         # such as the rest of the user's app.
167         def class_collisions(*class_names)
168           class_names.flatten.each do |class_name|
169             # Convert to string to allow symbol arguments.
170             class_name = class_name.to_s
172             # Skip empty strings.
173             next if class_name.strip.empty?
175             # Split the class from its module nesting.
176             nesting = class_name.split('::')
177             name = nesting.pop
179             # Extract the last Module in the nesting.
180             last = nesting.inject(Object) { |last, nest|
181               break unless last.const_defined?(nest)
182               last.const_get(nest)
183             }
185             # If the last Module exists, check whether the given
186             # class exists and raise a collision if so.
187             if last and last.const_defined?(name.camelize)
188               raise_class_collision(class_name)
189             end
190           end
191         end
193         # Copy a file from source to destination with collision checking.
194         #
195         # The file_options hash accepts :chmod and :shebang and :collision options.
196         # :chmod sets the permissions of the destination file:
197         #   file 'config/empty.log', 'log/test.log', :chmod => 0664
198         # :shebang sets the #!/usr/bin/ruby line for scripts
199         #   file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby'
200         # :collision sets the collision option only for the destination file: 
201         #   file 'settings/server.yml', 'config/server.yml', :collision => :skip
202         #
203         # Collisions are handled by checking whether the destination file
204         # exists and either skipping the file, forcing overwrite, or asking
205         # the user what to do.
206         def file(relative_source, relative_destination, file_options = {}, &block)
207           # Determine full paths for source and destination files.
208           source              = source_path(relative_source)
209           destination         = destination_path(relative_destination)
210           destination_exists  = File.exists?(destination)
212           # If source and destination are identical then we're done.
213           if destination_exists and identical?(source, destination, &block)
214             return logger.identical(relative_destination) 
215           end
217           # Check for and resolve file collisions.
218           if destination_exists
220             # Make a choice whether to overwrite the file.  :force and
221             # :skip already have their mind made up, but give :ask a shot.
222             choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask
223               when :ask   then force_file_collision?(relative_destination, source, destination, file_options, &block)
224               when :force then :force
225               when :skip  then :skip
226               else raise "Invalid collision option: #{options[:collision].inspect}"
227             end
229             # Take action based on our choice.  Bail out if we chose to
230             # skip the file; otherwise, log our transgression and continue.
231             case choice
232               when :force then logger.force(relative_destination)
233               when :skip  then return(logger.skip(relative_destination))
234               else raise "Invalid collision choice: #{choice}.inspect"
235             end
237           # File doesn't exist so log its unbesmirched creation.
238           else
239             logger.create relative_destination
240           end
242           # If we're pretending, back off now.
243           return if options[:pretend]
245           # Write destination file with optional shebang.  Yield for content
246           # if block given so templaters may render the source file.  If a
247           # shebang is requested, replace the existing shebang or insert a
248           # new one.
249           File.open(destination, 'wb') do |dest|
250             dest.write render_file(source, file_options, &block)
251           end
253           # Optionally change permissions.
254           if file_options[:chmod]
255             FileUtils.chmod(file_options[:chmod], destination)
256           end
258           # Optionally add file to subversion
259           system("svn add #{destination}") if options[:svn]
260         end
262         # Checks if the source and the destination file are identical. If
263         # passed a block then the source file is a template that needs to first
264         # be evaluated before being compared to the destination.
265         def identical?(source, destination, &block)
266           return false if File.directory? destination
267           source      = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source)
268           destination = IO.read(destination)
269           source == destination
270         end
272         # Generate a file for a Rails application using an ERuby template.
273         # Looks up and evaluates a template by name and writes the result.
274         #
275         # The ERB template uses explicit trim mode to best control the
276         # proliferation of whitespace in generated code.  <%- trims leading
277         # whitespace; -%> trims trailing whitespace including one newline.
278         #
279         # A hash of template options may be passed as the last argument.
280         # The options accepted by the file are accepted as well as :assigns,
281         # a hash of variable bindings.  Example:
282         #   template 'foo', 'bar', :assigns => { :action => 'view' }
283         #
284         # Template is implemented in terms of file.  It calls file with a
285         # block which takes a file handle and returns its rendered contents.
286         def template(relative_source, relative_destination, template_options = {})
287           file(relative_source, relative_destination, template_options) do |file|
288             # Evaluate any assignments in a temporary, throwaway binding.
289             vars = template_options[:assigns] || {}
290             b = binding
291             vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }
293             # Render the source file with the temporary binding.
294             ERB.new(file.read, nil, '-').result(b)
295           end
296         end
298         def complex_template(relative_source, relative_destination, template_options = {})
299           options = template_options.dup
300           options[:assigns] ||= {}
301           options[:assigns]['template_for_inclusion'] = render_template_part(template_options)
302           template(relative_source, relative_destination, options)
303         end
305         # Create a directory including any missing parent directories.
306         # Always directories which exist.
307         def directory(relative_path)
308           path = destination_path(relative_path)
309           if File.exists?(path)
310             logger.exists relative_path
311           else
312             logger.create relative_path
313             unless options[:pretend]
314               FileUtils.mkdir_p(path)
315               
316               # Subversion doesn't do path adds, so we need to add
317               # each directory individually.
318               # So stack up the directory tree and add the paths to
319               # subversion in order without recursion.
320               if options[:svn]
321                 stack=[relative_path]
322                 until File.dirname(stack.last) == stack.last # dirname('.') == '.'
323                   stack.push File.dirname(stack.last)
324                 end
325                 stack.reverse_each do |rel_path|
326                   svn_path = destination_path(rel_path)
327                   system("svn add -N #{svn_path}") unless File.directory?(File.join(svn_path, '.svn'))
328                 end
329               end
330             end
331           end
332         end
334         # Display a README.
335         def readme(*relative_sources)
336           relative_sources.flatten.each do |relative_source|
337             logger.readme relative_source
338             puts File.read(source_path(relative_source)) unless options[:pretend]
339           end
340         end
342         # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template.
343         def migration_template(relative_source, relative_destination, template_options = {})
344           migration_directory relative_destination
345           migration_file_name = template_options[:migration_file_name] || file_name
346           raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name)
347           template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options)
348         end
350         def route_resources(*resources)
351           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
352           sentinel = 'ActionController::Routing::Routes.draw do |map|'
354           logger.route "map.resources #{resource_list}"
355           unless options[:pretend]
356             gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
357               "#{match}\n  map.resources #{resource_list}\n"
358             end
359           end
360         end
362         private
363           def render_file(path, options = {})
364             File.open(path, 'rb') do |file|
365               if block_given?
366                 yield file
367               else
368                 content = ''
369                 if shebang = options[:shebang]
370                   content << "#!#{shebang}\n"
371                   if line = file.gets
372                     content << "line\n" if line !~ /^#!/
373                   end
374                 end
375                 content << file.read
376               end
377             end
378           end
380           # Raise a usage error with an informative WordNet suggestion.
381           # Thanks to Florian Gross (flgr).
382           def raise_class_collision(class_name)
383             message = <<end_message
384   The name '#{class_name}' is reserved by Ruby on Rails.
385   Please choose an alternative and run this generator again.
386 end_message
387             if suggest = find_synonyms(class_name)
388               message << "\n  Suggestions:  \n\n"
389               message << suggest.join("\n")
390             end
391             raise UsageError, message
392           end
394           SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/cgi-bin/webwn2.0?stage=2&word=%s&posnumber=1&searchtypenumber=2&senses=&showglosses=1"
396           # Look up synonyms on WordNet.  Thanks to Florian Gross (flgr).
397           def find_synonyms(word)
398             require 'open-uri'
399             require 'timeout'
400             timeout(5) do
401               open(SYNONYM_LOOKUP_URI % word) do |stream|
402                 data = stream.read.gsub("&nbsp;", " ").gsub("<BR>", "")
403                 data.scan(/^Sense \d+\n.+?\n\n/m)
404               end
405             end
406           rescue Exception
407             return nil
408           end
409       end
412       # Undo the actions performed by a generator.  Rewind the action
413       # manifest and attempt to completely erase the results of each action.
414       class Destroy < RewindBase
415         # Remove a file if it exists and is a file.
416         def file(relative_source, relative_destination, file_options = {})
417           destination = destination_path(relative_destination)
418           if File.exists?(destination)
419             logger.rm relative_destination
420             unless options[:pretend]
421               if options[:svn]
422                 # If the file has been marked to be added
423                 # but has not yet been checked in, revert and delete
424                 if options[:svn][relative_destination]
425                   system("svn revert #{destination}")
426                   FileUtils.rm(destination)
427                 else
428                 # If the directory is not in the status list, it
429                 # has no modifications so we can simply remove it
430                   system("svn rm #{destination}")
431                 end  
432               else
433                 FileUtils.rm(destination)
434               end
435             end
436           else
437             logger.missing relative_destination
438             return
439           end
440         end
442         # Templates are deleted just like files and the actions take the
443         # same parameters, so simply alias the file method.
444         alias_method :template, :file
446         # Remove each directory in the given path from right to left.
447         # Remove each subdirectory if it exists and is a directory.
448         def directory(relative_path)
449           parts = relative_path.split('/')
450           until parts.empty?
451             partial = File.join(parts)
452             path = destination_path(partial)
453             if File.exists?(path)
454               if Dir[File.join(path, '*')].empty?
455                 logger.rmdir partial
456                 unless options[:pretend]
457                   if options[:svn]
458                     # If the directory has been marked to be added
459                     # but has not yet been checked in, revert and delete
460                     if options[:svn][relative_path]
461                       system("svn revert #{path}")
462                       FileUtils.rmdir(path)
463                     else
464                     # If the directory is not in the status list, it
465                     # has no modifications so we can simply remove it
466                       system("svn rm #{path}")
467                     end
468                   else
469                     FileUtils.rmdir(path)
470                   end
471                 end
472               else
473                 logger.notempty partial
474               end
475             else
476               logger.missing partial
477             end
478             parts.pop
479           end
480         end
482         def complex_template(*args)
483           # nothing should be done here
484         end
486         # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}".
487         def migration_template(relative_source, relative_destination, template_options = {})
488           migration_directory relative_destination
490           migration_file_name = template_options[:migration_file_name] || file_name
491           unless migration_exists?(migration_file_name)
492             puts "There is no migration named #{migration_file_name}"
493             return
494           end
497           existing_migrations(migration_file_name).each do |file_path|
498             file(relative_source, file_path, template_options)
499           end
500         end
502         def route_resources(*resources)
503           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
504           look_for = "\n  map.resources #{resource_list}\n"
505           logger.route "map.resources #{resource_list}"
506           gsub_file 'config/routes.rb', /(#{look_for})/mi, ''
507         end
508       end
511       # List a generator's action manifest.
512       class List < Base
513         def dependency(generator_name, args, options = {})
514           logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})"
515         end
517         def class_collisions(*class_names)
518           logger.class_collisions class_names.join(', ')
519         end
521         def file(relative_source, relative_destination, options = {})
522           logger.file relative_destination
523         end
525         def template(relative_source, relative_destination, options = {})
526           logger.template relative_destination
527         end
529         def complex_template(relative_source, relative_destination, options = {})
530           logger.template "#{options[:insert]} inside #{relative_destination}"
531         end
533         def directory(relative_path)
534           logger.directory "#{destination_path(relative_path)}/"
535         end
537         def readme(*args)
538           logger.readme args.join(', ')
539         end
540         
541         def migration_template(relative_source, relative_destination, options = {})
542           migration_directory relative_destination
543           logger.migration_template file_name
544         end
546         def route_resources(*resources)
547           resource_list = resources.map { |r| r.to_sym.inspect }.join(', ')
548           logger.route "map.resources #{resource_list}"
549         end
550       end
552       # Update generator's action manifest.
553       class Update < Create
554         def file(relative_source, relative_destination, options = {})
555           # logger.file relative_destination
556         end
558         def template(relative_source, relative_destination, options = {})
559           # logger.template relative_destination
560         end
562         def complex_template(relative_source, relative_destination, template_options = {})
564            begin
565              dest_file = destination_path(relative_destination)
566              source_to_update = File.readlines(dest_file).join
567            rescue Errno::ENOENT
568              logger.missing relative_destination
569              return
570            end
572            logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}"
574            begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id]))
575            end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id]))
577            # Refreshing inner part of the template with freshly rendered part.
578            rendered_part = render_template_part(template_options)
579            source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part)
581            File.open(dest_file, 'w') { |file| file.write(source_to_update) }
582         end
584         def directory(relative_path)
585           # logger.directory "#{destination_path(relative_path)}/"
586         end
587       end
589     end
590   end