aboutsummaryrefslogblamecommitdiffstats
path: root/railties/lib/rails_generator/commands.rb
blob: 299044c3d750e8dc56f7d6338968302b43676f9e (plain) (tree)
1
2
3
4


                   
                  














                                                                        
                             





















                                                                          
                        















                                                                                                 
                 










                                                                                                       



                                                           
                                            
                                                                                           





                                                     
                                                




                                                        

             





                                                                    

                                                                  
                                                                                     


                                                                                      
                                                                                      

                                                                   
                                                                    


                                  
                           

                                                   
                           

                                                    











                                                          




                 



                                          










                                                                                                      
             
 

                                          



















                                                                                           
                                  




                                                          
                                           





                                                      


                                                        

             




                                                                




                                                                         
                                                                                   



                                                                                                      
                                                                             
                                                                                



                                                                           
                                                                                  
                                                                  

                                                                      
                                                        


                                                                           
                                                         
             

                                                  
                               


                                                                          
                                                                                           
                                                                                                                     
























                                                                                   

                                                                





                                                              
 
                                                    
                                                           
                                                                       

           



                                                                               
                                                     

                                                                                           
                               

           
                                                                          
                                                                          















                                                                                  
                                                     














                                                                                              
                                               

                                                
                              


                                       


















                                                                                                      


               








                                                                                 

                                                                                                                               
                                                  


                                                                                                                                                                        

           




                                                                           



                                                                                     


             
               
















                                                        



                                                                       
                                                                                                   


                                                            



                                                   



                                     
                                                                             






                                                                         

                                                                                                      











                                                                            
                                                                          
                                                              
                                     
                                          


                                                         
                                                                    






                                                                 













                                                                    



                                         
















                                                                        
                                

                                                 


                                                                  
                                                                        







                                                                     

                                                                    



                                         












                                       


                                                                                               
                                                  

                                                                                   




                                                                     

                                                                      


                                                              






                                                                           































                                                                                       
 
                                                                                   
                                                  

                                             




                                                                           





















                                                                                          
                                                                                                           


















                                                                                                                   
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)
          after_generate
        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)
            "<!--[#{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 = template_options[:binding] || 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 = <<end_message
  The name '#{class_name}' is either already used in your application or reserved by Ruby on Rails.
  Please choose an alternative and run this generator again.
end_message
            if suggest = find_synonyms(class_name)
              if suggest.any?
                message << "\n  Suggestions:  \n\n"
                message << suggest.join("\n")
              end
            end
            raise UsageError, message
          end

          SYNONYM_LOOKUP_URI = "http://wordnet.princeton.edu/perl/webwn?s=%s"

          # Look up synonyms on WordNet.  Thanks to Florian Gross (flgr).
          def find_synonyms(word)
            require 'open-uri'
            require 'timeout'
            timeout(5) do
              open(SYNONYM_LOOKUP_URI % word) do |stream|
                # Grab words linked to dictionary entries as possible synonyms
                data = stream.read.gsub("&nbsp;", " ").scan(/<a href="webwn.*?">([\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