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