X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=vendor%2Frails%2Frailties%2Flib%2Frails_generator%2Fcommands.rb;fp=vendor%2Frails%2Frailties%2Flib%2Frails_generator%2Fcommands.rb;h=6b9a63684781f2c99f709d0747a4d8fffc84c194;hb=d115f2e23823271635bad69229a42cd8ac68debe;hp=0000000000000000000000000000000000000000;hpb=37cb670bf3ddde90b214e591f100ed4446469484;p=depot.git diff --git a/vendor/rails/railties/lib/rails_generator/commands.rb b/vendor/rails/railties/lib/rails_generator/commands.rb new file mode 100644 index 0000000..6b9a636 --- /dev/null +++ b/vendor/rails/railties/lib/rails_generator/commands.rb @@ -0,0 +1,616 @@ +require 'delegate' +require 'optparse' +require 'fileutils' +require 'tempfile' +require 'erb' + +module Rails + module Generator + module Commands + # Here's a convenient way to get a handle on generator commands. + # Command.instance('destroy', my_generator) instantiates a Destroy + # delegate of my_generator ready to do your dirty work. + def self.instance(command, generator) + const_get(command.to_s.camelize).new(generator) + end + + # Even more convenient access to commands. Include Commands in + # the generator Base class to get a nice #command instance method + # which returns a delegate for the requested command. + def self.included(base) + base.send(:define_method, :command) do |command| + Commands.instance(command, self) + end + end + + + # Generator commands delegate Rails::Generator::Base and implement + # a standard set of actions. Their behavior is defined by the way + # they respond to these actions: Create brings life; Destroy brings + # death; List passively observes. + # + # Commands are invoked by replaying (or rewinding) the generator's + # manifest of actions. See Rails::Generator::Manifest and + # Rails::Generator::Base#manifest method that generator subclasses + # are required to override. + # + # Commands allows generators to "plug in" invocation behavior, which + # corresponds to the GoF Strategy pattern. + class Base < DelegateClass(Rails::Generator::Base) + # Replay action manifest. RewindBase subclass rewinds manifest. + def invoke! + manifest.replay(self) + end + + def dependency(generator_name, args, runtime_options = {}) + logger.dependency(generator_name) do + self.class.new(instance(generator_name, args, full_options(runtime_options))).invoke! + end + end + + # Does nothing for all commands except Create. + def class_collisions(*class_names) + end + + # Does nothing for all commands except Create. + def readme(*args) + end + + protected + def current_migration_number + Dir.glob("#{RAILS_ROOT}/#{@migration_directory}/[0-9]*_*.rb").inject(0) do |max, file_path| + n = File.basename(file_path).split('_', 2).first.to_i + if n > max then n else max end + end + end + + def next_migration_number + current_migration_number + 1 + end + + def migration_directory(relative_path) + directory(@migration_directory = relative_path) + end + + def existing_migrations(file_name) + Dir.glob("#{@migration_directory}/[0-9]*_*.rb").grep(/[0-9]+_#{file_name}.rb$/) + end + + def migration_exists?(file_name) + not existing_migrations(file_name).empty? + end + + def next_migration_string(padding = 3) + if ActiveRecord::Base.timestamped_migrations + Time.now.utc.strftime("%Y%m%d%H%M%S") + else + "%.#{padding}d" % next_migration_number + end + end + + def gsub_file(relative_destination, regexp, *args, &block) + path = destination_path(relative_destination) + content = File.read(path).gsub(regexp, *args, &block) + File.open(path, 'wb') { |file| file.write(content) } + end + + private + # Ask the user interactively whether to force collision. + def force_file_collision?(destination, src, dst, file_options = {}, &block) + $stdout.print "overwrite #{destination}? (enter \"h\" for help) [Ynaqdh] " + case $stdin.gets.chomp + when /\Ad\z/i + Tempfile.open(File.basename(destination), File.dirname(dst)) do |temp| + temp.write render_file(src, file_options, &block) + temp.rewind + $stdout.puts `#{diff_cmd} "#{dst}" "#{temp.path}"` + end + puts "retrying" + raise 'retry diff' + when /\Aa\z/i + $stdout.puts "forcing #{spec.name}" + options[:collision] = :force + when /\Aq\z/i + $stdout.puts "aborting #{spec.name}" + raise SystemExit + when /\An\z/i then :skip + when /\Ay\z/i then :force + else + $stdout.puts <<-HELP +Y - yes, overwrite +n - no, do not overwrite +a - all, overwrite this and all others +q - quit, abort +d - diff, show the differences between the old and the new +h - help, show this help +HELP + raise 'retry' + end + rescue + retry + end + + def diff_cmd + ENV['RAILS_DIFF'] || 'diff -u' + end + + def render_template_part(template_options) + # Getting Sandbox to evaluate part template in it + part_binding = template_options[:sandbox].call.sandbox_binding + part_rel_path = template_options[:insert] + part_path = source_path(part_rel_path) + + # Render inner template within Sandbox binding + rendered_part = ERB.new(File.readlines(part_path).join, nil, '-').result(part_binding) + begin_mark = template_part_mark(template_options[:begin_mark], template_options[:mark_id]) + end_mark = template_part_mark(template_options[:end_mark], template_options[:mark_id]) + begin_mark + rendered_part + end_mark + end + + def template_part_mark(name, id) + "\n" + end + end + + # Base class for commands which handle generator actions in reverse, such as Destroy. + class RewindBase < Base + # Rewind action manifest. + def invoke! + manifest.rewind(self) + end + end + + + # Create is the premier generator command. It copies files, creates + # directories, renders templates, and more. + class Create < Base + + # Check whether the given class names are already taken by + # Ruby or Rails. In the future, expand to check other namespaces + # such as the rest of the user's app. + def class_collisions(*class_names) + path = class_names.shift + class_names.flatten.each do |class_name| + # Convert to string to allow symbol arguments. + class_name = class_name.to_s + + # Skip empty strings. + next if class_name.strip.empty? + + # Split the class from its module nesting. + nesting = class_name.split('::') + name = nesting.pop + + # Extract the last Module in the nesting. + last = nesting.inject(Object) { |last, nest| + break unless last.const_defined?(nest) + last.const_get(nest) + } + + # If the last Module exists, check whether the given + # class exists and raise a collision if so. + if last and last.const_defined?(name.camelize) + raise_class_collision(class_name) + end + end + end + + # Copy a file from source to destination with collision checking. + # + # The file_options hash accepts :chmod and :shebang and :collision options. + # :chmod sets the permissions of the destination file: + # file 'config/empty.log', 'log/test.log', :chmod => 0664 + # :shebang sets the #!/usr/bin/ruby line for scripts + # file 'bin/generate.rb', 'script/generate', :chmod => 0755, :shebang => '/usr/bin/env ruby' + # :collision sets the collision option only for the destination file: + # file 'settings/server.yml', 'config/server.yml', :collision => :skip + # + # Collisions are handled by checking whether the destination file + # exists and either skipping the file, forcing overwrite, or asking + # the user what to do. + def file(relative_source, relative_destination, file_options = {}, &block) + # Determine full paths for source and destination files. + source = source_path(relative_source) + destination = destination_path(relative_destination) + destination_exists = File.exist?(destination) + + # If source and destination are identical then we're done. + if destination_exists and identical?(source, destination, &block) + return logger.identical(relative_destination) + end + + # Check for and resolve file collisions. + if destination_exists + + # Make a choice whether to overwrite the file. :force and + # :skip already have their mind made up, but give :ask a shot. + choice = case (file_options[:collision] || options[:collision]).to_sym #|| :ask + when :ask then force_file_collision?(relative_destination, source, destination, file_options, &block) + when :force then :force + when :skip then :skip + else raise "Invalid collision option: #{options[:collision].inspect}" + end + + # Take action based on our choice. Bail out if we chose to + # skip the file; otherwise, log our transgression and continue. + case choice + when :force then logger.force(relative_destination) + when :skip then return(logger.skip(relative_destination)) + else raise "Invalid collision choice: #{choice}.inspect" + end + + # File doesn't exist so log its unbesmirched creation. + else + logger.create relative_destination + end + + # If we're pretending, back off now. + return if options[:pretend] + + # Write destination file with optional shebang. Yield for content + # if block given so templaters may render the source file. If a + # shebang is requested, replace the existing shebang or insert a + # new one. + File.open(destination, 'wb') do |dest| + dest.write render_file(source, file_options, &block) + end + + # Optionally change permissions. + if file_options[:chmod] + FileUtils.chmod(file_options[:chmod], destination) + end + + # Optionally add file to subversion or git + system("svn add #{destination}") if options[:svn] + system("git add -v #{relative_destination}") if options[:git] + end + + # Checks if the source and the destination file are identical. If + # passed a block then the source file is a template that needs to first + # be evaluated before being compared to the destination. + def identical?(source, destination, &block) + return false if File.directory? destination + source = block_given? ? File.open(source) {|sf| yield(sf)} : IO.read(source) + destination = IO.read(destination) + source == destination + end + + # Generate a file for a Rails application using an ERuby template. + # Looks up and evaluates a template by name and writes the result. + # + # The ERB template uses explicit trim mode to best control the + # proliferation of whitespace in generated code. <%- trims leading + # whitespace; -%> trims trailing whitespace including one newline. + # + # A hash of template options may be passed as the last argument. + # The options accepted by the file are accepted as well as :assigns, + # a hash of variable bindings. Example: + # template 'foo', 'bar', :assigns => { :action => 'view' } + # + # Template is implemented in terms of file. It calls file with a + # block which takes a file handle and returns its rendered contents. + def template(relative_source, relative_destination, template_options = {}) + file(relative_source, relative_destination, template_options) do |file| + # Evaluate any assignments in a temporary, throwaway binding. + vars = template_options[:assigns] || {} + b = binding + vars.each { |k,v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b } + + # Render the source file with the temporary binding. + ERB.new(file.read, nil, '-').result(b) + end + end + + def complex_template(relative_source, relative_destination, template_options = {}) + options = template_options.dup + options[:assigns] ||= {} + options[:assigns]['template_for_inclusion'] = render_template_part(template_options) + template(relative_source, relative_destination, options) + end + + # Create a directory including any missing parent directories. + # Always skips directories which exist. + def directory(relative_path) + path = destination_path(relative_path) + if File.exist?(path) + logger.exists relative_path + else + logger.create relative_path + unless options[:pretend] + FileUtils.mkdir_p(path) + # git doesn't require adding the paths, adding the files later will + # automatically do a path add. + + # Subversion doesn't do path adds, so we need to add + # each directory individually. + # So stack up the directory tree and add the paths to + # subversion in order without recursion. + if options[:svn] + stack = [relative_path] + until File.dirname(stack.last) == stack.last # dirname('.') == '.' + stack.push File.dirname(stack.last) + end + stack.reverse_each do |rel_path| + svn_path = destination_path(rel_path) + system("svn add -N #{svn_path}") unless File.directory?(File.join(svn_path, '.svn')) + end + end + end + end + end + + # Display a README. + def readme(*relative_sources) + relative_sources.flatten.each do |relative_source| + logger.readme relative_source + puts File.read(source_path(relative_source)) unless options[:pretend] + end + end + + # When creating a migration, it knows to find the first available file in db/migrate and use the migration.rb template. + def migration_template(relative_source, relative_destination, template_options = {}) + migration_directory relative_destination + migration_file_name = template_options[:migration_file_name] || file_name + raise "Another migration is already named #{migration_file_name}: #{existing_migrations(migration_file_name).first}" if migration_exists?(migration_file_name) + template(relative_source, "#{relative_destination}/#{next_migration_string}_#{migration_file_name}.rb", template_options) + end + + def route_resources(*resources) + resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') + sentinel = 'ActionController::Routing::Routes.draw do |map|' + + logger.route "map.resources #{resource_list}" + unless options[:pretend] + gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match| + "#{match}\n map.resources #{resource_list}\n" + end + end + end + + private + def render_file(path, options = {}) + File.open(path, 'rb') do |file| + if block_given? + yield file + else + content = '' + if shebang = options[:shebang] + content << "#!#{shebang}\n" + if line = file.gets + content << "line\n" if line !~ /^#!/ + end + end + content << file.read + end + end + end + + # Raise a usage error with an informative WordNet suggestion. + # Thanks to Florian Gross (flgr). + def raise_class_collision(class_name) + message = <([\w ]*?)<\/a>/s).uniq + end + end + rescue Exception + return nil + end + end + + + # Undo the actions performed by a generator. Rewind the action + # manifest and attempt to completely erase the results of each action. + class Destroy < RewindBase + # Remove a file if it exists and is a file. + def file(relative_source, relative_destination, file_options = {}) + destination = destination_path(relative_destination) + if File.exist?(destination) + logger.rm relative_destination + unless options[:pretend] + if options[:svn] + # If the file has been marked to be added + # but has not yet been checked in, revert and delete + if options[:svn][relative_destination] + system("svn revert #{destination}") + FileUtils.rm(destination) + else + # If the directory is not in the status list, it + # has no modifications so we can simply remove it + system("svn rm #{destination}") + end + elsif options[:git] + if options[:git][:new][relative_destination] + # file has been added, but not committed + system("git reset HEAD #{relative_destination}") + FileUtils.rm(destination) + elsif options[:git][:modified][relative_destination] + # file is committed and modified + system("git rm -f #{relative_destination}") + else + # If the directory is not in the status list, it + # has no modifications so we can simply remove it + system("git rm #{relative_destination}") + end + else + FileUtils.rm(destination) + end + end + else + logger.missing relative_destination + return + end + end + + # Templates are deleted just like files and the actions take the + # same parameters, so simply alias the file method. + alias_method :template, :file + + # Remove each directory in the given path from right to left. + # Remove each subdirectory if it exists and is a directory. + def directory(relative_path) + parts = relative_path.split('/') + until parts.empty? + partial = File.join(parts) + path = destination_path(partial) + if File.exist?(path) + if Dir[File.join(path, '*')].empty? + logger.rmdir partial + unless options[:pretend] + if options[:svn] + # If the directory has been marked to be added + # but has not yet been checked in, revert and delete + if options[:svn][relative_path] + system("svn revert #{path}") + FileUtils.rmdir(path) + else + # If the directory is not in the status list, it + # has no modifications so we can simply remove it + system("svn rm #{path}") + end + # I don't think git needs to remove directories?.. + # or maybe they have special consideration... + else + FileUtils.rmdir(path) + end + end + else + logger.notempty partial + end + else + logger.missing partial + end + parts.pop + end + end + + def complex_template(*args) + # nothing should be done here + end + + # When deleting a migration, it knows to delete every file named "[0-9]*_#{file_name}". + def migration_template(relative_source, relative_destination, template_options = {}) + migration_directory relative_destination + + migration_file_name = template_options[:migration_file_name] || file_name + unless migration_exists?(migration_file_name) + puts "There is no migration named #{migration_file_name}" + return + end + + + existing_migrations(migration_file_name).each do |file_path| + file(relative_source, file_path, template_options) + end + end + + def route_resources(*resources) + resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') + look_for = "\n map.resources #{resource_list}\n" + logger.route "map.resources #{resource_list}" + gsub_file 'config/routes.rb', /(#{look_for})/mi, '' + end + end + + + # List a generator's action manifest. + class List < Base + def dependency(generator_name, args, options = {}) + logger.dependency "#{generator_name}(#{args.join(', ')}, #{options.inspect})" + end + + def class_collisions(*class_names) + logger.class_collisions class_names.join(', ') + end + + def file(relative_source, relative_destination, options = {}) + logger.file relative_destination + end + + def template(relative_source, relative_destination, options = {}) + logger.template relative_destination + end + + def complex_template(relative_source, relative_destination, options = {}) + logger.template "#{options[:insert]} inside #{relative_destination}" + end + + def directory(relative_path) + logger.directory "#{destination_path(relative_path)}/" + end + + def readme(*args) + logger.readme args.join(', ') + end + + def migration_template(relative_source, relative_destination, options = {}) + migration_directory relative_destination + logger.migration_template file_name + end + + def route_resources(*resources) + resource_list = resources.map { |r| r.to_sym.inspect }.join(', ') + logger.route "map.resources #{resource_list}" + end + end + + # Update generator's action manifest. + class Update < Create + def file(relative_source, relative_destination, options = {}) + # logger.file relative_destination + end + + def template(relative_source, relative_destination, options = {}) + # logger.template relative_destination + end + + def complex_template(relative_source, relative_destination, template_options = {}) + + begin + dest_file = destination_path(relative_destination) + source_to_update = File.readlines(dest_file).join + rescue Errno::ENOENT + logger.missing relative_destination + return + end + + logger.refreshing "#{template_options[:insert].gsub(/\.erb/,'')} inside #{relative_destination}" + + begin_mark = Regexp.quote(template_part_mark(template_options[:begin_mark], template_options[:mark_id])) + end_mark = Regexp.quote(template_part_mark(template_options[:end_mark], template_options[:mark_id])) + + # Refreshing inner part of the template with freshly rendered part. + rendered_part = render_template_part(template_options) + source_to_update.gsub!(/#{begin_mark}.*?#{end_mark}/m, rendered_part) + + File.open(dest_file, 'w') { |file| file.write(source_to_update) } + end + + def directory(relative_path) + # logger.directory "#{destination_path(relative_path)}/" + end + end + + end + end +end