diff options
Diffstat (limited to 'railties/lib/rails/commands')
24 files changed, 643 insertions, 105 deletions
diff --git a/railties/lib/rails/commands/application/application_command.rb b/railties/lib/rails/commands/application/application_command.rb index 7675d3b3d1..f77553b830 100644 --- a/railties/lib/rails/commands/application/application_command.rb +++ b/railties/lib/rails/commands/application/application_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/generators" require "rails/generators/rails/app/app_generator" diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index 62e3aa19df..e35faa5b01 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "irb" require "irb/completion" @@ -70,8 +72,19 @@ module Rails class_option :sandbox, aliases: "-s", type: :boolean, default: false, desc: "Rollback database modifications on exit." - class_option :environment, aliases: "-e", type: :string, - desc: "Specifies the environment to run this console under (test/development/production)." + def initialize(args = [], local_options = {}, config = {}) + console_options = [] + + # For the same behavior as OptionParser, leave only options after "--" in ARGV. + termination = local_options.find_index("--") + if termination + console_options = local_options[termination + 1..-1] + local_options = local_options[0...termination] + end + + ARGV.replace(console_options) + super(args, local_options, config) + end def perform extract_environment_option_from_argument @@ -79,8 +92,6 @@ module Rails # RAILS_ENV needs to be set before config/application is required. ENV["RAILS_ENV"] = options[:environment] - ARGV.clear # Clear ARGV so IRB doesn't freak. - require_application_and_environment! Rails::Console.start(Rails.application, options) end diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE new file mode 100644 index 0000000000..ea429f58d8 --- /dev/null +++ b/railties/lib/rails/commands/credentials/USAGE @@ -0,0 +1,40 @@ +=== Storing Encrypted Credentials in Source Control + +The Rails `credentials` commands provide access to encrypted credentials, +so you can safely store access tokens, database passwords, and the like +safely inside the app without relying on a mess of ENVs. + +This also allows for atomic deploys: no need to coordinate key changes +to get everything working as the keys are shipped with the code. + +=== Setup + +Applications after Rails 5.2 automatically have a basic credentials file generated +that just contains the secret_key_base used by MessageVerifiers/MessageEncryptors, like the ones +signing and encrypting cookies. + +For applications created prior to Rails 5.2, we'll automatically generate a new +credentials file in `config/credentials.yml.enc` the first time you run `rails credentials:edit`. +If you didn't have a master key saved in `config/master.key`, that'll be created too. + +Don't lose this master key! Put it in a password manager your team can access. +Should you lose it no one, including you, will be able to access any encrypted +credentials. + +Don't commit the key! Add `config/master.key` to your source control's +ignore file. If you use Git, Rails handles this for you. + +Rails also looks for the master key in `ENV["RAILS_MASTER_KEY"]`, if that's easier to manage. + +You could prepend that to your server's start command like this: + + RAILS_MASTER_KEY="very-secret-and-secure" server.start + +=== Editing Credentials + +This will open a temporary file in `$EDITOR` with the decrypted contents to edit +the encrypted credentials. + +When the temporary file is next saved the contents are encrypted and written to +`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials +from leaking. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb new file mode 100644 index 0000000000..65c5218fc8 --- /dev/null +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class CredentialsCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end + end + + def edit + require_application_and_environment! + + ensure_editor_available(command: "bin/rails credentials:edit") || (return) + ensure_master_key_has_been_added if Rails.application.credentials.key.nil? + ensure_credentials_have_been_added + + catch_editing_exceptions do + change_credentials_in_system_editor + end + + say "New credentials encrypted and saved." + end + + def show + require_application_and_environment! + + say Rails.application.credentials.read.presence || missing_credentials_message + end + + private + def ensure_master_key_has_been_added + master_key_generator.add_master_key_file + master_key_generator.ignore_master_key_file + end + + def ensure_credentials_have_been_added + credentials_generator.add_credentials_file_silently + end + + def change_credentials_in_system_editor + Rails.application.credentials.change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + + def master_key_generator + require "rails/generators" + require "rails/generators/rails/master_key/master_key_generator" + + Rails::Generators::MasterKeyGenerator.new + end + + def credentials_generator + require "rails/generators" + require "rails/generators/rails/credentials/credentials_generator" + + Rails::Generators::CredentialsGenerator.new + end + + def missing_credentials_message + if Rails.application.credentials.key.nil? + "Missing master key to decrypt credentials. See `rails credentials:help`" + else + "No credentials have been added yet. Use `rails credentials:edit` to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index 588fb06b15..806b7de6d6 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "rails/command/environment_argument" module Rails @@ -11,7 +13,7 @@ module Rails end def start - ENV["RAILS_ENV"] = @options[:environment] || environment + ENV["RAILS_ENV"] ||= @options[:environment] || environment case config["adapter"] when /^(jdbc)?mysql/ @@ -58,7 +60,7 @@ module Rails logon = "" if config["username"] - logon = config["username"] + logon = config["username"].dup logon << "/#{config['password']}" if config["password"] && @options["include_password"] logon << "@#{config['database']}" if config["database"] end @@ -73,7 +75,7 @@ module Rails args += ["-P", "#{config['password']}"] if config["password"] if config["host"] - host_arg = "#{config['host']}" + host_arg = "#{config['host']}".dup host_arg << ":#{config['port']}" if config["port"] args += ["-S", host_arg] end @@ -87,10 +89,15 @@ module Rails def config @config ||= begin - if configurations[environment].blank? + # We need to check whether the user passed the connection the + # first time around to show a consistent error message to people + # relying on 2-level database configuration. + if @options["connection"] && configurations[connection].blank? + raise ActiveRecord::AdapterNotSpecified, "'#{connection}' connection is not configured. Available configuration: #{configurations.inspect}" + elsif configurations[environment].blank? && configurations[connection].blank? raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" else - configurations[environment] + configurations[connection] || configurations[environment].presence end end end @@ -99,6 +106,10 @@ module Rails Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment end + def connection + @options.fetch(:connection, "primary") + end + private def configurations # :doc: require APP_PATH @@ -140,14 +151,18 @@ module Rails class_option :mode, enum: %w( html list line column ), type: :string, desc: "Automatically put the sqlite3 database in the specified mode (html, list, line, column)." - class_option :header, type: :string + class_option :header, type: :boolean - class_option :environment, aliases: "-e", type: :string, - desc: "Specifies the environment to run this console under (test/development/production)." + class_option :connection, aliases: "-c", type: :string, + desc: "Specifies the connection to use." def perform extract_environment_option_from_argument + # RAILS_ENV needs to be set before config/application is required. + ENV["RAILS_ENV"] = options[:environment] + + require_application_and_environment! Rails::DBConsole.start(options) end end diff --git a/railties/lib/rails/commands/destroy/destroy_command.rb b/railties/lib/rails/commands/destroy/destroy_command.rb index 5b552b2070..dd432d28fd 100644 --- a/railties/lib/rails/commands/destroy/destroy_command.rb +++ b/railties/lib/rails/commands/destroy/destroy_command.rb @@ -1,10 +1,17 @@ +# frozen_string_literal: true + require "rails/generators" module Rails module Command class DestroyCommand < Base # :nodoc: - def help - Rails::Generators.help self.class.command_name + no_commands do + def help + require_application_and_environment! + load_generators + + Rails::Generators.help self.class.command_name + end end def perform(*) @@ -12,9 +19,9 @@ module Rails return help unless generator require_application_and_environment! - Rails.application.load_generators + load_generators - Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails.root + Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails::Command.root end end end diff --git a/railties/lib/rails/commands/dev/dev_command.rb b/railties/lib/rails/commands/dev/dev_command.rb new file mode 100644 index 0000000000..a3f02f3172 --- /dev/null +++ b/railties/lib/rails/commands/dev/dev_command.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "rails/dev_caching" + +module Rails + module Command + class DevCommand < Base # :nodoc: + def help + say "rails dev:cache # Toggle development mode caching on/off." + end + + def cache + Rails::DevCaching.enable_by_file + end + end + end +end diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb new file mode 100644 index 0000000000..8d5947652a --- /dev/null +++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "pathname" +require "active_support" +require "rails/command/helpers/editor" + +module Rails + module Command + class EncryptedCommand < Rails::Command::Base # :nodoc: + include Helpers::Editor + + class_option :key, aliases: "-k", type: :string, + default: "config/master.key", desc: "The Rails.root relative path to the encryption key" + + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + end + end + + def edit(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + ensure_editor_available(command: "bin/rails encrypted:edit") || (return) + ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil? + ensure_encrypted_file_has_been_added(file_path, options[:key]) + + catch_editing_exceptions do + change_encrypted_file_in_system_editor(file_path, options[:key]) + end + + say "File encrypted and saved." + rescue ActiveSupport::MessageEncryptor::InvalidMessage + say "Couldn't decrypt #{file_path}. Perhaps you passed the wrong key?" + end + + def show(file_path) + require_application_and_environment! + encrypted = Rails.application.encrypted(file_path, key_path: options[:key]) + + say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: options[:key], file_path: file_path) + end + + private + def ensure_encryption_key_has_been_added(key_path) + encryption_key_file_generator.add_key_file(key_path) + encryption_key_file_generator.ignore_key_file(key_path) + end + + def ensure_encrypted_file_has_been_added(file_path, key_path) + encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + end + + def change_encrypted_file_in_system_editor(file_path, key_path) + Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| + system("#{ENV["EDITOR"]} #{tmp_path}") + end + end + + + def encryption_key_file_generator + require "rails/generators" + require "rails/generators/rails/encryption_key_file/encryption_key_file_generator" + + Rails::Generators::EncryptionKeyFileGenerator.new + end + + def encrypted_file_generator + require "rails/generators" + require "rails/generators/rails/encrypted_file/encrypted_file_generator" + + Rails::Generators::EncryptedFileGenerator.new + end + + def missing_encrypted_message(key:, key_path:, file_path:) + if key.nil? + "Missing '#{key_path}' to decrypt data. See `rails encrypted:help`" + else + "File '#{file_path}' does not exist. Use `rails encrypted:edit #{file_path}` to change that." + end + end + end + end +end diff --git a/railties/lib/rails/commands/generate/generate_command.rb b/railties/lib/rails/commands/generate/generate_command.rb index 2718b453a8..93d7a0ce3a 100644 --- a/railties/lib/rails/commands/generate/generate_command.rb +++ b/railties/lib/rails/commands/generate/generate_command.rb @@ -1,13 +1,17 @@ +# frozen_string_literal: true + require "rails/generators" module Rails module Command class GenerateCommand < Base # :nodoc: - def help - require_application_and_environment! - load_generators + no_commands do + def help + require_application_and_environment! + load_generators - Rails::Generators.help self.class.command_name + Rails::Generators.help self.class.command_name + end end def perform(*) diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE index c5f8ab72bb..8eb98319d2 100644 --- a/railties/lib/rails/commands/help/USAGE +++ b/railties/lib/rails/commands/help/USAGE @@ -1,13 +1,14 @@ The most common rails commands are: - generate Generate new code (short-cut alias: "g") - console Start the Rails console (short-cut alias: "c") - server Start the Rails server (short-cut alias: "s") - test Run tests (short-cut alias: "t") - dbconsole Start a console for the database specified in config/database.yml - (short-cut alias: "db") + generate Generate new code (short-cut alias: "g") + console Start the Rails console (short-cut alias: "c") + server Start the Rails server (short-cut alias: "s") + test Run tests except system tests (short-cut alias: "t") + test:system Run system tests + dbconsole Start a console for the database specified in config/database.yml + (short-cut alias: "db") <% unless engine? %> - new Create a new Rails application. "rails new my_app" creates a - new application called MyApp in "./my_app" + new Create a new Rails application. "rails new my_app" creates a + new application called MyApp in "./my_app" <% end %> All commands can be run with -h (or --help) for more information. diff --git a/railties/lib/rails/commands/help/help_command.rb b/railties/lib/rails/commands/help/help_command.rb index 90d37217fc..9df34e9b79 100644 --- a/railties/lib/rails/commands/help/help_command.rb +++ b/railties/lib/rails/commands/help/help_command.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module Rails module Command class HelpCommand < Base # :nodoc: hide_command! def help(*) - puts self.class.desc + say self.class.desc Rails::Command.print_commands end diff --git a/railties/lib/rails/commands/initializers/initializers_command.rb b/railties/lib/rails/commands/initializers/initializers_command.rb new file mode 100644 index 0000000000..33596177af --- /dev/null +++ b/railties/lib/rails/commands/initializers/initializers_command.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Rails + module Command + class InitializersCommand < Base # :nodoc: + desc "initializers", "Print out all defined initializers in the order they are invoked by Rails." + def perform + require_application_and_environment! + + Rails.application.initializers.tsort_each do |initializer| + say "#{initializer.context_class}.#{initializer.name}" + end + end + end + end +end diff --git a/railties/lib/rails/commands/new/new_command.rb b/railties/lib/rails/commands/new/new_command.rb index 74d1fa5021..a4f2081510 100644 --- a/railties/lib/rails/commands/new/new_command.rb +++ b/railties/lib/rails/commands/new/new_command.rb @@ -1,13 +1,17 @@ +# frozen_string_literal: true + module Rails module Command class NewCommand < Base # :nodoc: - def help - Rails::Command.invoke :application, [ "--help" ] + no_commands do + def help + Rails::Command.invoke :application, [ "--help" ] + end end def perform(*) - puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" - puts "Type 'rails' for help." + say "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n" + say "Type 'rails' for help." exit 1 end end diff --git a/railties/lib/rails/commands/notes/notes_command.rb b/railties/lib/rails/commands/notes/notes_command.rb new file mode 100644 index 0000000000..64b339b3cd --- /dev/null +++ b/railties/lib/rails/commands/notes/notes_command.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "rails/source_annotation_extractor" + +module Rails + module Command + class NotesCommand < Base # :nodoc: + class_option :annotations, aliases: "-a", desc: "Filter by specific annotations, e.g. Foobar TODO", type: :array, default: %w(OPTIMIZE FIXME TODO) + + def perform(*) + require_application_and_environment! + + deprecation_warning + display_annotations + end + + private + def display_annotations + annotations = options[:annotations] + tag = (annotations.length > 1) + + Rails::SourceAnnotationExtractor.enumerate annotations.join("|"), tag: tag, dirs: directories + end + + def directories + Rails::SourceAnnotationExtractor::Annotation.directories + source_annotation_directories + end + + def deprecation_warning + return if source_annotation_directories.empty? + ActiveSupport::Deprecation.warn("`SOURCE_ANNOTATION_DIRECTORIES` is deprecated and will be removed in Rails 6.1. You can add default directories by using config.annotations.register_directories instead.") + end + + def source_annotation_directories + ENV["SOURCE_ANNOTATION_DIRECTORIES"].to_s.split(",") + end + end + end +end diff --git a/railties/lib/rails/commands/plugin/plugin_command.rb b/railties/lib/rails/commands/plugin/plugin_command.rb index b40ab006af..96187aa952 100644 --- a/railties/lib/rails/commands/plugin/plugin_command.rb +++ b/railties/lib/rails/commands/plugin/plugin_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Command class PluginCommand < Base # :nodoc: @@ -24,7 +26,7 @@ module Rails if File.exist?(railsrc) extra_args = File.read(railsrc).split(/\n+/).flat_map(&:split) - puts "Using #{extra_args.join(" ")} from #{railsrc}" + say "Using #{extra_args.join(" ")} from #{railsrc}" plugin_args.insert(1, *extra_args) end end diff --git a/railties/lib/rails/commands/rake/rake_command.rb b/railties/lib/rails/commands/rake/rake_command.rb index 075b1fd23d..535df0c430 100644 --- a/railties/lib/rails/commands/rake/rake_command.rb +++ b/railties/lib/rails/commands/rake/rake_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Command class RakeCommand < Base # :nodoc: diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb new file mode 100644 index 0000000000..b592a5212f --- /dev/null +++ b/railties/lib/rails/commands/routes/routes_command.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/command" + +module Rails + module Command + class RoutesCommand < Base # :nodoc: + class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController." + class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern." + class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained." + + def perform(*) + require_application_and_environment! + require "action_dispatch/routing/inspector" + + say inspector.format(formatter, routes_filter) + end + + private + def inspector + ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes) + end + + def formatter + if options.key?("expanded") + ActionDispatch::Routing::ConsoleFormatter::Expanded.new + else + ActionDispatch::Routing::ConsoleFormatter::Sheet.new + end + end + + def routes_filter + options.symbolize_keys.slice(:controller, :grep) + end + end + end +end diff --git a/railties/lib/rails/commands/runner/USAGE b/railties/lib/rails/commands/runner/USAGE index b2a6e8493d..24b60037f0 100644 --- a/railties/lib/rails/commands/runner/USAGE +++ b/railties/lib/rails/commands/runner/USAGE @@ -8,6 +8,9 @@ Run the Ruby file located at `path/to/filename.rb` after loading the app: <%= executable %> path/to/filename.rb +Run the Ruby script read from stdin after loading the app: + <%= executable %> - + <% unless Gem.win_platform? %> You can also use the runner command as a shebang line for your executables: diff --git a/railties/lib/rails/commands/runner/runner_command.rb b/railties/lib/rails/commands/runner/runner_command.rb index 4989a7837d..cb693bcf34 100644 --- a/railties/lib/rails/commands/runner/runner_command.rb +++ b/railties/lib/rails/commands/runner/runner_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Command class RunnerCommand < Base # :nodoc: @@ -5,16 +7,18 @@ module Rails default: Rails::Command.environment.dup, desc: "The environment for the runner to operate under (test/development/production)" - def help - super - puts self.class.desc + no_commands do + def help + super + say self.class.desc + end end def self.banner(*) - "#{super} [<'Some.ruby(code)'> | <filename.rb>]" + "#{super} [<'Some.ruby(code)'> | <filename.rb> | -]" end - def perform(code_or_file = nil, *file_argv) + def perform(code_or_file = nil, *command_argv) unless code_or_file help exit 1 @@ -25,18 +29,21 @@ module Rails require_application_and_environment! Rails.application.load_runner - if File.exist?(code_or_file) + ARGV.replace(command_argv) + + if code_or_file == "-" + eval($stdin.read, TOPLEVEL_BINDING, "stdin") + elsif File.exist?(code_or_file) $0 = code_or_file - ARGV.replace(file_argv) Kernel.load code_or_file else begin - eval(code_or_file, binding, __FILE__, __LINE__) - rescue SyntaxError, NameError => error - $stderr.puts "Please specify a valid ruby command or the path of a script to run." - $stderr.puts "Run '#{self.class.executable} -h' for help." - $stderr.puts - $stderr.puts error + eval(code_or_file, TOPLEVEL_BINDING, __FILE__, __LINE__) + rescue SyntaxError, NameError => e + error "Please specify a valid ruby command or the path of a script to run." + error "Run '#{self.class.executable} -h' for help." + error "" + error e exit 1 end end diff --git a/railties/lib/rails/commands/secrets/USAGE b/railties/lib/rails/commands/secrets/USAGE index 4b7deb4e2a..e205cdc001 100644 --- a/railties/lib/rails/commands/secrets/USAGE +++ b/railties/lib/rails/commands/secrets/USAGE @@ -7,7 +7,7 @@ with the code. === Setup -Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key` +Run `rails secrets:setup` to opt in and generate the `config/secrets.yml.key` and `config/secrets.yml.enc` files. The latter contains all the keys to be encrypted while the former holds the @@ -40,9 +40,17 @@ be encrypted. A `shared:` top level key is also supported such that any keys there is merged into the other environments. +Additionally, Rails won't read encrypted secrets out of the box even if you have +the key. Add this: + + config.read_encrypted_secrets = true + +to the environment you'd like to read encrypted secrets. `rails secrets:setup` +inserts this into the production environment by default. + === Editing Secrets -After `bin/rails secrets:setup`, run `bin/rails secrets:edit`. +After `rails secrets:setup`, run `rails secrets:edit`. That command opens a temporary file in `$EDITOR` with the decrypted contents of `config/secrets.yml.enc` to edit the encrypted secrets. diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb index 3ba8c0c85b..2eebc0f35f 100644 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -1,36 +1,65 @@ +# frozen_string_literal: true + require "active_support" require "rails/secrets" module Rails module Command class SecretsCommand < Rails::Command::Base # :nodoc: - def help - say "Usage:\n #{self.class.banner}" - say "" - say self.class.desc + no_commands do + def help + say "Usage:\n #{self.class.banner}" + say "" + say self.class.desc + end end def setup - require "rails/generators" - require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - - Rails::Generators::EncryptedSecretsGenerator.start + deprecate_in_favor_of_credentials_and_exit end def edit + if ENV["EDITOR"].to_s.empty? + say "No $EDITOR to open decrypted secrets in. Assign one like this:" + say "" + say %(EDITOR="mate --wait" rails secrets:edit) + say "" + say "For editors that fork and exit immediately, it's important to pass a wait flag," + say "otherwise the secrets will be saved immediately with no chance to edit." + + return + end + require_application_and_environment! Rails::Secrets.read_for_editing do |tmp_path| - puts "Waiting for secrets file to be saved. Abort with Ctrl-C." - system("\$EDITOR #{tmp_path}") + system("#{ENV["EDITOR"]} #{tmp_path}") end - puts "New secrets encrypted and saved." + say "New secrets encrypted and saved." rescue Interrupt - puts "Aborted changing encrypted secrets: nothing saved." + say "Aborted changing encrypted secrets: nothing saved." rescue Rails::Secrets::MissingKeyError => error say error.message + rescue Errno::ENOENT => error + if /secrets\.yml\.enc/.match?(error.message) + deprecate_in_favor_of_credentials_and_exit + else + raise + end + end + + def show + say Rails::Secrets.read end + + private + def deprecate_in_favor_of_credentials_and_exit + say "Encrypted secrets is deprecated in favor of credentials. Run:" + say "rails credentials:help" + + exit 1 + end end end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 1fa27a3155..9d517f3239 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + require "fileutils" -require "optparse" require "action_dispatch" require "rails" +require "active_support/deprecation" +require "active_support/core_ext/string/filters" require "rails/dev_caching" module Rails @@ -18,10 +21,15 @@ module Rails set_environment end - # TODO: this is no longer required but we keep it for the moment to support older config.ru files. def app @app ||= begin app = super + if app.is_a?(Class) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. + Please change `run #{app}` to `run Rails.application` in config.ru. + MSG + end app.respond_to?(:to_app) ? app.to_app : app end end @@ -34,18 +42,22 @@ module Rails ENV["RAILS_ENV"] ||= options[:environment] end - def start - print_boot_information + def start(after_stop_callback = nil) trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] - super + super() ensure - # The '-h' option calls exit before @options is set. - # If we call 'options' with it unset, we get double help banners. - puts "Exiting" unless @options && options[:daemonize] + after_stop_callback.call if after_stop_callback + end + + def serveable? # :nodoc: + server + true + rescue LoadError, NameError + false end def middleware @@ -56,6 +68,10 @@ module Rails super.merge(@default_options) end + def served_url + "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma? + end + private def setup_dev_caching if options[:environment] == "development" @@ -63,13 +79,6 @@ module Rails end end - def print_boot_information - url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" - puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}" - puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}" - puts "=> Run `rails server -h` for more startup options" - end - def create_tmp_directories %w(cache pids sockets).each do |dir_to_make| FileUtils.mkdir_p(File.join(Rails.root, "tmp", dir_to_make)) @@ -88,17 +97,24 @@ module Rails end end - def restart_command - "bin/rails server #{ARGV.join(' ')}" + def use_puma? + server.to_s == "Rack::Handler::Puma" end end module Command class ServerCommand < Base # :nodoc: + # Hard-coding a bunch of handlers here as we don't have a public way of + # querying them from the Rack::Handler registry. + RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn) + + DEFAULT_PORT = 3000 DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze + argument :using, optional: true + class_option :port, aliases: "-p", type: :numeric, - desc: "Runs Rails on the specified port.", banner: :port, default: 3000 + desc: "Runs Rails on the specified port - defaults to 3000.", banner: :port class_option :binding, aliases: "-b", type: :string, desc: "Binds Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.", banner: :IP @@ -108,26 +124,41 @@ module Rails desc: "Runs server as a Daemon." class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this server under (development/test/production).", banner: :name + class_option :using, aliases: "-u", type: :string, + desc: "Specifies the Rack server used to run the application (thin/puma/webrick).", banner: :name class_option :pid, aliases: "-P", type: :string, default: DEFAULT_PID_PATH, desc: "Specifies the PID file." - class_option "dev-caching", aliases: "-C", type: :boolean, default: nil, + class_option :dev_caching, aliases: "-C", type: :boolean, default: nil, desc: "Specifies whether to perform caching in development." + class_option :restart, type: :boolean, default: nil, hide: true + class_option :early_hints, type: :boolean, default: nil, desc: "Enables HTTP/2 early hints." + class_option :log_to_stdout, type: :boolean, default: nil, optional: true, + desc: "Whether to log to stdout. Enabled by default in development when not daemonized." - def initialize(args = [], local_options = {}, config = {}) - @original_options = local_options + def initialize(args, local_options, *) super - @server = self.args.shift - @log_stdout = options[:daemon].blank? && (options[:environment] || Rails.env) == "development" + + @original_options = local_options - %w( --restart ) + deprecate_positional_rack_server_and_rewrite_to_option(@original_options) end def perform set_application_directory! + prepare_restart + Rails::Server.new(server_options).tap do |server| # Require application after server sets environment to propagate # the --environment option. require APP_PATH Dir.chdir(Rails.application.root) - server.start + + if server.serveable? + print_boot_information(server.server, server.served_url) + after_stop_callback = -> { say "Exiting" unless options[:daemon] } + server.start(after_stop_callback) + else + say rack_server_suggestion(using) + end end end @@ -135,8 +166,8 @@ module Rails def server_options { user_supplied_options: user_supplied_options, - server: @server, - log_stdout: @log_stdout, + server: using, + log_stdout: log_to_stdout?, Port: port, Host: host, DoNotReverseLookup: true, @@ -144,8 +175,9 @@ module Rails environment: environment, daemonize: options[:daemon], pid: pid, - caching: options["dev-caching"], - restart_cmd: restart_command + caching: options[:dev_caching], + restart_cmd: restart_command, + early_hints: early_hints } end end @@ -154,9 +186,16 @@ module Rails def user_supplied_options @user_supplied_options ||= begin # Convert incoming options array to a hash of flags - # ["-p", "3001", "-c", "foo"] # => {"-p" => true, "-c" => true} + # ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true} user_flag = {} - @original_options.each_with_index { |command, i| user_flag[command] = true if i.even? } + @original_options.each do |command| + if command.to_s.start_with?("--") + option = command.split("=")[0] + user_flag[option] = true + elsif command =~ /\A(-.)/ + user_flag[Regexp.last_match[0]] = true + end + end # Collect all options that the user has explicitly defined so we can # differentiate them from defaults @@ -169,7 +208,7 @@ module Rails name = :Port when :binding name = :Host - when :"dev-caching" + when :dev_caching name = :caching when :daemonize name = :daemon @@ -177,21 +216,33 @@ module Rails user_supplied_options << name end end - user_supplied_options << :Host if ENV["Host"] + user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] user_supplied_options << :Port if ENV["PORT"] user_supplied_options.uniq end end def port - ENV.fetch("PORT", options[:port]).to_i + options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i end def host - unless (default_host = options[:binding]) + if options[:binding] + options[:binding] + else default_host = environment == "development" ? "localhost" : "0.0.0.0" + + if ENV["HOST"] && !ENV["BINDING"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment instead. + MSG + + return ENV["HOST"] + end + + ENV.fetch("BINDING", default_host) end - ENV.fetch("HOST", default_host) end def environment @@ -199,7 +250,17 @@ module Rails end def restart_command - "bin/rails server #{@server} #{@original_options.join(" ")}" + "bin/rails server #{@original_options.join(" ")} --restart" + end + + def early_hints + options[:early_hints] + end + + def log_to_stdout? + options.fetch(:log_to_stdout) do + options[:daemon].blank? && environment == "development" + end end def pid @@ -207,7 +268,54 @@ module Rails end def self.banner(*) - "rails server [puma, thin etc] [options]" + "rails server [thin/puma/webrick] [options]" + end + + def prepare_restart + FileUtils.rm_f(options[:pid]) if options[:restart] + end + + def deprecate_positional_rack_server_and_rewrite_to_option(original_options) + if using + ActiveSupport::Deprecation.warn(<<~MSG) + Passing the Rack server name as a regular argument is deprecated + and will be removed in the next Rails version. Please, use the -u + option instead. + MSG + + original_options.concat [ "-u", using ] + else + # Use positional internally to get around Thor's immutable options. + # TODO: Replace `using` occurrences with `options[:using]` after deprecation removal. + @using = options[:using] + end + end + + def rack_server_suggestion(server) + if server.in?(RACK_SERVERS) + <<~MSG + Could not load server "#{server}". Maybe you need to the add it to the Gemfile? + + gem "#{server}" + + Run `rails server --help` for more options. + MSG + else + suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) + + <<~MSG + Could not find server "#{server}". Maybe you meant #{suggestion.inspect}? + Run `rails server --help` for more options. + MSG + end + end + + def print_boot_information(server, url) + say <<~MSG + => Booting #{ActiveSupport::Inflector.demodulize(server)} + => Rails #{Rails.version} application starting in #{Rails.env} #{url} + => Run `rails server --help` for more startup options + MSG end end end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 629fb5b425..00ea9ac4a6 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -1,19 +1,36 @@ +# frozen_string_literal: true + require "rails/command" -require "rails/test_unit/minitest_plugin" +require "rails/test_unit/runner" +require "rails/test_unit/reporter" module Rails module Command class TestCommand < Base # :nodoc: - def help - perform # Hand over help printing to minitest. + no_commands do + def help + say "Usage: #{Rails::TestUnitReporter.executable} [options] [files or directories]" + say "" + say "You can run a single test by appending a line number to a filename:" + say "" + say " #{Rails::TestUnitReporter.executable} test/models/user_test.rb:27" + say "" + say "You can run multiple files and directories at the same time:" + say "" + say " #{Rails::TestUnitReporter.executable} test/controllers test/integration/login_test.rb" + say "" + say "By default test failures and errors are reported inline during a run." + say "" + + Minitest.run(%w(--help)) + end end def perform(*) - $LOAD_PATH << Rails::Command.root.join("test") - - Minitest.run_via = :rails + $LOAD_PATH << Rails::Command.root.join("test").to_s - require "active_support/testing/autorun" + Rails::TestUnit::Runner.parse_options(ARGV) + Rails::TestUnit::Runner.run(ARGV) end end end diff --git a/railties/lib/rails/commands/version/version_command.rb b/railties/lib/rails/commands/version/version_command.rb index ac745594ee..3e2112b6d4 100644 --- a/railties/lib/rails/commands/version/version_command.rb +++ b/railties/lib/rails/commands/version/version_command.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Rails module Command class VersionCommand < Base # :nodoc: |