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