├── CHANGELOG ├── MIT-LICENSE ├── README ├── Rakefile ├── bin ├── cap └── capify ├── capistrano.gemspec ├── examples └── sample.rb ├── lib ├── capistrano.rb └── capistrano │ ├── callback.rb │ ├── cli.rb │ ├── cli │ ├── execute.rb │ ├── help.rb │ ├── help.txt │ ├── options.rb │ └── ui.rb │ ├── command.rb │ ├── configuration.rb │ ├── configuration │ ├── actions │ │ ├── file_transfer.rb │ │ ├── inspect.rb │ │ └── invocation.rb │ ├── callbacks.rb │ ├── connections.rb │ ├── execution.rb │ ├── loading.rb │ ├── namespaces.rb │ ├── roles.rb │ ├── servers.rb │ └── variables.rb │ ├── errors.rb │ ├── extensions.rb │ ├── gateway.rb │ ├── logger.rb │ ├── recipes │ ├── compat.rb │ ├── deploy.rb │ ├── deploy │ │ ├── dependencies.rb │ │ ├── local_dependency.rb │ │ ├── remote_dependency.rb │ │ ├── scm.rb │ │ ├── scm │ │ │ ├── accurev.rb │ │ │ ├── base.rb │ │ │ ├── bzr.rb │ │ │ ├── cvs.rb │ │ │ ├── darcs.rb │ │ │ ├── git.rb │ │ │ ├── mercurial.rb │ │ │ ├── none.rb │ │ │ ├── perforce.rb │ │ │ └── subversion.rb │ │ ├── strategy.rb │ │ ├── strategy │ │ │ ├── base.rb │ │ │ ├── checkout.rb │ │ │ ├── copy.rb │ │ │ ├── export.rb │ │ │ ├── remote.rb │ │ │ └── remote_cache.rb │ │ └── templates │ │ │ └── maintenance.rhtml │ ├── standard.rb │ ├── templates │ │ └── maintenance.rhtml │ └── upgrade.rb │ ├── role.rb │ ├── server_definition.rb │ ├── shell.rb │ ├── ssh.rb │ ├── task_definition.rb │ ├── upload.rb │ └── version.rb ├── setup.rb └── test ├── cli ├── execute_test.rb ├── help_test.rb ├── options_test.rb └── ui_test.rb ├── cli_test.rb ├── command_test.rb ├── configuration ├── actions │ ├── file_transfer_test.rb │ ├── inspect_test.rb │ └── invocation_test.rb ├── callbacks_test.rb ├── connections_test.rb ├── execution_test.rb ├── loading_test.rb ├── namespace_dsl_test.rb ├── roles_test.rb ├── servers_test.rb └── variables_test.rb ├── configuration_test.rb ├── deploy ├── scm │ ├── accurev_test.rb │ ├── base_test.rb │ ├── git_test.rb │ └── mercurial_test.rb └── strategy │ └── copy_test.rb ├── extensions_test.rb ├── fixtures ├── cli_integration.rb ├── config.rb └── custom.rb ├── gateway_test.rb ├── logger_test.rb ├── server_definition_test.rb ├── shell_test.rb ├── ssh_test.rb ├── task_definition_test.rb ├── upload_test.rb ├── utils.rb └── version_test.rb /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005-2007 Jamis Buck 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Capistrano 2 | 3 | Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH. It uses a simple DSL (borrowed in part from Rake, http://rake.rubyforge.org/) that allows you to define _tasks_, which may be applied to machines in certain roles. It also supports tunneling connections via some gateway machine to allow operations to be performed behind VPN's and firewalls. 4 | 5 | Capistrano was originally designed to simplify and automate deployment of web applications to distributed environments, and originally came bundled with a set of tasks designed for deploying Rails applications. The deployment tasks are now (as of Capistrano 2.0) opt-in and require clients to explicitly put 6 | "load 'deploy'" in their recipes. 7 | 8 | == Dependencies 9 | 10 | * Net::SSH and Net::SFTP (http://net-ssh.rubyforge.org) 11 | * Needle (via Net::SSH) 12 | * HighLine (http://highline.rubyforge.org) 13 | 14 | If you want to run the tests, you'll also need to have the following dependencies installed: 15 | 16 | * Mocha (http://mocha.rubyforge.org) 17 | 18 | == Assumptions 19 | 20 | Capistrano is "opinionated software", which means it has very firm ideas about how things ought to be done, and tries to force those ideas on you. Some of the assumptions behind these opinions are: 21 | 22 | * You are using SSH to access the remote servers. 23 | * You either have the same password to all target machines, or you have public keys in place to allow passwordless access to them. 24 | 25 | Do not expect these assumptions to change. 26 | 27 | == Usage 28 | 29 | In general, you'll use Capistrano as follows: 30 | 31 | * Create a recipe file ("capfile" or "Capfile"). 32 | * Use the +cap+ script to execute your recipe. 33 | 34 | Use the +cap+ script as follows: 35 | 36 | cap sometask 37 | 38 | By default, the script will look for a file called one of +capfile+ or +Capfile+. The +someaction+ text indicates which task to execute. You can do "cap -h" to see all the available options and "cap -T" to see all the available tasks. 39 | 40 | == KNOWN ISSUES 41 | 42 | * Using "put" to upload a file to two or more hosts when a gateway is in effect has a good chance of crashing with a "corrupt mac detected" error. This is due to a bug in Net::SSH. 43 | * Running commands may rarely hang inexplicably. This appears to be specific only to certain platforms. Most people will never see this behavior. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | require 'rake/packagetask' 5 | require 'rake/gempackagetask' 6 | require 'rake/contrib/rubyforgepublisher' 7 | 8 | require "./lib/capistrano/version" 9 | 10 | PKG_NAME = "capistrano" 11 | PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' 12 | PKG_VERSION = Capistrano::Version::STRING + PKG_BUILD 13 | PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}" 14 | 15 | desc "Default task" 16 | task :default => [ :test ] 17 | 18 | desc "Build documentation" 19 | task :doc => [ :rdoc ] 20 | 21 | Rake::TestTask.new do |t| 22 | t.test_files = Dir["test/**/*_test.rb"] 23 | t.verbose = true 24 | end 25 | 26 | desc "Run code-coverage analysis using rcov" 27 | task :coverage do 28 | rm_rf "coverage" 29 | files = Dir["test/**/*_test.rb"] 30 | system "rcov #{files.join(' ')}" 31 | end 32 | 33 | GEM_SPEC = eval(File.read("#{File.dirname(__FILE__)}/#{PKG_NAME}.gemspec")) 34 | 35 | Rake::GemPackageTask.new(GEM_SPEC) do |p| 36 | p.gem_spec = GEM_SPEC 37 | p.need_tar = true 38 | p.need_zip = true 39 | end 40 | 41 | desc "Build the RDoc API documentation" 42 | Rake::RDocTask.new do |rdoc| 43 | rdoc.rdoc_dir = "doc" 44 | rdoc.title = "Capistrano -- A framework for remote command execution" 45 | rdoc.options += %w(--line-numbers --inline-source --main README) 46 | rdoc.rdoc_files.include 'README' 47 | rdoc.rdoc_files.include 'lib/**/*.rb' 48 | rdoc.template = "jamis" 49 | end 50 | 51 | desc "Publish the beta gem" 52 | task :pgem => [:package] do 53 | Rake::SshFilePublisher.new("wrath.rubyonrails.org", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload 54 | `ssh wrath.rubyonrails.org './gemupdate.sh'` 55 | end 56 | 57 | desc "Clean up generated directories and files" 58 | task :clean do 59 | rm_rf "pkg" 60 | rm_rf "doc" 61 | rm_rf "coverage" 62 | end 63 | -------------------------------------------------------------------------------- /bin/cap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'capistrano/cli' 4 | Capistrano::CLI.execute 5 | -------------------------------------------------------------------------------- /bin/capify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'optparse' 4 | 5 | OptionParser.new do |opts| 6 | opts.banner = "Usage: #{File.basename($0)} [path]" 7 | 8 | opts.on("-h", "--help", "Displays this help info") do 9 | puts opts 10 | exit 0 11 | end 12 | 13 | begin 14 | opts.parse!(ARGV) 15 | rescue OptionParser::ParseError => e 16 | warn e.message 17 | puts opts 18 | exit 1 19 | end 20 | end 21 | 22 | if ARGV.empty? 23 | abort "Please specify the directory to capify, e.g. `#{File.basename($0)} .'" 24 | elsif !File.exists?(ARGV.first) 25 | abort "`#{ARGV.first}' does not exist." 26 | elsif !File.directory?(ARGV.first) 27 | abort "`#{ARGV.first}' is not a directory." 28 | elsif ARGV.length > 1 29 | abort "Too many arguments; please specify only the directory to capify." 30 | end 31 | 32 | def unindent(string) 33 | indentation = string[/\A\s*/] 34 | string.strip.gsub(/^#{indentation}/, "") 35 | end 36 | 37 | files = { 38 | "Capfile" => unindent(<<-FILE), 39 | load 'deploy' if respond_to?(:namespace) # cap2 differentiator 40 | Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } 41 | load 'config/deploy' 42 | FILE 43 | 44 | "config/deploy.rb" => unindent(<<-FILE), 45 | set :application, "set your application name here" 46 | set :repository, "set your repository location here" 47 | 48 | # If you aren't deploying to /u/apps/\#{application} on the target 49 | # servers (which is the default), you can specify the actual location 50 | # via the :deploy_to variable: 51 | # set :deploy_to, "/var/www/\#{application}" 52 | 53 | # If you aren't using Subversion to manage your source code, specify 54 | # your SCM below: 55 | # set :scm, :subversion 56 | 57 | role :app, "your app-server here" 58 | role :web, "your web-server here" 59 | role :db, "your db-server here", :primary => true 60 | FILE 61 | } 62 | 63 | base = ARGV.shift 64 | files.each do |file, content| 65 | file = File.join(base, file) 66 | if File.exists?(file) 67 | warn "[skip] `#{file}' already exists" 68 | elsif File.exists?(file.downcase) 69 | warn "[skip] `#{file.downcase}' exists, which could conflict with `#{file}'" 70 | elsif !File.exists?(File.dirname(file)) 71 | warn "[skip] directory `#{File.dirname(file)}' does not exist" 72 | else 73 | puts "[add] writing `#{file}'" 74 | File.open(file, "w") { |f| f.write(content) } 75 | end 76 | end 77 | 78 | puts "[done] capified!" 79 | -------------------------------------------------------------------------------- /capistrano.gemspec: -------------------------------------------------------------------------------- 1 | require './lib/capistrano/version' 2 | 3 | Gem::Specification.new do |s| 4 | 5 | s.name = 'capistrano' 6 | s.version = PKG_VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.summary = <<-DESC.strip.gsub(/\n\s+/, " ") 9 | Capistrano is a utility and framework for executing commands in parallel 10 | on multiple remote machines, via SSH. 11 | DESC 12 | 13 | s.files = Dir.glob("{bin,lib,examples,test}/**/*") + %w(README MIT-LICENSE CHANGELOG) 14 | s.require_path = 'lib' 15 | s.has_rdoc = true 16 | 17 | s.bindir = "bin" 18 | s.executables << "cap" << "capify" 19 | 20 | s.add_dependency 'net-ssh', ">= #{Capistrano::Version::SSH_REQUIRED.join(".")}", "< 1.99.0" 21 | s.add_dependency 'net-sftp', ">= #{Capistrano::Version::SFTP_REQUIRED.join(".")}", "< 1.99.0" 22 | s.add_dependency 'highline' 23 | 24 | s.author = "Jamis Buck" 25 | s.email = "jamis@37signals.com" 26 | s.homepage = "http://www.capify.org" 27 | s.rubyforge_project = "capistrano" 28 | 29 | end 30 | -------------------------------------------------------------------------------- /examples/sample.rb: -------------------------------------------------------------------------------- 1 | # set :user, "flippy" 2 | # set :password, "hello-flippy" 3 | # set :gateway, "gateway.example.com" 4 | 5 | role :web, "web1.example.com" 6 | role :app, "app1.example.com", "app2.example.com" 7 | 8 | desc <<-DESC 9 | This is a sample task. It is only intended to be used as a demonstration of \ 10 | how you can define your own tasks. 11 | DESC 12 | task :sample_task, :roles => :app do 13 | run "ls -l" 14 | end 15 | -------------------------------------------------------------------------------- /lib/capistrano.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/configuration' 2 | require 'capistrano/extensions' -------------------------------------------------------------------------------- /lib/capistrano/callback.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class Callback 3 | attr_reader :source, :options, :only, :except 4 | 5 | def initialize(source, options={}) 6 | @source = source 7 | @options = options 8 | @only = Array(options[:only]).map { |v| v.to_s } 9 | @except = Array(options[:except]).map { |v| v.to_s } 10 | end 11 | 12 | def applies_to?(task) 13 | if task && only.any? 14 | return only.include?(task.fully_qualified_name) 15 | elsif task && except.any? 16 | return !except.include?(task.fully_qualified_name) 17 | else 18 | return true 19 | end 20 | end 21 | end 22 | 23 | class ProcCallback < Callback 24 | def call 25 | source.call 26 | end 27 | end 28 | 29 | class TaskCallback < Callback 30 | attr_reader :config 31 | 32 | def initialize(config, source, options={}) 33 | super(source, options) 34 | @config = config 35 | end 36 | 37 | def call 38 | config.find_and_execute_task(source) 39 | end 40 | 41 | def applies_to?(task) 42 | super && (task.nil? || task.fully_qualified_name != source.to_s) 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /lib/capistrano/cli.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano' 2 | require 'capistrano/cli/execute' 3 | require 'capistrano/cli/help' 4 | require 'capistrano/cli/options' 5 | require 'capistrano/cli/ui' 6 | 7 | module Capistrano 8 | # The CLI class encapsulates the behavior of capistrano when it is invoked 9 | # as a command-line utility. This allows other programs to embed Capistrano 10 | # and preserve its command-line semantics. 11 | class CLI 12 | # The array of (unparsed) command-line options 13 | attr_reader :args 14 | 15 | # Create a new CLI instance using the given array of command-line parameters 16 | # to initialize it. By default, +ARGV+ is used, but you can specify a 17 | # different set of parameters (such as when embedded cap in a program): 18 | # 19 | # require 'capistrano/cli' 20 | # Capistrano::CLI.parse(%w(-vvvv -r config/deploy update_code)).execute! 21 | # 22 | # Note that you can also embed cap directly by creating a new Configuration 23 | # instance and setting it up, but you'll often wind up duplicating logic 24 | # defined in the CLI class. The above snippet, redone using the Configuration 25 | # class directly, would look like: 26 | # 27 | # require 'capistrano' 28 | # require 'capistrano/cli' 29 | # config = Capistrano::Configuration.new 30 | # config.logger_level = Capistrano::Logger::TRACE 31 | # config.set(:password) { Capistrano::CLI.password_prompt } 32 | # config.load "config/deploy" 33 | # config.update_code 34 | # 35 | # There may be times that you want/need the additional control offered by 36 | # manipulating the Configuration directly, but generally interfacing with 37 | # the CLI class is recommended. 38 | def initialize(args) 39 | @args = args.dup 40 | $stdout.sync = true # so that Net::SSH prompts show up 41 | end 42 | 43 | # Mix-in the actual behavior 44 | include Execute, Options, UI 45 | include Help # needs to be included last, because it overrides some methods 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/capistrano/cli/execute.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/configuration' 2 | 3 | module Capistrano 4 | class CLI 5 | module Execute 6 | def self.included(base) #:nodoc: 7 | base.extend(ClassMethods) 8 | end 9 | 10 | module ClassMethods 11 | # Invoke capistrano using the ARGV array as the option parameters. This 12 | # is what the command-line capistrano utility does. 13 | def execute 14 | parse(ARGV).execute! 15 | end 16 | end 17 | 18 | # Using the options build when the command-line was parsed, instantiate 19 | # a new Capistrano configuration, initialize it, and execute the 20 | # requested actions. 21 | # 22 | # Returns the Configuration instance used, if successful. 23 | def execute! 24 | config = instantiate_configuration 25 | config.logger.level = options[:verbose] 26 | 27 | set_pre_vars(config) 28 | load_recipes(config) 29 | 30 | config.trigger(:load) 31 | execute_requested_actions(config) 32 | config.trigger(:exit) 33 | 34 | config 35 | rescue Exception => error 36 | handle_error(error) 37 | end 38 | 39 | def execute_requested_actions(config) 40 | Array(options[:vars]).each { |name, value| config.set(name, value) } 41 | 42 | Array(options[:actions]).each do |action| 43 | config.find_and_execute_task(action, :before => :start, :after => :finish) 44 | end 45 | end 46 | 47 | def set_pre_vars(config) #:nodoc: 48 | config.set :password, options[:password] 49 | Array(options[:pre_vars]).each { |name, value| config.set(name, value) } 50 | end 51 | 52 | def load_recipes(config) #:nodoc: 53 | # load the standard recipe definition 54 | config.load "standard" 55 | 56 | # load systemwide config/recipe definition 57 | config.load(options[:sysconf]) if options[:sysconf] && File.file?(options[:sysconf]) 58 | 59 | # load user config/recipe definition 60 | config.load(options[:dotfile]) if options[:dotfile] && File.file?(options[:dotfile]) 61 | 62 | Array(options[:recipes]).each { |recipe| config.load(recipe) } 63 | end 64 | 65 | # Primarily useful for testing, but subclasses of CLI could conceivably 66 | # override this method to return a Configuration subclass or replacement. 67 | def instantiate_configuration #:nodoc: 68 | Capistrano::Configuration.new 69 | end 70 | 71 | def handle_error(error) #:nodoc: 72 | case error 73 | when Net::SSH::AuthenticationFailed 74 | abort "authentication failed for `#{error.message}'" 75 | when Capistrano::Error 76 | abort(error.message) 77 | else raise error 78 | end 79 | end 80 | end 81 | end 82 | end -------------------------------------------------------------------------------- /lib/capistrano/cli/help.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class CLI 3 | module Help 4 | LINE_PADDING = 7 5 | MIN_MAX_LEN = 30 6 | HEADER_LEN = 60 7 | 8 | def self.included(base) #:nodoc: 9 | base.send :alias_method, :execute_requested_actions_without_help, :execute_requested_actions 10 | base.send :alias_method, :execute_requested_actions, :execute_requested_actions_with_help 11 | end 12 | 13 | def execute_requested_actions_with_help(config) 14 | if options[:tasks] 15 | task_list(config) 16 | elsif options[:explain] 17 | explain_task(config, options[:explain]) 18 | else 19 | execute_requested_actions_without_help(config) 20 | end 21 | end 22 | 23 | def task_list(config) #:nodoc: 24 | tasks = config.task_list(:all) 25 | 26 | if tasks.empty? 27 | warn "There are no tasks available. Please specify a recipe file to load." 28 | else 29 | all_tasks_length = tasks.length 30 | if options[:verbose].to_i < 1 31 | tasks = tasks.reject { |t| t.description.empty? || t.description =~ /^\[internal\]/ } 32 | end 33 | 34 | tasks = tasks.sort_by { |task| task.fully_qualified_name } 35 | 36 | longest = tasks.map { |task| task.fully_qualified_name.length }.max 37 | max_length = output_columns - longest - LINE_PADDING 38 | max_length = MIN_MAX_LEN if max_length < MIN_MAX_LEN 39 | 40 | tasks.each do |task| 41 | puts "cap %-#{longest}s # %s" % [task.fully_qualified_name, task.brief_description(max_length)] 42 | end 43 | 44 | if all_tasks_length > tasks.length 45 | puts 46 | puts "Some tasks were not listed, either because they have no description," 47 | puts "or because they are only used internally by other tasks. To see all" 48 | puts "tasks, type `#{File.basename($0)} -Tv'." 49 | end 50 | 51 | puts 52 | puts "Extended help may be available for these tasks." 53 | puts "Type `#{File.basename($0)} -e taskname' to view it." 54 | end 55 | end 56 | 57 | def explain_task(config, name) #:nodoc: 58 | task = config.find_task(name) 59 | if task.nil? 60 | warn "The task `#{name}' does not exist." 61 | else 62 | puts "-" * HEADER_LEN 63 | puts "cap #{name}" 64 | puts "-" * HEADER_LEN 65 | 66 | if task.description.empty? 67 | puts "There is no description for this task." 68 | else 69 | puts format_text(task.description) 70 | end 71 | 72 | puts 73 | end 74 | end 75 | 76 | def long_help #:nodoc: 77 | help_text = File.read(File.join(File.dirname(__FILE__), "help.txt")) 78 | self.class.ui.page_at = self.class.ui.output_rows - 2 79 | self.class.ui.say format_text(help_text) 80 | end 81 | 82 | def format_text(text) #:nodoc: 83 | formatted = "" 84 | text.each_line do |line| 85 | indentation = line[/^\s+/] || "" 86 | indentation_size = indentation.split(//).inject(0) { |c,s| c + (s[0] == ?\t ? 8 : 1) } 87 | line_length = output_columns - indentation_size 88 | line_length = MIN_MAX_LEN if line_length < MIN_MAX_LEN 89 | lines = line.strip.gsub(/(.{1,#{line_length}})(?:\s+|\Z)/, "\\1\n").split(/\n/) 90 | if lines.empty? 91 | formatted << "\n" 92 | else 93 | formatted << lines.map { |l| "#{indentation}#{l}\n" }.join 94 | end 95 | end 96 | formatted 97 | end 98 | 99 | def output_columns #:nodoc: 100 | @output_columns ||= self.class.ui.output_cols > 80 ? 80 : self.class.ui.output_cols 101 | end 102 | end 103 | end 104 | end -------------------------------------------------------------------------------- /lib/capistrano/cli/help.txt: -------------------------------------------------------------------------------- 1 | ----------------------------- 2 | <%= color('Capistrano', :bold) %> 3 | ----------------------------- 4 | 5 | Capistrano is a utility for automating the execution of commands across multiple remote machines. It was originally conceived as an aid to deploy Ruby on Rails web applications, but has since evolved to become a much more general-purpose tool. 6 | 7 | The command-line interface to Capistrano is via the `cap' command. 8 | 9 | cap [ option ] ... action ... 10 | 11 | The following options are understood: 12 | 13 | <%= color '-e, --explain TASK', :bold %> 14 | Displays the extended description of the given task. Not all tasks will have an extended description, but for those that do, this can provide a wealth of additional usage information, such as describing environment variables or settings that can affect the execution of the task. 15 | 16 | <%= color '-F, --default-config', :bold %> 17 | By default, cap will search for a config file named `Capfile' or `capfile' in the current directory, or in any parent directory, and will automatically load it. However, if you specify the -f flag (see below), cap will use that file instead of the default config. If you want to use both the default config, and files loaded via -f, you can specify -F to force cap to search for and load the default config, even if additional files were specified via -f. 18 | 19 | <%= color '-f, --file FILE', :bold %> 20 | Causes the named file to be loaded. Capistrano will search both its own recipe directory, as well as the current directory, looking for the named file. An ".rb" extension is optional. The -f option may be given any number of times, but if it is given, it will take the place of the normal `Capfile' or `capfile' detection. Use -F if you want the default capfile to be loaded when you use -f. 21 | 22 | <%= color '-H, --long-help', :bold %> 23 | Displays this document and exits. 24 | 25 | <%= color '-h, --help', :bold %> 26 | Shows a brief summary of these options and exits. 27 | 28 | <%= color '-p, --password', :bold %> 29 | Normally, cap will prompt for the password on-demand, the first time it is needed. This can make it hard to walk away from Capistrano, since you might not know if it will prompt for a password down the road. In such cases, you can use the -p option to force cap to prompt for the password immediately. 30 | 31 | <%= color '-q, --quiet', :bold %> 32 | Display only critical error messages. All other output is suppressed. 33 | 34 | <%= color '-S, --set-before NAME=VALUE', :bold %> 35 | Sets the given variable to the given value, before loading any recipe files. This is useful if you have a recipe file that depends on a certain variable being set, at the time it is loaded. 36 | 37 | <%= color '-s, --set NAME=VALUE', :bold %> 38 | Sets the given variable to the given value, after loading all recipe files. This is useful when you want to override the value of a variable which is used in a task. Note that this will set the variables too late for them to affect conditions that are executed as the recipes are loaded. 39 | 40 | <%= color '-T, --tasks', :bold %> 41 | Displays the list of all tasks in all loaded recipe files. If a task has no description, or if the description starts with the [internal] tag, the task will not be listed unless you also specify -v. 42 | 43 | <%= color '-V, --version', :bold %> 44 | Shows the current Capistrano version number and exits. 45 | 46 | <%= color '-v, --verbose', :bold %> 47 | Increase the verbosity. You can specify this option up to three times to further increase verbosity. By default, cap will use maximum verbosity, but if you specify an explicit verbosity, that will be used instead. See also -q. 48 | 49 | <%= color '-X, --skip-system-config', :bold %> 50 | By default, cap will look for and (if it exists) load the global system configuration file located in /etc/capistrano.conf. If you don't want cap to load that file, give this option. 51 | 52 | <%= color '-x, --skip-user-config', :bold %> 53 | By default, cap will look for and (if it exists) load the user-specific configuration file located in $HOME/.caprc. If you don't want cap to load that file, give this option. 54 | -------------------------------------------------------------------------------- /lib/capistrano/cli/ui.rb: -------------------------------------------------------------------------------- 1 | require 'highline' 2 | 3 | # work around problem where HighLine detects an eof on $stdin and raises an 4 | # error. 5 | HighLine.track_eof = false 6 | 7 | module Capistrano 8 | class CLI 9 | module UI 10 | def self.included(base) #:nodoc: 11 | base.extend(ClassMethods) 12 | end 13 | 14 | module ClassMethods 15 | # Return the object that provides UI-specific methods, such as prompts 16 | # and more. 17 | def ui 18 | @ui ||= HighLine.new 19 | end 20 | 21 | # Prompt for a password using echo suppression. 22 | def password_prompt(prompt="Password: ") 23 | ui.ask(prompt) { |q| q.echo = false } 24 | end 25 | end 26 | end 27 | end 28 | end -------------------------------------------------------------------------------- /lib/capistrano/command.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/errors' 2 | 3 | module Capistrano 4 | 5 | # This class encapsulates a single command to be executed on a set of remote 6 | # machines, in parallel. 7 | class Command 8 | attr_reader :command, :sessions, :options 9 | 10 | def self.process(command, sessions, options={}, &block) 11 | new(command, sessions, options, &block).process! 12 | end 13 | 14 | # Instantiates a new command object. The +command+ must be a string 15 | # containing the command to execute. +sessions+ is an array of Net::SSH 16 | # session instances, and +options+ must be a hash containing any of the 17 | # following keys: 18 | # 19 | # * +logger+: (optional), a Capistrano::Logger instance 20 | # * +data+: (optional), a string to be sent to the command via it's stdin 21 | # * +env+: (optional), a string or hash to be interpreted as environment 22 | # variables that should be defined for this command invocation. 23 | def initialize(command, sessions, options={}, &block) 24 | @command = command.strip.gsub(/\r?\n/, "\\\n") 25 | @sessions = sessions 26 | @options = options 27 | @callback = block 28 | @channels = open_channels 29 | end 30 | 31 | # Processes the command in parallel on all specified hosts. If the command 32 | # fails (non-zero return code) on any of the hosts, this will raise a 33 | # Capistrano::CommandError. 34 | def process! 35 | since = Time.now 36 | loop do 37 | active = 0 38 | @channels.each do |ch| 39 | next if ch[:closed] 40 | active += 1 41 | ch.connection.process(true) 42 | end 43 | 44 | break if active == 0 45 | if Time.now - since >= 1 46 | since = Time.now 47 | @channels.each { |ch| ch.connection.ping! } 48 | end 49 | sleep 0.01 # a brief respite, to keep the CPU from going crazy 50 | end 51 | 52 | logger.trace "command finished" if logger 53 | 54 | if (failed = @channels.select { |ch| ch[:status] != 0 }).any? 55 | hosts = failed.map { |ch| ch[:server] } 56 | error = CommandError.new("command #{command.inspect} failed on #{hosts.join(',')}") 57 | error.hosts = hosts 58 | raise error 59 | end 60 | 61 | self 62 | end 63 | 64 | # Force the command to stop processing, by closing all open channels 65 | # associated with this command. 66 | def stop! 67 | @channels.each do |ch| 68 | ch.close unless ch[:closed] 69 | end 70 | end 71 | 72 | private 73 | 74 | def logger 75 | options[:logger] 76 | end 77 | 78 | def open_channels 79 | sessions.map do |session| 80 | session.open_channel do |channel| 81 | server = session.xserver 82 | 83 | channel[:server] = server 84 | channel[:host] = server.host 85 | channel[:options] = options 86 | 87 | execute_command = Proc.new do |ch| 88 | logger.trace "executing command", ch[:server] if logger 89 | cmd = replace_placeholders(command, ch) 90 | 91 | if options[:shell] == false 92 | shell = nil 93 | else 94 | shell = "#{options[:shell] || "sh"} -c" 95 | cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" } 96 | cmd = "\"#{cmd}\"" 97 | end 98 | 99 | command_line = [environment, shell, cmd].compact.join(" ") 100 | 101 | ch.exec(command_line) 102 | ch.send_data(options[:data]) if options[:data] 103 | end 104 | 105 | if options[:pty] 106 | channel.request_pty(:want_reply => true) 107 | channel.on_success(&execute_command) 108 | channel.on_failure do |ch| 109 | # just log it, don't actually raise an exception, since the 110 | # process method will see that the status is not zero and will 111 | # raise an exception then. 112 | logger.important "could not open channel", ch[:server] if logger 113 | ch.close 114 | end 115 | else 116 | execute_command.call(channel) 117 | end 118 | 119 | channel.on_data do |ch, data| 120 | @callback[ch, :out, data] if @callback 121 | end 122 | 123 | channel.on_extended_data do |ch, type, data| 124 | @callback[ch, :err, data] if @callback 125 | end 126 | 127 | channel.on_request do |ch, request, reply, data| 128 | ch[:status] = data.read_long if request == "exit-status" 129 | end 130 | 131 | channel.on_close do |ch| 132 | ch[:closed] = true 133 | end 134 | end 135 | end 136 | end 137 | 138 | def replace_placeholders(command, channel) 139 | command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host]) 140 | end 141 | 142 | # prepare a space-separated sequence of variables assignments 143 | # intended to be prepended to a command, so the shell sets 144 | # the environment before running the command. 145 | # i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH', 146 | # 'TEST' => '( "quoted" )'} 147 | # environment returns: 148 | # "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH" 149 | def environment 150 | return if options[:env].nil? || options[:env].empty? 151 | @environment ||= if String === options[:env] 152 | "env #{options[:env]}" 153 | else 154 | options[:env].inject("env") do |string, (name, value)| 155 | value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" } 156 | string << " #{name}=#{value}" 157 | end 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/capistrano/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/logger' 2 | 3 | require 'capistrano/configuration/callbacks' 4 | require 'capistrano/configuration/connections' 5 | require 'capistrano/configuration/execution' 6 | require 'capistrano/configuration/loading' 7 | require 'capistrano/configuration/namespaces' 8 | require 'capistrano/configuration/roles' 9 | require 'capistrano/configuration/servers' 10 | require 'capistrano/configuration/variables' 11 | 12 | require 'capistrano/configuration/actions/file_transfer' 13 | require 'capistrano/configuration/actions/inspect' 14 | require 'capistrano/configuration/actions/invocation' 15 | 16 | module Capistrano 17 | # Represents a specific Capistrano configuration. A Configuration instance 18 | # may be used to load multiple recipe files, define and describe tasks, 19 | # define roles, and set configuration variables. 20 | class Configuration 21 | # The logger instance defined for this configuration. 22 | attr_accessor :logger 23 | 24 | def initialize #:nodoc: 25 | @logger = Logger.new 26 | end 27 | 28 | # make the DSL easier to read when using lazy evaluation via lambdas 29 | alias defer lambda 30 | 31 | # The includes must come at the bottom, since they may redefine methods 32 | # defined in the base class. 33 | include Connections, Execution, Loading, Namespaces, Roles, Servers, Variables 34 | 35 | # Mix in the actions 36 | include Actions::FileTransfer, Actions::Inspect, Actions::Invocation 37 | 38 | # Must mix last, because it hooks into previously defined methods 39 | include Callbacks 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/capistrano/configuration/actions/file_transfer.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/upload' 2 | 3 | module Capistrano 4 | class Configuration 5 | module Actions 6 | module FileTransfer 7 | 8 | # Store the given data at the given location on all servers targetted 9 | # by the current task. If :mode is specified it is used to 10 | # set the mode on the file. 11 | def put(data, path, options={}) 12 | execute_on_servers(options) do |servers| 13 | targets = servers.map { |s| sessions[s] } 14 | Upload.process(targets, path, :data => data, :mode => options[:mode], :logger => logger) 15 | end 16 | end 17 | 18 | # Get file remote_path from FIRST server targetted by 19 | # the current task and transfer it to local machine as path. 20 | # 21 | # get "#{deploy_to}/current/log/production.log", "log/production.log.web" 22 | def get(remote_path, path, options = {}) 23 | execute_on_servers(options.merge(:once => true)) do |servers| 24 | logger.info "downloading `#{servers.first.host}:#{remote_path}' to `#{path}'" 25 | sftp = sessions[servers.first].sftp 26 | sftp.connect unless sftp.state == :open 27 | sftp.get_file remote_path, path 28 | logger.debug "download finished" 29 | end 30 | end 31 | 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/capistrano/configuration/actions/inspect.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/errors' 2 | 3 | module Capistrano 4 | class Configuration 5 | module Actions 6 | module Inspect 7 | 8 | # Streams the result of the command from all servers that are the 9 | # target of the current task. All these streams will be joined into a 10 | # single one, so you can, say, watch 10 log files as though they were 11 | # one. Do note that this is quite expensive from a bandwidth 12 | # perspective, so use it with care. 13 | # 14 | # The command is invoked via #invoke_command. 15 | # 16 | # Usage: 17 | # 18 | # desc "Run a tail on multiple log files at the same time" 19 | # task :tail_fcgi, :roles => :app do 20 | # stream "tail -f #{shared_path}/log/fastcgi.crash.log" 21 | # end 22 | def stream(command, options={}) 23 | invoke_command(command, options) do |ch, stream, out| 24 | puts out if stream == :out 25 | warn "[err :: #{ch[:server]}] #{out}" if stream == :err 26 | end 27 | end 28 | 29 | # Executes the given command on the first server targetted by the 30 | # current task, collects it's stdout into a string, and returns the 31 | # string. The command is invoked via #invoke_command. 32 | def capture(command, options={}) 33 | output = "" 34 | invoke_command(command, options.merge(:once => true)) do |ch, stream, data| 35 | case stream 36 | when :out then output << data 37 | when :err then raise CaptureError, "error processing #{command.inspect}: #{data.inspect}" 38 | end 39 | end 40 | output 41 | end 42 | 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/capistrano/configuration/actions/invocation.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/command' 2 | 3 | module Capistrano 4 | class Configuration 5 | module Actions 6 | module Invocation 7 | def self.included(base) #:nodoc: 8 | base.extend(ClassMethods) 9 | 10 | base.send :alias_method, :initialize_without_invocation, :initialize 11 | base.send :alias_method, :initialize, :initialize_with_invocation 12 | 13 | base.default_io_proc = Proc.new do |ch, stream, out| 14 | level = stream == :err ? :important : :info 15 | ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}") 16 | end 17 | end 18 | 19 | module ClassMethods 20 | attr_accessor :default_io_proc 21 | end 22 | 23 | def initialize_with_invocation(*args) #:nodoc: 24 | initialize_without_invocation(*args) 25 | set :default_environment, {} 26 | set :default_run_options, {} 27 | end 28 | 29 | # Invokes the given command. If a +via+ key is given, it will be used 30 | # to determine what method to use to invoke the command. It defaults 31 | # to :run, but may be :sudo, or any other method that conforms to the 32 | # same interface as run and sudo. 33 | def invoke_command(cmd, options={}, &block) 34 | options = options.dup 35 | via = options.delete(:via) || :run 36 | send(via, cmd, options, &block) 37 | end 38 | 39 | # Execute the given command on all servers that are the target of the 40 | # current task. If a block is given, it is invoked for all output 41 | # generated by the command, and should accept three parameters: the SSH 42 | # channel (which may be used to send data back to the remote process), 43 | # the stream identifier (:err for stderr, and :out for 44 | # stdout), and the data that was received. 45 | def run(cmd, options={}, &block) 46 | block ||= self.class.default_io_proc 47 | logger.debug "executing #{cmd.strip.inspect}" 48 | 49 | options = add_default_command_options(options) 50 | 51 | execute_on_servers(options) do |servers| 52 | targets = servers.map { |s| sessions[s] } 53 | Command.process(cmd, targets, options.merge(:logger => logger), &block) 54 | end 55 | end 56 | 57 | # Like #run, but executes the command via sudo. This assumes 58 | # that the sudo password (if required) is the same as the password for 59 | # logging in to the server. 60 | # 61 | # Also, this module accepts a :sudo configuration variable, 62 | # which (if specified) will be used as the full path to the sudo 63 | # executable on the remote machine: 64 | # 65 | # set :sudo, "/opt/local/bin/sudo" 66 | def sudo(command, options={}, &block) 67 | block ||= self.class.default_io_proc 68 | 69 | options = options.dup 70 | as = options.delete(:as) 71 | 72 | user = as && "-u #{as}" 73 | command = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user, command].compact.join(" ") 74 | 75 | run(command, options, &sudo_behavior_callback(block)) 76 | end 77 | 78 | # Returns a Proc object that defines the behavior of the sudo 79 | # callback. The returned Proc will defer to the +fallback+ argument 80 | # (which should also be a Proc) for any output it does not 81 | # explicitly handle. 82 | def sudo_behavior_callback(fallback) #:nodoc: 83 | # in order to prevent _each host_ from prompting when the password 84 | # was wrong, let's track which host prompted first and only allow 85 | # subsequent prompts from that host. 86 | prompt_host = nil 87 | 88 | Proc.new do |ch, stream, out| 89 | if out =~ /^#{Regexp.escape(sudo_prompt)}/ 90 | ch.send_data "#{self[:password]}\n" 91 | elsif out =~ /try again/ 92 | if prompt_host.nil? || prompt_host == ch[:server] 93 | prompt_host = ch[:server] 94 | logger.important out, "#{stream} :: #{ch[:server]}" 95 | reset! :password 96 | end 97 | else 98 | fallback.call(ch, stream, out) 99 | end 100 | end 101 | end 102 | 103 | # Merges the various default command options into the options hash and 104 | # returns the result. The default command options that are understand 105 | # are: 106 | # 107 | # * :default_environment: If the :env key already exists, the :env 108 | # key is merged into default_environment and then added back into 109 | # options. 110 | # * :default_shell: if the :shell key already exists, it will be used. 111 | # Otherwise, if the :default_shell key exists in the configuration, 112 | # it will be used. Otherwise, no :shell key is added. 113 | def add_default_command_options(options) 114 | defaults = self[:default_run_options] 115 | options = defaults.merge(options) 116 | 117 | env = self[:default_environment] 118 | env = env.merge(options[:env]) if options[:env] 119 | options[:env] = env unless env.empty? 120 | 121 | shell = options[:shell] || self[:default_shell] 122 | options[:shell] = shell if shell 123 | 124 | options 125 | end 126 | 127 | # Returns the prompt text to use with sudo 128 | def sudo_prompt 129 | fetch(:sudo_prompt, "sudo password: ") 130 | end 131 | end 132 | end 133 | end 134 | end -------------------------------------------------------------------------------- /lib/capistrano/configuration/execution.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/errors' 2 | 3 | module Capistrano 4 | class Configuration 5 | module Execution 6 | def self.included(base) #:nodoc: 7 | base.send :alias_method, :initialize_without_execution, :initialize 8 | base.send :alias_method, :initialize, :initialize_with_execution 9 | end 10 | 11 | # The call stack of the tasks. The currently executing task may inspect 12 | # this to see who its caller was. The current task is always the last 13 | # element of this stack. 14 | attr_reader :task_call_frames 15 | 16 | # The stack of tasks that have registered rollback handlers within the 17 | # current transaction. If this is nil, then there is no transaction 18 | # that is currently active. 19 | attr_reader :rollback_requests 20 | 21 | # A struct for representing a single instance of an invoked task. 22 | TaskCallFrame = Struct.new(:task, :rollback) 23 | 24 | def initialize_with_execution(*args) #:nodoc: 25 | initialize_without_execution(*args) 26 | @task_call_frames = [] 27 | end 28 | private :initialize_with_execution 29 | 30 | # Returns true if there is a transaction currently active. 31 | def transaction? 32 | !rollback_requests.nil? 33 | end 34 | 35 | # Invoke a set of tasks in a transaction. If any task fails (raises an 36 | # exception), all tasks executed within the transaction are inspected to 37 | # see if they have an associated on_rollback hook, and if so, that hook 38 | # is called. 39 | def transaction 40 | raise ArgumentError, "expected a block" unless block_given? 41 | raise ScriptError, "transaction must be called from within a task" if task_call_frames.empty? 42 | 43 | return yield if transaction? 44 | 45 | logger.info "transaction: start" 46 | begin 47 | @rollback_requests = [] 48 | yield 49 | logger.info "transaction: commit" 50 | rescue Object => e 51 | rollback! 52 | raise 53 | ensure 54 | @rollback_requests = nil 55 | end 56 | end 57 | 58 | # Specifies an on_rollback hook for the currently executing task. If this 59 | # or any subsequent task then fails, and a transaction is active, this 60 | # hook will be executed. 61 | def on_rollback(&block) 62 | if transaction? 63 | task_call_frames.last.rollback = block 64 | rollback_requests << task_call_frames.last 65 | end 66 | end 67 | 68 | # Returns the TaskDefinition object for the currently executing task. 69 | # It returns nil if there is no task being executed. 70 | def current_task 71 | return nil if task_call_frames.empty? 72 | task_call_frames.last.task 73 | end 74 | 75 | # Executes the task with the given name, including the before and after 76 | # hooks. 77 | def execute_task(task) 78 | logger.debug "executing `#{task.fully_qualified_name}'" 79 | push_task_call_frame(task) 80 | task.namespace.instance_eval(&task.body) 81 | ensure 82 | pop_task_call_frame 83 | end 84 | 85 | # Attempts to locate the task at the given fully-qualified path, and 86 | # execute it. If no such task exists, a Capistrano::NoSuchTaskError will 87 | # be raised. 88 | def find_and_execute_task(path, hooks={}) 89 | task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" 90 | 91 | trigger(hooks[:before], task) if hooks[:before] 92 | result = execute_task(task) 93 | trigger(hooks[:after], task) if hooks[:after] 94 | 95 | result 96 | end 97 | 98 | protected 99 | 100 | def rollback! 101 | # throw the task back on the stack so that roles are properly 102 | # interpreted in the scope of the task in question. 103 | rollback_requests.reverse.each do |frame| 104 | begin 105 | push_task_call_frame(frame.task) 106 | logger.important "rolling back", frame.task.fully_qualified_name 107 | frame.rollback.call 108 | rescue Object => e 109 | logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name 110 | ensure 111 | pop_task_call_frame 112 | end 113 | end 114 | end 115 | 116 | def push_task_call_frame(task) 117 | frame = TaskCallFrame.new(task) 118 | task_call_frames.push frame 119 | end 120 | 121 | def pop_task_call_frame 122 | task_call_frames.pop 123 | end 124 | end 125 | end 126 | end -------------------------------------------------------------------------------- /lib/capistrano/configuration/roles.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/server_definition' 2 | require 'capistrano/role' 3 | 4 | module Capistrano 5 | class Configuration 6 | module Roles 7 | def self.included(base) #:nodoc: 8 | base.send :alias_method, :initialize_without_roles, :initialize 9 | base.send :alias_method, :initialize, :initialize_with_roles 10 | end 11 | 12 | # The hash of roles defined for this configuration. Each entry in the 13 | # hash points to an array of server definitions that belong in that 14 | # role. 15 | attr_reader :roles 16 | 17 | def initialize_with_roles(*args) #:nodoc: 18 | initialize_without_roles(*args) 19 | @roles = Hash.new { |h,k| h[k] = Role.new } 20 | end 21 | 22 | # Define a new role and its associated servers. You must specify at least 23 | # one host for each role. Also, you can specify additional information 24 | # (in the form of a Hash) which can be used to more uniquely specify the 25 | # subset of servers specified by this specific role definition. 26 | # 27 | # Usage: 28 | # 29 | # role :db, "db1.example.com", "db2.example.com" 30 | # role :db, "master.example.com", :primary => true 31 | # role :app, "app1.example.com", "app2.example.com" 32 | # 33 | # You can also encode the username and port number for each host in the 34 | # server string, if needed: 35 | # 36 | # role :web, "www@web1.example.com" 37 | # role :file, "files.example.com:4144" 38 | # role :db, "admin@db3.example.com:1234" 39 | # 40 | # Lastly, username and port number may be passed as options, if that is 41 | # preferred; note that the options apply to all servers defined in 42 | # that call to "role": 43 | # 44 | # role :web, "web2", "web3", :user => "www", :port => 2345 45 | def role(which, *args, &block) 46 | options = args.last.is_a?(Hash) ? args.pop : {} 47 | which = which.to_sym 48 | roles[which].push(block, options) if block_given? 49 | args.each { |host| roles[which] << ServerDefinition.new(host, options) } 50 | end 51 | 52 | # An alternative way to associate servers with roles. If you have a server 53 | # that participates in multiple roles, this can be a DRYer way to describe 54 | # the relationships. Pass the host definition as the first parameter, and 55 | # the roles as the remaining parameters: 56 | # 57 | # server "master.example.com", :web, :app 58 | def server(host, *roles) 59 | options = roles.last.is_a?(Hash) ? roles.pop : {} 60 | raise ArgumentError, "you must associate a server with at least one role" if roles.empty? 61 | roles.each { |name| role(name, host, options) } 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/capistrano/configuration/servers.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class Configuration 3 | module Servers 4 | # Identifies all servers that the given task should be executed on. 5 | # The options hash accepts the same arguments as #find_servers, and any 6 | # preexisting options there will take precedence over the options in 7 | # the task. 8 | def find_servers_for_task(task, options={}) 9 | find_servers(task.options.merge(options)) 10 | end 11 | 12 | # Attempts to find all defined servers that match the given criteria. 13 | # The options hash may include a :hosts option (which should specify 14 | # an array of host names or ServerDefinition instances), a :roles 15 | # option (specifying an array of roles), an :only option (specifying 16 | # a hash of key/value pairs that any matching server must match), and 17 | # an :exception option (like :only, but the inverse). 18 | # 19 | # Additionally, if the HOSTS environment variable is set, it will take 20 | # precedence over any other options. Similarly, the ROLES environment 21 | # variable will take precedence over other options. If both HOSTS and 22 | # ROLES are given, HOSTS wins. 23 | # 24 | # Usage: 25 | # 26 | # # return all known servers 27 | # servers = find_servers 28 | # 29 | # # find all servers in the app role that are not exempted from 30 | # # deployment 31 | # servers = find_servers :roles => :app, 32 | # :except => { :no_release => true } 33 | # 34 | # # returns the given hosts, translated to ServerDefinition objects 35 | # servers = find_servers :hosts => "jamis@example.host.com" 36 | def find_servers(options={}) 37 | hosts = server_list_from(ENV['HOSTS'] || options[:hosts]) 38 | roles = role_list_from(ENV['ROLES'] || options[:roles] || self.roles.keys) 39 | only = options[:only] || {} 40 | except = options[:except] || {} 41 | 42 | if hosts.any? 43 | hosts.uniq 44 | else 45 | servers = roles.inject([]) { |list, role| list.concat(self.roles[role]) } 46 | servers = servers.select { |server| only.all? { |key,value| server.options[key] == value } } 47 | servers = servers.reject { |server| except.any? { |key,value| server.options[key] == value } } 48 | servers.uniq 49 | end 50 | end 51 | 52 | protected 53 | 54 | def server_list_from(hosts) 55 | hosts = hosts.split(/,/) if String === hosts 56 | hosts = build_list(hosts) 57 | hosts.map { |s| String === s ? ServerDefinition.new(s.strip) : s } 58 | end 59 | 60 | def role_list_from(roles) 61 | roles = roles.split(/,/) if String === roles 62 | roles = build_list(roles) 63 | roles.map do |role| 64 | role = String === role ? role.strip.to_sym : role 65 | raise ArgumentError, "unknown role `#{role}'" unless self.roles.key?(role) 66 | role 67 | end 68 | end 69 | 70 | def build_list(list) 71 | Array(list).map { |item| item.respond_to?(:call) ? item.call : item }.flatten 72 | end 73 | end 74 | end 75 | end -------------------------------------------------------------------------------- /lib/capistrano/configuration/variables.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | 3 | module Capistrano 4 | class Configuration 5 | module Variables 6 | def self.included(base) #:nodoc: 7 | %w(initialize respond_to? method_missing).each do |m| 8 | base_name = m[/^\w+/] 9 | punct = m[/\W+$/] 10 | base.send :alias_method, "#{base_name}_without_variables#{punct}", m 11 | base.send :alias_method, m, "#{base_name}_with_variables#{punct}" 12 | end 13 | end 14 | 15 | # The hash of variables that have been defined in this configuration 16 | # instance. 17 | attr_reader :variables 18 | 19 | # Set a variable to the given value. 20 | def set(variable, *args, &block) 21 | if variable.to_s !~ /^[_a-z]/ 22 | raise ArgumentError, "invalid variable `#{variable}' (variables must begin with an underscore, or a lower-case letter)" 23 | end 24 | 25 | if !block_given? && args.empty? || block_given? && !args.empty? 26 | raise ArgumentError, "you must specify exactly one of either a value or a block" 27 | end 28 | 29 | if args.length > 1 30 | raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" 31 | end 32 | 33 | value = args.empty? ? block : args.first 34 | sym = variable.to_sym 35 | protect(sym) { @variables[sym] = value } 36 | end 37 | 38 | alias :[]= :set 39 | 40 | # Removes any trace of the given variable. 41 | def unset(variable) 42 | sym = variable.to_sym 43 | protect(sym) do 44 | @original_procs.delete(sym) 45 | @variables.delete(sym) 46 | end 47 | end 48 | 49 | # Returns true if the variable has been defined, and false otherwise. 50 | def exists?(variable) 51 | @variables.key?(variable.to_sym) 52 | end 53 | 54 | # If the variable was originally a proc value, it will be reset to it's 55 | # original proc value. Otherwise, this method does nothing. It returns 56 | # true if the variable was actually reset. 57 | def reset!(variable) 58 | sym = variable.to_sym 59 | protect(sym) do 60 | if @original_procs.key?(sym) 61 | @variables[sym] = @original_procs.delete(sym) 62 | true 63 | else 64 | false 65 | end 66 | end 67 | end 68 | 69 | # Access a named variable. If the value of the variable responds_to? :call, 70 | # #call will be invoked (without parameters) and the return value cached 71 | # and returned. 72 | def fetch(variable, *args) 73 | if !args.empty? && block_given? 74 | raise ArgumentError, "you must specify either a default value or a block, but not both" 75 | end 76 | 77 | sym = variable.to_sym 78 | protect(sym) do 79 | if !@variables.key?(sym) 80 | return args.first unless args.empty? 81 | return yield(variable) if block_given? 82 | raise IndexError, "`#{variable}' not found" 83 | end 84 | 85 | if @variables[sym].respond_to?(:call) 86 | @original_procs[sym] = @variables[sym] 87 | @variables[sym] = @variables[sym].call 88 | end 89 | end 90 | 91 | @variables[sym] 92 | end 93 | 94 | def [](variable) 95 | fetch(variable, nil) 96 | end 97 | 98 | def initialize_with_variables(*args) #:nodoc: 99 | initialize_without_variables(*args) 100 | @variables = {} 101 | @original_procs = {} 102 | @variable_locks = Hash.new { |h,k| h[k] = Mutex.new } 103 | 104 | set :ssh_options, {} 105 | set :logger, logger 106 | end 107 | private :initialize_with_variables 108 | 109 | def protect(variable) 110 | @variable_locks[variable.to_sym].synchronize { yield } 111 | end 112 | private :protect 113 | 114 | def respond_to_with_variables?(sym) #:nodoc: 115 | @variables.has_key?(sym) || respond_to_without_variables?(sym) 116 | end 117 | 118 | def method_missing_with_variables(sym, *args, &block) #:nodoc: 119 | if args.length == 0 && block.nil? && @variables.has_key?(sym) 120 | self[sym] 121 | else 122 | method_missing_without_variables(sym, *args, &block) 123 | end 124 | end 125 | end 126 | end 127 | end -------------------------------------------------------------------------------- /lib/capistrano/errors.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class Error < RuntimeError; end 3 | 4 | class CaptureError < Error; end 5 | class NoSuchTaskError < Error; end 6 | class NoMatchingServersError < Error; end 7 | 8 | class RemoteError < Error 9 | attr_accessor :hosts 10 | end 11 | 12 | class ConnectionError < RemoteError; end 13 | class UploadError < RemoteError; end 14 | class CommandError < RemoteError; end 15 | end 16 | -------------------------------------------------------------------------------- /lib/capistrano/extensions.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class ExtensionProxy #:nodoc: 3 | def initialize(config, mod) 4 | @config = config 5 | extend(mod) 6 | end 7 | 8 | def method_missing(sym, *args, &block) 9 | @config.send(sym, *args, &block) 10 | end 11 | end 12 | 13 | # Holds the set of registered plugins, keyed by name (where the name is a 14 | # symbol). 15 | EXTENSIONS = {} 16 | 17 | # Register the given module as a plugin with the given name. It will henceforth 18 | # be available via a proxy object on Configuration instances, accessible by 19 | # a method with the given name. 20 | def self.plugin(name, mod) 21 | name = name.to_sym 22 | return false if EXTENSIONS.has_key?(name) 23 | 24 | methods = Capistrano::Configuration.public_instance_methods + 25 | Capistrano::Configuration.protected_instance_methods + 26 | Capistrano::Configuration.private_instance_methods 27 | 28 | if methods.include?(name.to_s) 29 | raise Capistrano::Error, "registering a plugin named `#{name}' would shadow a method on Capistrano::Configuration with the same name" 30 | end 31 | 32 | Capistrano::Configuration.class_eval <<-STR, __FILE__, __LINE__+1 33 | def #{name} 34 | @__#{name}_proxy ||= Capistrano::ExtensionProxy.new(self, Capistrano::EXTENSIONS[#{name.inspect}]) 35 | end 36 | STR 37 | 38 | EXTENSIONS[name] = mod 39 | return true 40 | end 41 | 42 | # Unregister the plugin with the given name. 43 | def self.remove_plugin(name) 44 | name = name.to_sym 45 | if EXTENSIONS.delete(name) 46 | Capistrano::Configuration.send(:remove_method, name) 47 | return true 48 | end 49 | 50 | return false 51 | end 52 | 53 | def self.configuration(*args) #:nodoc: 54 | warn "[DEPRECATION] Capistrano.configuration is deprecated. Use Capistrano::Configuration.instance instead" 55 | Capistrano::Configuration.instance(*args) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/capistrano/gateway.rb: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION == "1.8.6" 2 | begin 3 | require 'fastthread' 4 | rescue LoadError 5 | warn "You are running Ruby 1.8.6, which has a bug in its threading implementation." 6 | warn "You are liable to encounter deadlocks running Capistrano, unless you install" 7 | warn "the fastthread library, which is available as a gem:" 8 | warn " gem install fastthread" 9 | end 10 | end 11 | 12 | require 'thread' 13 | require 'capistrano/errors' 14 | require 'capistrano/ssh' 15 | require 'capistrano/server_definition' 16 | 17 | Thread.abort_on_exception = true 18 | 19 | module Capistrano 20 | 21 | # Black magic. It uses threads and Net::SSH to set up a connection to a 22 | # gateway server, through which connections to other servers may be 23 | # tunnelled. 24 | # 25 | # It is used internally by Capistrano, but may be useful on its own, as well. 26 | # 27 | # Usage: 28 | # 29 | # gateway = Capistrano::Gateway.new(Capistrano::ServerDefinition.new('gateway.example.com')) 30 | # 31 | # sess1 = gateway.connect_to(Capistrano::ServerDefinition.new('hidden.example.com')) 32 | # sess2 = gateway.connect_to(Capistrano::ServerDefinition.new('other.example.com')) 33 | class Gateway 34 | # The Thread instance driving the gateway connection. 35 | attr_reader :thread 36 | 37 | # The Net::SSH session representing the gateway connection. 38 | attr_reader :session 39 | 40 | MAX_PORT = 65535 41 | MIN_PORT = 1024 42 | 43 | def initialize(server, options={}) #:nodoc: 44 | @options = options 45 | @next_port = MAX_PORT 46 | @terminate_thread = false 47 | @port_guard = Mutex.new 48 | 49 | mutex = Mutex.new 50 | waiter = ConditionVariable.new 51 | 52 | mutex.synchronize do 53 | @thread = Thread.new do 54 | logger.trace "starting connection to gateway `#{server}'" if logger 55 | SSH.connect(server, @options) do |@session| 56 | logger.trace "gateway connection established" if logger 57 | mutex.synchronize { waiter.signal } 58 | @session.loop do 59 | !@terminate_thread 60 | end 61 | end 62 | end 63 | 64 | waiter.wait(mutex) 65 | end 66 | end 67 | 68 | # Shuts down all forwarded connections and terminates the gateway. 69 | def shutdown! 70 | # cancel all active forward channels 71 | session.forward.active_locals.each do |lport, host, port| 72 | session.forward.cancel_local(lport) 73 | end 74 | 75 | # terminate the gateway thread 76 | @terminate_thread = true 77 | 78 | # wait for the gateway thread to stop 79 | thread.join 80 | end 81 | 82 | # Connects to the given server by opening a forwarded port from the local 83 | # host to the server, via the gateway, and then opens and returns a new 84 | # Net::SSH connection via that port. 85 | def connect_to(server) 86 | connection = nil 87 | logger.debug "establishing connection to `#{server}' via gateway" if logger 88 | local_port = next_port 89 | 90 | thread = Thread.new do 91 | begin 92 | local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => local_port) 93 | session.forward.local(local_port, server.host, server.port || 22) 94 | connection = SSH.connect(local_host, @options) 95 | connection.xserver = server 96 | logger.trace "connected: `#{server}' (via gateway)" if logger 97 | rescue Errno::EADDRINUSE 98 | local_port = next_port 99 | retry 100 | rescue Exception => e 101 | warn "#{e.class}: #{e.message}" 102 | warn e.backtrace.join("\n") 103 | end 104 | end 105 | 106 | thread.join 107 | if connection.nil? 108 | error = ConnectionError.new("could not establish connection to `#{server}'") 109 | error.hosts = [server] 110 | raise error 111 | end 112 | 113 | connection 114 | end 115 | 116 | private 117 | 118 | def logger 119 | @options[:logger] 120 | end 121 | 122 | def next_port 123 | @port_guard.synchronize do 124 | port = @next_port 125 | @next_port -= 1 126 | @next_port = MAX_PORT if @next_port < MIN_PORT 127 | port 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/capistrano/logger.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class Logger #:nodoc: 3 | attr_accessor :level 4 | attr_reader :device 5 | 6 | IMPORTANT = 0 7 | INFO = 1 8 | DEBUG = 2 9 | TRACE = 3 10 | 11 | MAX_LEVEL = 3 12 | 13 | def initialize(options={}) 14 | output = options[:output] || $stderr 15 | if output.respond_to?(:puts) 16 | @device = output 17 | else 18 | @device = File.open(output.to_str, "a") 19 | @needs_close = true 20 | end 21 | 22 | @options = options 23 | @level = 0 24 | end 25 | 26 | def close 27 | device.close if @needs_close 28 | end 29 | 30 | def log(level, message, line_prefix=nil) 31 | if level <= self.level 32 | indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)] 33 | message.each do |line| 34 | if line_prefix 35 | device.puts "#{indent} [#{line_prefix}] #{line.strip}\n" 36 | else 37 | device.puts "#{indent} #{line.strip}\n" 38 | end 39 | end 40 | end 41 | end 42 | 43 | def important(message, line_prefix=nil) 44 | log(IMPORTANT, message, line_prefix) 45 | end 46 | 47 | def info(message, line_prefix=nil) 48 | log(INFO, message, line_prefix) 49 | end 50 | 51 | def debug(message, line_prefix=nil) 52 | log(DEBUG, message, line_prefix) 53 | end 54 | 55 | def trace(message, line_prefix=nil) 56 | log(TRACE, message, line_prefix) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/compat.rb: -------------------------------------------------------------------------------- 1 | # A collection of compatibility scripts, to ease the transition between 2 | # Capistrano 1.x and Capistrano 2.x. 3 | 4 | # Depends on the deployment system 5 | load 'deploy' 6 | 7 | map = { "diff_from_last_deploy" => "deploy:pending:diff", 8 | "update" => "deploy:update", 9 | "update_code" => "deploy:update_code", 10 | "symlink" => "deploy:symlink", 11 | "restart" => "deploy:restart", 12 | "rollback" => "deploy:rollback", 13 | "cleanup" => "deploy:cleanup", 14 | "disable_web" => "deploy:web:disable", 15 | "enable_web" => "deploy:web:enable", 16 | "cold_deploy" => "deploy:cold", 17 | "deploy_with_migrations" => "deploy:migrations" } 18 | 19 | map.each do |old, new| 20 | desc "DEPRECATED: See #{new}." 21 | eval "task(#{old.inspect}) do 22 | warn \"[DEPRECATED] `#{old}' is deprecated. Use `#{new}' instead.\" 23 | find_and_execute_task(#{new.inspect}) 24 | end" 25 | end 26 | 27 | desc "DEPRECATED: See deploy:start." 28 | task :spinner do 29 | warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead." 30 | set :runner, fetch(:spinner_user, "app") 31 | deploy.start 32 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/dependencies.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/local_dependency' 2 | require 'capistrano/recipes/deploy/remote_dependency' 3 | 4 | module Capistrano 5 | module Deploy 6 | class Dependencies 7 | include Enumerable 8 | 9 | attr_reader :configuration 10 | 11 | def initialize(configuration) 12 | @configuration = configuration 13 | @dependencies = [] 14 | yield self if block_given? 15 | end 16 | 17 | def check 18 | yield self 19 | self 20 | end 21 | 22 | def remote 23 | dep = RemoteDependency.new(configuration) 24 | @dependencies << dep 25 | dep 26 | end 27 | 28 | def local 29 | dep = LocalDependency.new(configuration) 30 | @dependencies << dep 31 | dep 32 | end 33 | 34 | def each 35 | @dependencies.each { |d| yield d } 36 | self 37 | end 38 | 39 | def pass? 40 | all? { |d| d.pass? } 41 | end 42 | end 43 | end 44 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/local_dependency.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Deploy 3 | class LocalDependency 4 | attr_reader :configuration 5 | attr_reader :message 6 | 7 | def initialize(configuration) 8 | @configuration = configuration 9 | @success = true 10 | end 11 | 12 | def command(command) 13 | @message ||= "`#{command}' could not be found in the path on the local host" 14 | @success = find_in_path(command) 15 | self 16 | end 17 | 18 | def or(message) 19 | @message = message 20 | self 21 | end 22 | 23 | def pass? 24 | @success 25 | end 26 | 27 | private 28 | 29 | # Searches the path, looking for the given utility. If an executable 30 | # file is found that matches the parameter, this returns true. 31 | def find_in_path(utility) 32 | path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR) 33 | suffixes = RUBY_PLATFORM =~ /mswin/ ? %w(.bat .exe .com .cmd) : [""] 34 | 35 | path.each do |dir| 36 | suffixes.each do |sfx| 37 | file = File.join(dir, utility + sfx) 38 | return true if File.executable?(file) 39 | end 40 | end 41 | 42 | false 43 | end 44 | end 45 | end 46 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/remote_dependency.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Deploy 3 | class RemoteDependency 4 | attr_reader :configuration 5 | attr_reader :hosts 6 | 7 | def initialize(configuration) 8 | @configuration = configuration 9 | @success = true 10 | end 11 | 12 | def directory(path, options={}) 13 | @message ||= "`#{path}' is not a directory" 14 | try("test -d #{path}", options) 15 | self 16 | end 17 | 18 | def writable(path, options={}) 19 | @message ||= "`#{path}' is not writable" 20 | try("test -w #{path}", options) 21 | self 22 | end 23 | 24 | def command(command, options={}) 25 | @message ||= "`#{command}' could not be found in the path" 26 | try("which #{command}", options) 27 | self 28 | end 29 | 30 | def gem(name, version, options={}) 31 | @message ||= "gem `#{name}' #{version} could not be found" 32 | gem_cmd = configuration.fetch(:gem_command, "gem") 33 | try("#{gem_cmd} specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'", options) 34 | self 35 | end 36 | 37 | def match(command, expect, options={}) 38 | expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp) 39 | 40 | output_per_server = {} 41 | try("#{command} ", options) do |ch, stream, out| 42 | output_per_server[ch[:server]] ||= '' 43 | output_per_server[ch[:server]] += out 44 | end 45 | 46 | # It is possible for some of these commands to return a status != 0 47 | # (for example, rake --version exits with a 1). For this check we 48 | # just care if the output matches, so we reset the success flag. 49 | @success = true 50 | 51 | errored_hosts = [] 52 | output_per_server.each_pair do |server, output| 53 | next if output =~ expect 54 | errored_hosts << server 55 | end 56 | 57 | if errored_hosts.any? 58 | @hosts = errored_hosts.join(', ') 59 | output = output_per_server[errored_hosts.first] 60 | @message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}" 61 | @success = false 62 | end 63 | 64 | self 65 | end 66 | 67 | def or(message) 68 | @message = message 69 | self 70 | end 71 | 72 | def pass? 73 | @success 74 | end 75 | 76 | def message 77 | s = @message.dup 78 | s << " (#{@hosts})" if @hosts && @hosts.any? 79 | s 80 | end 81 | 82 | private 83 | 84 | def try(command, options) 85 | return unless @success # short-circuit evaluation 86 | configuration.run(command, options) do |ch,stream,out| 87 | warn "#{ch[:server]}: #{out}" if stream == :err 88 | yield ch, stream, out if block_given? 89 | end 90 | rescue Capistrano::CommandError => e 91 | @success = false 92 | @hosts = e.hosts.join(', ') 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Deploy 3 | module SCM 4 | def self.new(scm, config={}) 5 | scm_file = "capistrano/recipes/deploy/scm/#{scm}" 6 | require(scm_file) 7 | 8 | scm_const = scm.to_s.capitalize.gsub(/_(.)/) { $1.upcase } 9 | if const_defined?(scm_const) 10 | const_get(scm_const).new(config) 11 | else 12 | raise Capistrano::Error, "could not find `#{name}::#{scm_const}' in `#{scm_file}'" 13 | end 14 | rescue LoadError 15 | raise Capistrano::Error, "could not find any SCM named `#{scm}'" 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/bzr.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | 3 | module Capistrano 4 | module Deploy 5 | module SCM 6 | 7 | # Implements the Capistrano SCM interface for the Bazaar-NG revision 8 | # control system (http://bazaar-vcs.org/). 9 | class Bzr < Base 10 | # Sets the default command name for this SCM. Users may override this 11 | # by setting the :scm_command variable. 12 | default_command "bzr" 13 | 14 | # Bazaar-NG doesn't support any pseudo-id's, so we'll use the convention 15 | # in this adapter that the :head symbol means the most recently 16 | # committed revision. 17 | def head 18 | :head 19 | end 20 | 21 | # Returns the command that will check out the given revision to the 22 | # given destination. 23 | def checkout(revision, destination) 24 | scm :checkout, "--lightweight", revswitch(revision), repository, destination 25 | end 26 | 27 | # The bzr 'update' command does not support updating to a specific 28 | # revision, so this just does update, followed by revert (unless 29 | # updating to head). 30 | def sync(revision, destination) 31 | commands = [scm(:update, destination)] 32 | commands << [scm(:revert, revswitch(revision), destination)] if revision != head 33 | commands.join(" && ") 34 | end 35 | 36 | # The bzr 'export' does an export similar to other SCM systems 37 | def export(revision, destination) 38 | scm :export, revswitch(revision), destination, repository 39 | end 40 | 41 | # The bzr "diff" command doesn't accept a repository argument, so it 42 | # must be run from within a working tree. 43 | def diff(from, to=nil) 44 | switch = "-r#{from}" 45 | switch << "..#{to}" if to 46 | 47 | scm :diff, switch 48 | end 49 | 50 | # Returns a log of changes between the two revisions (inclusive). 51 | def log(from, to=nil) 52 | scm :log, "--short", "-r#{from}..#{to}", repository 53 | end 54 | 55 | # Attempts to translate the given revision identifier to a "real" 56 | # revision. If the identifier is :head, the "bzr revno" command will 57 | # be yielded, and the block must execute the command and return the 58 | # output. The revision will be extracted from the output and returned. 59 | # If the 'revision' argument, on the other hand, is not :head, it is 60 | # simply returned. 61 | def query_revision(revision) 62 | revision 63 | end 64 | 65 | # Increments the given revision number and returns it. 66 | def next_revision(revision) 67 | revision.to_i + 1 68 | end 69 | 70 | private 71 | 72 | def revswitch(revision) 73 | if revision == :head || revision.nil? 74 | nil 75 | else 76 | "-r #{revision}" 77 | end 78 | end 79 | end 80 | 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/cvs.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | 3 | module Capistrano 4 | module Deploy 5 | module SCM 6 | 7 | # Implements the Capistrano SCM interface for the CVS revision 8 | # control system. 9 | class Cvs < Base 10 | # Sets the default command name for this SCM. Users may override this 11 | # by setting the :scm_command variable. 12 | default_command "cvs" 13 | 14 | # CVS understands 'HEAD' to refer to the latest revision in the 15 | # repository. 16 | def head 17 | "HEAD" 18 | end 19 | 20 | # Returns the command that will check out the given revision to the 21 | # given destination. 22 | def checkout(revision, destination) 23 | [ prep_destination(destination), 24 | scm(verbose, cvs_root, :checkout, cvs_revision(revision), cvs_destination(destination), variable(:scm_module)) 25 | ].join(' && ') 26 | end 27 | 28 | # Returns the command that will do an "cvs update" to the given 29 | # revision, for the working copy at the given destination. 30 | def sync(revision, destination) 31 | [ prep_destination(destination), 32 | scm(verbose, cvs_root, :update, cvs_revision(revision), cvs_destination(destination)) 33 | ].join(' && ') 34 | end 35 | 36 | # Returns the command that will do an "cvs export" of the given revision 37 | # to the given destination. 38 | def export(revision, destination) 39 | [ prep_destination(destination), 40 | scm(verbose, cvs_root, :export, cvs_revision(revision), cvs_destination(destination), variable(:scm_module)) 41 | ].join(' && ') 42 | end 43 | 44 | # Returns the command that will do an "cvs diff" for the two revisions. 45 | def diff(from, to=nil) 46 | rev_type = revision_type(from) 47 | if rev_type == :date 48 | range_args = "-D '#{from}' -D '#{to || 'now'}'" 49 | else 50 | range_args = "-r '#{from}' -r '#{to || head}'" 51 | end 52 | scm cvs_root, :diff, range_args 53 | end 54 | 55 | # Returns an "cvs log" command for the two revisions. 56 | def log(from, to=nil) 57 | rev_type = revision_type(from) 58 | if rev_type == :date 59 | range_arg = "-d '#{from}<#{to || 'now'}'" 60 | else 61 | range_arg = "-r '#{from}:#{to || head}'" 62 | end 63 | scm cvs_root, :log, range_arg 64 | end 65 | 66 | # Unfortunately, cvs doesn't support the concept of a revision number like 67 | # subversion and other SCM's do. For now, we'll rely on getting the timestamp 68 | # of the latest checkin under the revision that's passed to us. 69 | def query_revision(revision) 70 | return revision if revision_type(revision) == :date 71 | revision = yield(scm(cvs_root, :log, "-r#{revision}")). 72 | grep(/^date:/). 73 | map { |line| line[/^date: (.*?);/, 1] }. 74 | sort.last 75 | return revision 76 | end 77 | 78 | # Determines what the response should be for a particular bit of text 79 | # from the SCM. Password prompts, connection requests, passphrases, 80 | # etc. are handled here. 81 | def handle_data(state, stream, text) 82 | logger.info "[#{stream}] #{text}" 83 | case text 84 | when /\bpassword.*:/i 85 | # prompting for a password 86 | "#{variable(:scm_password) || variable(:password)}\n" 87 | when %r{\(yes/no\)} 88 | # let's be agreeable... 89 | "yes\n" 90 | end 91 | end 92 | 93 | private 94 | 95 | # Constructs the CVSROOT command-line option 96 | def cvs_root 97 | root = "" 98 | root << "-d #{repository} " if repository 99 | root 100 | end 101 | 102 | # Constructs the destination dir command-line option 103 | def cvs_destination(destination) 104 | dest = "" 105 | if destination 106 | dest_parts = destination.split(/\//); 107 | dest << "-d #{dest_parts.pop}" 108 | end 109 | dest 110 | end 111 | 112 | # attempts to guess what type of revision we're working with 113 | def revision_type(rev) 114 | return :date if rev =~ /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/ # i.e 2007-05-15 08:13:25 115 | return :revision if rev =~ /^\d/ # i.e. 1.2.1 116 | return :tag # i.e. RELEASE_1_2 117 | end 118 | 119 | # constructs the appropriate command-line switch for specifying a 120 | # "revision" in CVS. This could be a tag, branch, revision (i.e. 1.3) 121 | # or a date (to be used with -d) 122 | def cvs_revision(rev) 123 | revision = "" 124 | revision << case revision_type(rev) 125 | when :date: 126 | "-D \"#{rev}\"" if revision_type(rev) == :date 127 | when :revision: 128 | "-r #{rev}" 129 | else 130 | "-r #{head}" 131 | end 132 | return revision 133 | end 134 | 135 | # If verbose output is requested, return nil, otherwise return the 136 | # command-line switch for "quiet" ("-Q"). 137 | def verbose 138 | variable(:scm_verbose) ? nil : "-Q" 139 | end 140 | 141 | def prep_destination(destination) 142 | dest_parts = destination.split(/\//); 143 | checkout_dir = dest_parts.pop 144 | dest = dest_parts.join('/') 145 | "mkdir -p #{ dest } && cd #{ dest }" 146 | end 147 | end 148 | 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/darcs.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | 3 | module Capistrano 4 | module Deploy 5 | module SCM 6 | 7 | # Implements the Capistrano SCM interface for the darcs revision 8 | # control system (http://www.abridgegame.org/darcs/). 9 | class Darcs < Base 10 | # Sets the default command name for this SCM. Users may override this 11 | # by setting the :scm_command variable. 12 | default_command "darcs" 13 | 14 | # Because darcs does not have any support for pseudo-ids, we'll just 15 | # return something here that we can use in the helpers below for 16 | # determining whether we need to look up the latest revision. 17 | def head 18 | :head 19 | end 20 | 21 | # Returns the command that will check out the given revision to the 22 | # given destination. The 'revision' parameter must be the 'hash' value 23 | # for the revision in question, as given by 'darcs changes --xml-output'. 24 | def checkout(revision, destination) 25 | scm :get, verbose, "--repo-name=#{destination}", "--to-match='hash #{revision}'", repository 26 | end 27 | 28 | # Tries to update the destination repository in-place, to bring it up 29 | # to the given revision. Note that because darcs' "pull" operation 30 | # does not support a "to-match" argument (or similar), this basically 31 | # nukes the destination directory and re-gets it. 32 | def sync(revision, destination) 33 | ["rm -rf #{destination}", checkout(revision, destination)].join(" && ") 34 | end 35 | 36 | # Darcs does not have a real 'export' option; there is 'darcs dist', 37 | # but that presupposes a utility that can untar and ungzip the dist 38 | # file. We'll cheat and just do a get, followed by a deletion of the 39 | # _darcs metadata directory. 40 | def export(revision, destination) 41 | [checkout(revision, destination), "rm -rf #{destination}/_darcs"].join(" && ") 42 | end 43 | 44 | # Returns the command that will do a "darcs diff" for the two revisions. 45 | # Each revision must be the 'hash' identifier of a darcs revision. 46 | def diff(from, to=nil) 47 | scm :diff, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'" 48 | end 49 | 50 | # Returns the log of changes between the two revisions. Each revision 51 | # must be the 'hash' identifier of a darcs revision. 52 | def log(from, to=nil) 53 | scm :changes, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'", "--repo=#{repository}" 54 | end 55 | 56 | # Attempts to translate the given revision identifier to a "real" 57 | # revision. If the identifier is a symbol, it is assumed to be a 58 | # pseudo-id. Otherwise, it will be immediately returned. If it is a 59 | # pseudo-id, a set of commands to execute will be yielded, and the 60 | # result of executing those commands must be returned by the block. 61 | # This method will then extract the actual revision hash from the 62 | # returned data. 63 | def query_revision(revision) 64 | case revision 65 | when :head 66 | xml = yield(scm(:changes, "--last 1", "--xml-output", "--repo=#{repository}")) 67 | return xml[/hash='(.*?)'/, 1] 68 | else return revision 69 | end 70 | end 71 | 72 | private 73 | 74 | def verbose 75 | case variable(:scm_verbose) 76 | when nil then "-q" 77 | when false then nil 78 | else "-v" 79 | end 80 | end 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/mercurial.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2007 Matthew Elder 2 | # based on work by Tobias Luetke 3 | 4 | require 'capistrano/recipes/deploy/scm/base' 5 | 6 | module Capistrano 7 | module Deploy 8 | module SCM 9 | 10 | # Implements the Capistrano SCM interface for the Mercurial revision 11 | # control system (http://www.selenic.com/mercurial/). 12 | # Latest updates at http://tackletechnology.org/oss/cap2-mercurial 13 | class Mercurial < Base 14 | # Sets the default command name for this SCM. Users may override this 15 | # by setting the :scm_command variable. 16 | default_command "hg" 17 | 18 | # For mercurial HEAD == tip except that it bases this assumption on what 19 | # tip is in the current repository (so push before you deploy) 20 | def head 21 | "tip" 22 | end 23 | 24 | # Clone the repository and update to the specified changeset. 25 | def checkout(changeset, destination) 26 | clone(destination) + " && " + update(changeset, destination) 27 | end 28 | 29 | # Pull from the repository and update to the specified changeset. 30 | def sync(changeset, destination) 31 | pull(destination) + " && " + update(changeset, destination) 32 | end 33 | 34 | # One day we will have hg archive, although i think its not needed 35 | def export(revision, destination) 36 | raise NotImplementedError, "`diff' is not implemented by #{self.class.name}" + 37 | "use checkout strategy" 38 | end 39 | 40 | # Compute the difference between the two changesets +from+ and +to+ 41 | # as a unified diff. 42 | def diff(from, to=nil) 43 | scm :diff, 44 | "--rev #{from}", 45 | (to ? "--rev #{to}" : nil) 46 | end 47 | 48 | # Return a log of all changes between the two specified changesets, 49 | # +from+ and +to+, inclusive or the log for +from+ if +to+ is omitted. 50 | def log(from, to=nil) 51 | scm :log, 52 | verbose, 53 | "--rev #{from}" + 54 | (to ? ":#{to}" : "") 55 | end 56 | 57 | # Translates a tag to a changeset if needed or just returns changeset. 58 | def query_revision(changeset) 59 | cmd = scm :log, 60 | verbose, 61 | "-r #{changeset}", 62 | "--template '{node|short}'" 63 | yield cmd 64 | end 65 | 66 | # Determine response for SCM prompts 67 | # user/pass can come from ssh and http distribution methods 68 | # yes/no is for when ssh asks you about fingerprints 69 | def handle_data(state, stream, text) 70 | logger.info "[#{stream}] #{text}" 71 | case text 72 | when /^user:/mi 73 | # support :scm_user for backwards compatibility of this module 74 | if user = variable(:scm_username) || variable(:scm_user) 75 | "#{user}\n" 76 | else 77 | raise "No variable :scm_username specified and Mercurial asked!\n" + 78 | "Prompt was: #{text}" 79 | end 80 | when /\bpassword:/mi 81 | unless pass = scm_password_or_prompt 82 | # fall back on old behavior of erroring out with msg 83 | raise "No variable :scm_password specified and Mercurial asked!\n" + 84 | "Prompt was: #{text}" 85 | end 86 | "#{pass}\n" 87 | when /yes\/no/i 88 | "yes\n" 89 | end 90 | end 91 | 92 | private 93 | 94 | # Fine grained mercurial commands 95 | def clone(destination) 96 | scm :clone, 97 | verbose, 98 | "--noupdate", # do not update to tip when cloning is done 99 | repository, # clone which repository? 100 | destination # and put the clone where? 101 | end 102 | 103 | def pull(destination) 104 | scm :pull, 105 | verbose, 106 | "--repository #{destination}", # pull changes into what? 107 | repository # and pull the changes from? 108 | end 109 | 110 | def update(changeset, destination) 111 | scm :update, 112 | verbose, 113 | "--repository #{destination}", # update what? 114 | "--clean", # ignore untracked changes 115 | changeset # update to this changeset 116 | end 117 | 118 | # verbosity configuration grokking :) 119 | def verbose 120 | case variable(:scm_verbose) 121 | when nil: nil 122 | when false: "--quiet" 123 | else "--verbose" 124 | end 125 | end 126 | 127 | # honor Cap 2.1+'s :scm_prefer_prompt if present 128 | def scm_password_or_prompt 129 | @scm_password_or_prompt ||= variable(:scm_password) || 130 | (Capistrano::CLI.password_prompt("hg password: ") if variable(:scm_prefer_prompt)) 131 | end 132 | 133 | end 134 | end 135 | end 136 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/none.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | 3 | module Capistrano 4 | module Deploy 5 | module SCM 6 | 7 | # A trivial SCM wrapper for representing the current working directory 8 | # as a repository. Obviously, not all operations are available for this 9 | # SCM, but it works sufficiently for use with the "copy" deployment 10 | # strategy. 11 | # 12 | # Use of this module is _not_ recommended; in general, it is good 13 | # practice to use some kind of source code management even for anything 14 | # you are wanting to deploy. However, this module is provided in 15 | # acknowledgement of the cases where trivial deployment of your current 16 | # working directory is desired. 17 | # 18 | # set :repository, "." 19 | # set :scm, :none 20 | # set :deploy_via, :copy 21 | class None < Base 22 | # No versioning, thus, no head. Returns the empty string. 23 | def head 24 | "" 25 | end 26 | 27 | # Simply does a copy from the :repository directory to the 28 | # :destination directory. 29 | def checkout(revision, destination) 30 | "cp -R #{repository} #{destination}" 31 | end 32 | 33 | alias_method :export, :checkout 34 | 35 | # No versioning, so this just returns the argument, with no 36 | # modification. 37 | def query_revision(revision) 38 | revision 39 | end 40 | end 41 | 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/perforce.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | 3 | # Notes: 4 | # no global verbose flag for scm_verbose 5 | # sync, checkout and export are just sync in p4 6 | # 7 | module Capistrano 8 | module Deploy 9 | module SCM 10 | 11 | # Implements the Capistrano SCM interface for the Perforce revision 12 | # control system (http://www.perforce.com). 13 | class Perforce < Base 14 | # Sets the default command name for this SCM. Users may override this 15 | # by setting the :scm_command variable. 16 | default_command "p4" 17 | 18 | # Perforce understands '#head' to refer to the latest revision in the 19 | # depot. 20 | def head 21 | 'head' 22 | end 23 | 24 | # Returns the command that will sync the given revision to the given 25 | # destination directory. The perforce client has a fixed destination so 26 | # the files must be copied from there to their intended resting place. 27 | def checkout(revision, destination) 28 | p4_sync(revision, destination, p4sync_flags) 29 | end 30 | 31 | # Returns the command that will sync the given revision to the given 32 | # destination directory. The perforce client has a fixed destination so 33 | # the files must be copied from there to their intended resting place. 34 | def sync(revision, destination) 35 | p4_sync(revision, destination, p4sync_flags) 36 | end 37 | 38 | # Returns the command that will sync the given revision to the given 39 | # destination directory. The perforce client has a fixed destination so 40 | # the files must be copied from there to their intended resting place. 41 | def export(revision, destination) 42 | p4_sync(revision, destination, p4sync_flags) 43 | end 44 | 45 | # Returns the command that will do an "p4 diff2" for the two revisions. 46 | def diff(from, to=head) 47 | scm authentication, :diff2, "-u -db", "//#{p4client}/...#{rev_no(from)}", "//#{p4client}/...#{rev_no(to)}" 48 | end 49 | 50 | # Returns a "p4 changes" command for the two revisions. 51 | def log(from=1, to=head) 52 | scm authentication, :changes, "-s submitted", "//#{p4client}/...#{rev_no(from)},#(rev_no(to)}" 53 | end 54 | 55 | def query_revision(revision) 56 | return revision if revision.to_s =~ /^\d+$/ 57 | command = scm(authentication, :changes, "-s submitted", "-m 1", "//#{p4client}/...#{rev_no(revision)}") 58 | yield(command)[/Change (\d+) on/, 1] 59 | end 60 | 61 | # Determines what the response should be for a particular bit of text 62 | # from the SCM. Password prompts, connection requests, passphrases, 63 | # etc. are handled here. 64 | def handle_data(state, stream, text) 65 | case text 66 | when /\(P4PASSWD\) invalid or unset\./i 67 | raise Capistrano::Error, "scm_password (or p4passwd) is incorrect or unset" 68 | when /Can.t create a new user.*/i 69 | raise Capistrano::Error, "scm_username (or p4user) is incorrect or unset" 70 | when /Perforce client error\:/i 71 | raise Capistrano::Error, "p4port is incorrect or unset" 72 | when /Client \'[\w\-\_\.]+\' unknown.*/i 73 | raise Capistrano::Error, "p4client is incorrect or unset" 74 | end 75 | end 76 | 77 | private 78 | 79 | # Builds the set of authentication switches that perforce understands. 80 | def authentication 81 | [ p4port && "-p #{p4port}", 82 | p4user && "-u #{p4user}", 83 | p4passwd && "-P #{p4passwd}", 84 | p4client && "-c #{p4client}" ].compact.join(" ") 85 | end 86 | 87 | # Returns the command that will sync the given revision to the given 88 | # destination directory with specific options. The perforce client has 89 | # a fixed destination so the files must be copied from there to their 90 | # intended resting place. 91 | def p4_sync(revision, destination, options="") 92 | scm authentication, :sync, options, "#{rev_no(revision)}", "&& cp -rf #{p4client_root} #{destination}" 93 | end 94 | 95 | def p4client 96 | variable(:p4client) 97 | end 98 | 99 | def p4port 100 | variable(:p4port) 101 | end 102 | 103 | def p4user 104 | variable(:p4user) || variable(:scm_username) 105 | end 106 | 107 | def p4passwd 108 | variable(:p4passwd) || variable(:scm_password) 109 | end 110 | 111 | def p4sync_flags 112 | variable(:p4sync_flags) || "-f" 113 | end 114 | 115 | def p4client_root 116 | variable(:p4client_root) || "`#{command} #{authentication} client -o | grep ^Root | cut -f2`" 117 | end 118 | 119 | def rev_no(revision) 120 | case revision.to_s 121 | when "head" 122 | "#head" 123 | when /^\d+/ 124 | "@#{revision}" 125 | else 126 | revision 127 | end 128 | end 129 | end 130 | 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/scm/subversion.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/scm/base' 2 | require 'yaml' 3 | 4 | module Capistrano 5 | module Deploy 6 | module SCM 7 | 8 | # Implements the Capistrano SCM interface for the Subversion revision 9 | # control system (http://subversion.tigris.org). 10 | class Subversion < Base 11 | # Sets the default command name for this SCM. Users may override this 12 | # by setting the :scm_command variable. 13 | default_command "svn" 14 | 15 | # Subversion understands 'HEAD' to refer to the latest revision in the 16 | # repository. 17 | def head 18 | "HEAD" 19 | end 20 | 21 | # Returns the command that will check out the given revision to the 22 | # given destination. 23 | def checkout(revision, destination) 24 | scm :checkout, verbose, authentication, "-r#{revision}", repository, destination 25 | end 26 | 27 | # Returns the command that will do an "svn update" to the given 28 | # revision, for the working copy at the given destination. 29 | def sync(revision, destination) 30 | scm :update, verbose, authentication, "-r#{revision}", destination 31 | end 32 | 33 | # Returns the command that will do an "svn export" of the given revision 34 | # to the given destination. 35 | def export(revision, destination) 36 | scm :export, verbose, authentication, "-r#{revision}", repository, destination 37 | end 38 | 39 | # Returns the command that will do an "svn diff" for the two revisions. 40 | def diff(from, to=nil) 41 | scm :diff, repository, authentication, "-r#{from}:#{to || head}" 42 | end 43 | 44 | # Returns an "svn log" command for the two revisions. 45 | def log(from, to=nil) 46 | scm :log, repository, authentication, "-r#{from}:#{to || head}" 47 | end 48 | 49 | # Attempts to translate the given revision identifier to a "real" 50 | # revision. If the identifier is an integer, it will simply be returned. 51 | # Otherwise, this will yield a string of the commands it needs to be 52 | # executed (svn info), and will extract the revision from the response. 53 | def query_revision(revision) 54 | return revision if revision =~ /^\d+$/ 55 | command = scm(:info, repository, authentication, "-r#{revision}") 56 | result = yield(command) 57 | yaml = YAML.load(result) 58 | raise "tried to run `#{command}' and got unexpected result #{result.inspect}" unless Hash === yaml 59 | yaml['Last Changed Rev'] || yaml['Revision'] 60 | end 61 | 62 | # Increments the given revision number and returns it. 63 | def next_revision(revision) 64 | revision.to_i + 1 65 | end 66 | 67 | # Determines what the response should be for a particular bit of text 68 | # from the SCM. Password prompts, connection requests, passphrases, 69 | # etc. are handled here. 70 | def handle_data(state, stream, text) 71 | logger.info "[#{stream}] #{text}" 72 | case text 73 | when /\bpassword.*:/i 74 | # subversion is prompting for a password 75 | "#{scm_password_prompt}\n" 76 | when %r{\(yes/no\)} 77 | # subversion is asking whether or not to connect 78 | "yes\n" 79 | when /passphrase/i 80 | # subversion is asking for the passphrase for the user's key 81 | "#{variable(:scm_passphrase)}\n" 82 | when /The entry \'(.+?)\' is no longer a directory/ 83 | raise Capistrano::Error, "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore." 84 | when /accept \(t\)emporarily/ 85 | # subversion is asking whether to accept the certificate 86 | "t\n" 87 | end 88 | end 89 | 90 | private 91 | 92 | # If a username is configured for the SCM, return the command-line 93 | # switches for that. Note that we don't need to return the password 94 | # switch, since Capistrano will check for that prompt in the output 95 | # and will respond appropriately. 96 | def authentication 97 | username = variable(:scm_username) 98 | return "" unless username 99 | result = "--username #{variable(:scm_username)} " 100 | result << "--password #{variable(:scm_password)} " unless variable(:scm_prefer_prompt) 101 | result << "--no-auth-cache " unless variable(:scm_auth_cache) 102 | result 103 | end 104 | 105 | # If verbose output is requested, return nil, otherwise return the 106 | # command-line switch for "quiet" ("-q"). 107 | def verbose 108 | variable(:scm_verbose) ? nil : "-q" 109 | end 110 | 111 | def scm_password_prompt 112 | @scm_password_prompt ||= variable(:scm_password) || 113 | variable(:password) || 114 | Capistrano::CLI.password_prompt("Subversion password: ") 115 | end 116 | end 117 | 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Deploy 3 | module Strategy 4 | def self.new(strategy, config={}) 5 | strategy_file = "capistrano/recipes/deploy/strategy/#{strategy}" 6 | require(strategy_file) 7 | 8 | strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase } 9 | if const_defined?(strategy_const) 10 | const_get(strategy_const).new(config) 11 | else 12 | raise Capistrano::Error, "could not find `#{name}::#{strategy_const}' in `#{strategy_file}'" 13 | end 14 | rescue LoadError 15 | raise Capistrano::Error, "could not find any strategy named `#{strategy}'" 16 | end 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/base.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/dependencies' 2 | 3 | module Capistrano 4 | module Deploy 5 | module Strategy 6 | 7 | # This class defines the abstract interface for all Capistrano 8 | # deployment strategies. Subclasses must implement at least the 9 | # #deploy! method. 10 | class Base 11 | attr_reader :configuration 12 | 13 | # Instantiates a strategy with a reference to the given configuration. 14 | def initialize(config={}) 15 | @configuration = config 16 | end 17 | 18 | # Executes the necessary commands to deploy the revision of the source 19 | # code identified by the +revision+ variable. Additionally, this 20 | # should write the value of the +revision+ variable to a file called 21 | # REVISION, in the base of the deployed revision. This file is used by 22 | # other tasks, to perform diffs and such. 23 | def deploy! 24 | raise NotImplementedError, "`deploy!' is not implemented by #{self.class.name}" 25 | end 26 | 27 | # Performs a check on the remote hosts to determine whether everything 28 | # is setup such that a deploy could succeed. 29 | def check! 30 | Dependencies.new(configuration) do |d| 31 | d.remote.directory(configuration[:releases_path]).or("`#{configuration[:releases_path]}' does not exist. Please run `cap deploy:setup'.") 32 | d.remote.writable(configuration[:deploy_to]).or("You do not have permissions to write to `#{configuration[:deploy_to]}'.") 33 | d.remote.writable(configuration[:releases_path]).or("You do not have permissions to write to `#{configuration[:releases_path]}'.") 34 | end 35 | end 36 | 37 | protected 38 | 39 | # This is to allow helper methods like "run" and "put" to be more 40 | # easily accessible to strategy implementations. 41 | def method_missing(sym, *args, &block) 42 | if configuration.respond_to?(sym) 43 | configuration.send(sym, *args, &block) 44 | else 45 | super 46 | end 47 | end 48 | 49 | # A wrapper for Kernel#system that logs the command being executed. 50 | def system(*args) 51 | logger.trace "executing locally: #{args.join(' ')}" 52 | super 53 | end 54 | 55 | private 56 | 57 | def logger 58 | @logger ||= configuration[:logger] || Capistrano::Logger.new(:output => STDOUT) 59 | end 60 | 61 | # The revision to deploy. Must return a real revision identifier, 62 | # and not a pseudo-id. 63 | def revision 64 | configuration[:real_revision] 65 | end 66 | end 67 | 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/checkout.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/strategy/remote' 2 | 3 | module Capistrano 4 | module Deploy 5 | module Strategy 6 | 7 | # Implements the deployment strategy which does an SCM checkout on each 8 | # target host. This is the default deployment strategy for Capistrano. 9 | class Checkout < Remote 10 | protected 11 | 12 | # Returns the SCM's checkout command for the revision to deploy. 13 | def command 14 | @command ||= source.checkout(revision, configuration[:release_path]) 15 | end 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/export.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/strategy/remote' 2 | 3 | module Capistrano 4 | module Deploy 5 | module Strategy 6 | 7 | # Implements the deployment strategy which does an SCM export on each 8 | # target host. 9 | class Export < Remote 10 | protected 11 | 12 | # Returns the SCM's export command for the revision to deploy. 13 | def command 14 | @command ||= source.export(revision, configuration[:release_path]) 15 | end 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/remote.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/strategy/base' 2 | 3 | module Capistrano 4 | module Deploy 5 | module Strategy 6 | 7 | # An abstract superclass, which forms the base for all deployment 8 | # strategies which work by grabbing the code from the repository directly 9 | # from remote host. This includes deploying by checkout (the default), 10 | # and deploying by export. 11 | class Remote < Base 12 | # Executes the SCM command for this strategy and writes the REVISION 13 | # mark file to each host. 14 | def deploy! 15 | scm_run "#{command} && #{mark}" 16 | end 17 | 18 | def check! 19 | super.check do |d| 20 | d.remote.command(source.command) 21 | end 22 | end 23 | 24 | protected 25 | 26 | # Runs the given command, filtering output back through the 27 | # #handle_data filter of the SCM implementation. 28 | def scm_run(command) 29 | run(command) do |ch,stream,text| 30 | ch[:state] ||= {} 31 | output = source.handle_data(ch[:state], stream, text) 32 | ch.send_data(output) if output 33 | end 34 | end 35 | 36 | # An abstract method which must be overridden in subclasses, to 37 | # return the actual SCM command(s) which must be executed on each 38 | # target host in order to perform the deployment. 39 | def command 40 | raise NotImplementedError, "`command' is not implemented by #{self.class.name}" 41 | end 42 | 43 | # Returns the command which will write the identifier of the 44 | # revision being deployed to the REVISION file on each host. 45 | def mark 46 | "(echo #{revision} > #{configuration[:release_path]}/REVISION)" 47 | end 48 | end 49 | 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/strategy/remote_cache.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/recipes/deploy/strategy/remote' 2 | 3 | module Capistrano 4 | module Deploy 5 | module Strategy 6 | 7 | # Implements the deployment strategy that keeps a cached checkout of 8 | # the source code on each remote server. Each deploy simply updates the 9 | # cached checkout, and then does a copy from the cached copy to the 10 | # final deployment location. 11 | class RemoteCache < Remote 12 | # Executes the SCM command for this strategy and writes the REVISION 13 | # mark file to each host. 14 | def deploy! 15 | update_repository_cache 16 | copy_repository_cache 17 | end 18 | 19 | def check! 20 | super.check do |d| 21 | d.remote.writable(shared_path) 22 | end 23 | end 24 | 25 | private 26 | 27 | def repository_cache 28 | File.join(shared_path, configuration[:repository_cache] || "cached-copy") 29 | end 30 | 31 | def update_repository_cache 32 | logger.trace "updating the cached checkout on all servers" 33 | command = "if [ -d #{repository_cache} ]; then " + 34 | "#{source.sync(revision, repository_cache)}; " + 35 | "else #{source.checkout(revision, repository_cache)}; fi" 36 | scm_run(command) 37 | end 38 | 39 | def copy_repository_cache 40 | logger.trace "copying the cached version to #{configuration[:release_path]}" 41 | run "cp -RPp #{repository_cache} #{configuration[:release_path]} && #{mark}" 42 | end 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/deploy/templates/maintenance.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | System down for maintenance 10 | 11 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |

42 | The system is down for <%= reason ? reason : "maintenance" %> 43 | as of <%= Time.now.strftime("%H:%M %Z") %>. 44 |

45 |

46 | It'll be back <%= deadline ? deadline : "shortly" %>. 47 |

48 |
49 |
50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/standard.rb: -------------------------------------------------------------------------------- 1 | desc <<-DESC 2 | Invoke a single command on the remote servers. This is useful for performing \ 3 | one-off commands that may not require a full task to be written for them. \ 4 | Simply specify the command to execute via the COMMAND environment variable. \ 5 | To execute the command only on certain roles, specify the ROLES environment \ 6 | variable as a comma-delimited list of role names. Alternatively, you can \ 7 | specify the HOSTS environment variable as a comma-delimited list of hostnames \ 8 | to execute the task on those hosts, explicitly. Lastly, if you want to \ 9 | execute the command via sudo, specify a non-empty value for the SUDO \ 10 | environment variable. 11 | 12 | Sample usage: 13 | 14 | $ cap COMMAND=uptime HOSTS=foo.capistano.test invoke 15 | $ cap ROLES=app,web SUDO=1 COMMAND="tail -f /var/log/messages" invoke 16 | DESC 17 | task :invoke do 18 | command = ENV["COMMAND"] || "" 19 | abort "Please specify a command to execute on the remote servers (via the COMMAND environment variable)" if command.empty? 20 | method = ENV["SUDO"] ? :sudo : :run 21 | invoke_command(command, :via => method) 22 | end 23 | 24 | desc <<-DESC 25 | Begin an interactive Capistrano session. This gives you an interactive \ 26 | terminal from which to execute tasks and commands on all of your servers. \ 27 | (This is still an experimental feature, and is subject to change without \ 28 | notice!) 29 | 30 | Sample usage: 31 | 32 | $ cap shell 33 | DESC 34 | task :shell do 35 | require 'capistrano/shell' 36 | Capistrano::Shell.run(self) 37 | end -------------------------------------------------------------------------------- /lib/capistrano/recipes/templates/maintenance.rhtml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | System down for maintenance 10 | 11 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 |

42 | The system is down for <%= reason ? reason : "maintenance" %> 43 | as of <%= Time.now.strftime("%H:%M %Z") %>. 44 |

45 |

46 | It'll be back <%= deadline ? deadline : "shortly" %>. 47 |

48 |
49 |
50 |
51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /lib/capistrano/recipes/upgrade.rb: -------------------------------------------------------------------------------- 1 | # Tasks to aid the migration of an established Capistrano 1.x installation to 2 | # Capistrano 2.x. 3 | 4 | namespace :upgrade do 5 | desc <<-DESC 6 | Migrate from the revisions log to REVISION. Capistrano 1.x recorded each \ 7 | deployment to a revisions.log file. Capistrano 2.x is cleaner, and just \ 8 | puts a REVISION file in the root of the deployed revision. This task \ 9 | migrates from the revisions.log used in Capistrano 1.x, to the REVISION \ 10 | tag file used in Capistrano 2.x. It is non-destructive and may be safely \ 11 | run any number of times. 12 | DESC 13 | task :revisions, :except => { :no_release => true } do 14 | revisions = capture("cat #{deploy_to}/revisions.log") 15 | 16 | mapping = {} 17 | revisions.each do |line| 18 | revision, directory = line.chomp.split[-2,2] 19 | mapping[directory] = revision 20 | end 21 | 22 | commands = mapping.keys.map do |directory| 23 | "echo '.'; test -d #{directory} && echo '#{mapping[directory]}' > #{directory}/REVISION" 24 | end 25 | 26 | command = commands.join(";") 27 | 28 | run "cd #{releases_path}; #{command}; true" do |ch, stream, out| 29 | STDOUT.print(".") 30 | STDOUT.flush 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/capistrano/role.rb: -------------------------------------------------------------------------------- 1 | 2 | module Capistrano 3 | class Role 4 | include Enumerable 5 | 6 | def initialize(*list) 7 | @static_servers = [] 8 | @dynamic_servers = [] 9 | push(*list) 10 | end 11 | 12 | def each(&block) 13 | servers.each &block 14 | end 15 | 16 | def push(*list) 17 | options = list.last.is_a?(Hash) ? list.pop : {} 18 | list.each do |item| 19 | if item.respond_to?(:call) 20 | @dynamic_servers << DynamicServerList.new(item, options) 21 | else 22 | @static_servers << self.class.wrap_server(item, options) 23 | end 24 | end 25 | end 26 | alias_method :<<, :push 27 | 28 | def servers 29 | @static_servers + dynamic_servers 30 | end 31 | alias_method :to_ary, :servers 32 | 33 | def empty? 34 | servers.empty? 35 | end 36 | 37 | # Resets the cache, so that proc values may be recalculated. 38 | # There should be a command in Configuration::Roles to do this, 39 | # but I haven't needed it yet, and I'm not sure yet 40 | # what to call that command. Suggestions? 41 | def reset! 42 | @dynamic_servers.each { |item| item.reset! } 43 | end 44 | 45 | # Clears everything. I still thing this should be 'clear!', but that's not 46 | # the way Array does it. 47 | def clear 48 | @dynamic_servers.clear 49 | @static_servers.clear 50 | end 51 | 52 | # Mostly for documentation purposes. Doesn't seem to do anything. 53 | protected 54 | 55 | # This is the combination of a block, a hash of options, and a cached value. 56 | # It is protected because it is an implementation detail -- the original 57 | # implementation was two lists (blocks and cached results of calling them). 58 | class DynamicServerList 59 | def initialize (block, options) 60 | @block = block 61 | @options = options 62 | @cached = [] 63 | @is_cached = false 64 | end 65 | 66 | # Convert to a list of ServerDefinitions 67 | def to_ary 68 | unless @is_cached 69 | @cached = Role::wrap_list(@block.call(@options), @options) 70 | @is_cached = true 71 | end 72 | @cached 73 | end 74 | 75 | # Clear the cached value 76 | def reset! 77 | @cached.clear 78 | @is_cached = false 79 | end 80 | end 81 | 82 | # Attribute reader for the cached results of executing the blocks in turn 83 | def dynamic_servers 84 | @dynamic_servers.inject([]) { |list, item| list.concat item } 85 | end 86 | 87 | # Wraps a string in a ServerDefinition, if it isn't already. 88 | # This and wrap_list should probably go in ServerDefinition in some form. 89 | def self.wrap_server (item, options) 90 | item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options) 91 | end 92 | 93 | # Turns a list, or something resembling a list, into a properly-formatted 94 | # ServerDefinition list. Keep an eye on this one -- it's entirely too 95 | # magical for its own good. In particular, if ServerDefinition ever inherits 96 | # from Array, this will break. 97 | def self.wrap_list (*list) 98 | options = list.last.is_a?(Hash) ? list.pop : {} 99 | if list.length == 1 100 | if list.first.nil? 101 | return [] 102 | elsif list.first.is_a?(Array) 103 | list = list.first 104 | end 105 | end 106 | options.merge! list.pop if list.last.is_a?(Hash) 107 | list.map do |item| 108 | self.wrap_server item, options 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/capistrano/server_definition.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | class ServerDefinition 3 | include Comparable 4 | 5 | attr_reader :host 6 | attr_reader :user 7 | attr_reader :port 8 | attr_reader :options 9 | 10 | def initialize(string, options={}) 11 | @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3] 12 | 13 | @options = options.dup 14 | user_opt, port_opt = @options.delete(:user), @options.delete(:port) 15 | 16 | @user ||= user_opt 17 | @port ||= port_opt 18 | 19 | @port = @port.to_i if @port 20 | end 21 | 22 | def <=>(server) 23 | [host, port, user] <=> [server.host, server.port, server.user] 24 | end 25 | 26 | # Redefined, so that Array#uniq will work to remove duplicate server 27 | # definitions, based solely on their host names. 28 | def eql?(server) 29 | host == server.host && 30 | user == server.user && 31 | port == server.port 32 | end 33 | 34 | alias :== :eql? 35 | 36 | # Redefined, so that Array#uniq will work to remove duplicate server 37 | # definitions, based on their connection information. 38 | def hash 39 | @hash ||= [host, user, port].hash 40 | end 41 | 42 | def to_s 43 | @to_s ||= begin 44 | s = host 45 | s = "#{user}@#{s}" if user 46 | s = "#{s}:#{port}" if port && port != 22 47 | s 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /lib/capistrano/ssh.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rubygems' 3 | gem 'net-ssh', "< 1.99.0" 4 | rescue LoadError, NameError 5 | end 6 | 7 | require 'net/ssh' 8 | 9 | module Capistrano 10 | unless ENV['SKIP_VERSION_CHECK'] 11 | require 'capistrano/version' 12 | require 'net/ssh/version' 13 | ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY] 14 | if !Version.check(Version::SSH_REQUIRED, ssh_version) 15 | raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{Version::SSH_REQUIRED.join(".")}" 16 | end 17 | end 18 | 19 | # Now, Net::SSH is kind of silly, and tries to lazy-load everything. This 20 | # wreaks havoc with the parallel connection trick that Capistrano wants to 21 | # use, so we're going to do something hideously ugly here and force all the 22 | # files that Net::SSH uses to load RIGHT NOW, rather than lazily. 23 | 24 | net_ssh_dependencies = %w(connection/services connection/channel connection/driver 25 | service/agentforward/services service/agentforward/driver 26 | service/process/driver util/prompter 27 | service/forward/services service/forward/driver service/forward/local-network-handler service/forward/remote-network-handler 28 | service/shell/services service/shell/driver 29 | lenient-host-key-verifier 30 | transport/compress/services transport/compress/zlib-compressor transport/compress/none-compressor transport/compress/zlib-decompressor transport/compress/none-decompressor 31 | transport/kex/services transport/kex/dh transport/kex/dh-gex 32 | transport/ossl/services 33 | transport/ossl/hmac/services transport/ossl/hmac/sha1 transport/ossl/hmac/sha1-96 transport/ossl/hmac/md5 transport/ossl/hmac/md5-96 transport/ossl/hmac/none 34 | transport/ossl/cipher-factory transport/ossl/hmac-factory transport/ossl/buffer-factory transport/ossl/key-factory transport/ossl/digest-factory 35 | transport/identity-cipher transport/packet-stream transport/version-negotiator transport/algorithm-negotiator transport/session 36 | userauth/methods/services userauth/methods/password userauth/methods/keyboard-interactive userauth/methods/publickey userauth/methods/hostbased 37 | userauth/services userauth/agent userauth/userkeys userauth/driver 38 | transport/services service/services 39 | ) 40 | 41 | net_ssh_dependencies << "userauth/pageant" if File::ALT_SEPARATOR 42 | net_ssh_dependencies.each do |path| 43 | begin 44 | require "net/ssh/#{path}" 45 | rescue LoadError 46 | # Ignore load errors from this, since some files are in the list which 47 | # do not exist in different (supported) versions of Net::SSH. We know 48 | # (by this point) that Net::SSH is installed, though, since we do a 49 | # require 'net/ssh' at the very top of this file, and we know the 50 | # installed version meets the minimum version requirements because of 51 | # the version check, also at the top of this file. So, if we get a 52 | # LoadError, it's simply because the file in question does not exist in 53 | # the version of Net::SSH that is installed. 54 | # 55 | # Whew! 56 | end 57 | end 58 | 59 | # A helper class for dealing with SSH connections. 60 | class SSH 61 | # Patch an accessor onto an SSH connection so that we can record the server 62 | # definition object that defines the connection. This is useful because 63 | # the gateway returns connections whose "host" is 127.0.0.1, instead of 64 | # the host on the other side of the tunnel. 65 | module Server #:nodoc: 66 | def self.apply_to(connection, server) 67 | connection.extend(Server) 68 | connection.xserver = server 69 | connection 70 | end 71 | 72 | attr_accessor :xserver 73 | end 74 | 75 | # The default port for SSH. 76 | DEFAULT_PORT = 22 77 | 78 | # An abstraction to make it possible to connect to the server via public key 79 | # without prompting for the password. If the public key authentication fails 80 | # this will fall back to password authentication. 81 | # 82 | # +server+ must be an instance of ServerDefinition. 83 | # 84 | # If a block is given, the new session is yielded to it, otherwise the new 85 | # session is returned. 86 | # 87 | # If an :ssh_options key exists in +options+, it is passed to the Net::SSH 88 | # constructor. Values in +options+ are then merged into it, and any 89 | # connection information in +server+ is added last, so that +server+ info 90 | # takes precedence over +options+, which takes precendence over ssh_options. 91 | def self.connect(server, options={}, &block) 92 | methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ] 93 | password_value = nil 94 | 95 | ssh_options = (options[:ssh_options] || {}).dup 96 | ssh_options[:username] = server.user || options[:user] || ssh_options[:username] 97 | ssh_options[:port] = server.port || options[:port] || ssh_options[:port] || DEFAULT_PORT 98 | 99 | begin 100 | connection_options = ssh_options.merge( 101 | :password => password_value, 102 | :auth_methods => ssh_options[:auth_methods] || methods.shift 103 | ) 104 | 105 | connection = Net::SSH.start(server.host, connection_options, &block) 106 | Server.apply_to(connection, server) 107 | 108 | rescue Net::SSH::AuthenticationFailed 109 | raise if methods.empty? || ssh_options[:auth_methods] 110 | password_value = options[:password] 111 | retry 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/capistrano/task_definition.rb: -------------------------------------------------------------------------------- 1 | require 'capistrano/server_definition' 2 | 3 | module Capistrano 4 | # Represents the definition of a single task. 5 | class TaskDefinition 6 | attr_reader :name, :namespace, :options, :body, :desc, :on_error, :max_hosts 7 | 8 | def initialize(name, namespace, options={}, &block) 9 | @name, @namespace, @options = name, namespace, options 10 | @desc = @options.delete(:desc) 11 | @on_error = options.delete(:on_error) 12 | @max_hosts = options[:max_hosts] && options[:max_hosts].to_i 13 | @body = block or raise ArgumentError, "a task requires a block" 14 | @servers = nil 15 | end 16 | 17 | # Returns the task's fully-qualified name, including the namespace 18 | def fully_qualified_name 19 | @fully_qualified_name ||= begin 20 | if namespace.default_task == self 21 | namespace.fully_qualified_name 22 | else 23 | [namespace.fully_qualified_name, name].compact.join(":") 24 | end 25 | end 26 | end 27 | 28 | # Returns the description for this task, with newlines collapsed and 29 | # whitespace stripped. Returns the empty string if there is no 30 | # description for this task. 31 | def description(rebuild=false) 32 | @description = nil if rebuild 33 | @description ||= begin 34 | description = @desc || "" 35 | 36 | indentation = description[/\A\s+/] 37 | if indentation 38 | reformatted_description = "" 39 | description.strip.each_line do |line| 40 | line = line.chomp.sub(/^#{indentation}/, "") 41 | line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/] 42 | reformatted_description << line << "\n" 43 | end 44 | description = reformatted_description 45 | end 46 | 47 | description.strip.gsub(/\r\n/, "\n") 48 | end 49 | end 50 | 51 | # Returns the first sentence of the full description. If +max_length+ is 52 | # given, the result will be truncated if it is longer than +max_length+, 53 | # and an ellipsis appended. 54 | def brief_description(max_length=nil) 55 | brief = description[/^.*?\.(?=\s|$)/] || description 56 | 57 | if max_length && brief.length > max_length 58 | brief = brief[0,max_length-3] + "..." 59 | end 60 | 61 | brief 62 | end 63 | 64 | # Indicates whether the task wants to continue, even if a server has failed 65 | # previously 66 | def continue_on_error? 67 | @on_error == :continue 68 | end 69 | end 70 | end -------------------------------------------------------------------------------- /lib/capistrano/upload.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'rubygems' 3 | gem 'net-sftp', "< 1.99.0" 4 | rescue LoadError, NameError 5 | end 6 | 7 | require 'net/sftp' 8 | require 'net/sftp/operations/errors' 9 | require 'capistrano/errors' 10 | 11 | module Capistrano 12 | unless ENV['SKIP_VERSION_CHECK'] 13 | require 'capistrano/version' 14 | require 'net/sftp/version' 15 | sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY] 16 | required_version = [1,1,0] 17 | if !Capistrano::Version.check(required_version, sftp_version) 18 | raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used." 19 | end 20 | end 21 | 22 | # This class encapsulates a single file upload to be performed in parallel 23 | # across multiple machines, using the SFTP protocol. Although it is intended 24 | # to be used primarily from within Capistrano, it may also be used standalone 25 | # if you need to simply upload a file to multiple servers. 26 | # 27 | # Basic Usage: 28 | # 29 | # begin 30 | # uploader = Capistrano::Upload.new(sessions, "remote-file.txt", 31 | # :data => "the contents of the file to upload") 32 | # uploader.process! 33 | # rescue Capistrano::UploadError => e 34 | # warn "Could not upload the file: #{e.message}" 35 | # end 36 | class Upload 37 | def self.process(sessions, filename, options) 38 | new(sessions, filename, options).process! 39 | end 40 | 41 | attr_reader :sessions, :filename, :options 42 | attr_reader :failed, :completed 43 | 44 | # Creates and prepares a new Upload instance. The +sessions+ parameter 45 | # must be an array of open Net::SSH sessions. The +filename+ is the name 46 | # (including path) of the destination file on the remote server. The 47 | # +options+ hash accepts the following keys (as symbols): 48 | # 49 | # * data: required. Should refer to a String containing the contents of 50 | # the file to upload. 51 | # * mode: optional. The "mode" of the destination file. Defaults to 0664. 52 | # * logger: optional. Should point to a Capistrano::Logger instance, if 53 | # given. 54 | def initialize(sessions, filename, options) 55 | raise ArgumentError, "you must specify the data to upload via the :data option" unless options[:data] 56 | 57 | @sessions = sessions 58 | @filename = filename 59 | @options = options 60 | 61 | @completed = @failed = 0 62 | @sftps = setup_sftp 63 | end 64 | 65 | # Uploads to all specified servers in parallel. If any one of the servers 66 | # fails, an exception will be raised (UploadError). 67 | def process! 68 | logger.debug "uploading #{filename}" if logger 69 | while running? 70 | @sftps.each do |sftp| 71 | next if sftp.channel[:done] 72 | begin 73 | sftp.channel.connection.process(true) 74 | rescue Net::SFTP::Operations::StatusException => error 75 | logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger 76 | failed!(sftp) 77 | end 78 | end 79 | sleep 0.01 # a brief respite, to keep the CPU from going crazy 80 | end 81 | logger.trace "upload finished" if logger 82 | 83 | if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any? 84 | hosts = failed.map { |sftp| sftp.channel[:server] } 85 | error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}") 86 | error.hosts = hosts 87 | raise error 88 | end 89 | 90 | self 91 | end 92 | 93 | private 94 | 95 | def logger 96 | options[:logger] 97 | end 98 | 99 | def setup_sftp 100 | sessions.map do |session| 101 | server = session.xserver 102 | sftp = session.sftp 103 | sftp.connect unless sftp.state == :open 104 | 105 | sftp.channel[:server] = server 106 | sftp.channel[:done] = false 107 | sftp.channel[:failed] = false 108 | 109 | real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host) 110 | sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0664) do |status, handle| 111 | break unless check_status(sftp, "open #{real_filename}", server, status) 112 | 113 | logger.info "uploading data to #{server}:#{real_filename}" if logger 114 | sftp.write(handle, options[:data] || "") do |status| 115 | break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status) 116 | sftp.close_handle(handle) do 117 | logger.debug "done uploading data to #{server}:#{real_filename}" if logger 118 | completed!(sftp) 119 | end 120 | end 121 | end 122 | 123 | sftp 124 | end 125 | end 126 | 127 | def check_status(sftp, action, server, status) 128 | return true if status.code == Net::SFTP::Session::FX_OK 129 | 130 | logger.error "could not #{action} on #{server} (#{status.message})" if logger 131 | failed!(sftp) 132 | 133 | return false 134 | end 135 | 136 | def running? 137 | completed < @sftps.length 138 | end 139 | 140 | def failed!(sftp) 141 | completed!(sftp) 142 | @failed += 1 143 | sftp.channel[:failed] = true 144 | end 145 | 146 | def completed!(sftp) 147 | @completed += 1 148 | sftp.channel[:done] = true 149 | end 150 | end 151 | 152 | end 153 | -------------------------------------------------------------------------------- /lib/capistrano/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Version #:nodoc: 3 | # A method for comparing versions of required modules. It expects two 4 | # arrays of integers as parameters, the first being the minimum version 5 | # required, and the second being the actual version available. It returns 6 | # true if the actual version is at least equal to the required version. 7 | def self.check(required, actual) #:nodoc: 8 | required = required.map { |v| "%06d" % v }.join(".") 9 | actual = actual.map { |v| "%06d" % v }.join(".") 10 | return actual >= required 11 | end 12 | 13 | MAJOR = 2 14 | MINOR = 2 15 | TINY = 0 16 | 17 | STRING = [MAJOR, MINOR, TINY].join(".") 18 | 19 | SSH_REQUIRED = [1,0,10] 20 | SFTP_REQUIRED = [1,1,0] 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/cli/execute_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/cli/execute' 3 | 4 | class CLIExecuteTest < Test::Unit::TestCase 5 | class MockCLI 6 | attr_reader :options 7 | 8 | def initialize 9 | @options = {} 10 | end 11 | 12 | include Capistrano::CLI::Execute 13 | end 14 | 15 | def setup 16 | @cli = MockCLI.new 17 | @logger = stub_everything 18 | @config = stub(:logger => @logger) 19 | @config.stubs(:set) 20 | @config.stubs(:load) 21 | @config.stubs(:trigger) 22 | @cli.stubs(:instantiate_configuration).returns(@config) 23 | end 24 | 25 | def test_execute_should_set_logger_verbosity 26 | @cli.options[:verbose] = 7 27 | @logger.expects(:level=).with(7) 28 | @cli.execute! 29 | end 30 | 31 | def test_execute_should_set_password 32 | @cli.options[:password] = "nosoup4u" 33 | @config.expects(:set).with(:password, "nosoup4u") 34 | @cli.execute! 35 | end 36 | 37 | def test_execute_should_set_prevars_before_loading 38 | @config.expects(:load).never 39 | @config.expects(:set).with(:stage, "foobar") 40 | @config.expects(:load).with("standard") 41 | @cli.options[:pre_vars] = { :stage => "foobar" } 42 | @cli.execute! 43 | end 44 | 45 | def test_execute_should_load_sysconf_if_sysconf_set_and_exists 46 | @cli.options[:sysconf] = "/etc/capistrano.conf" 47 | @config.expects(:load).with("/etc/capistrano.conf") 48 | File.expects(:file?).with("/etc/capistrano.conf").returns(true) 49 | @cli.execute! 50 | end 51 | 52 | def test_execute_should_not_load_sysconf_when_sysconf_set_and_not_exists 53 | @cli.options[:sysconf] = "/etc/capistrano.conf" 54 | File.expects(:file?).with("/etc/capistrano.conf").returns(false) 55 | @cli.execute! 56 | end 57 | 58 | def test_execute_should_load_dotfile_if_dotfile_set_and_exists 59 | @cli.options[:dotfile] = "/home/jamis/.caprc" 60 | @config.expects(:load).with("/home/jamis/.caprc") 61 | File.expects(:file?).with("/home/jamis/.caprc").returns(true) 62 | @cli.execute! 63 | end 64 | 65 | def test_execute_should_not_load_dotfile_when_dotfile_set_and_not_exists 66 | @cli.options[:dotfile] = "/home/jamis/.caprc" 67 | File.expects(:file?).with("/home/jamis/.caprc").returns(false) 68 | @cli.execute! 69 | end 70 | 71 | def test_execute_should_load_recipes_when_recipes_are_given 72 | @cli.options[:recipes] = %w(config/deploy path/to/extra) 73 | @config.expects(:load).with("config/deploy") 74 | @config.expects(:load).with("path/to/extra") 75 | @cli.execute! 76 | end 77 | 78 | def test_execute_should_set_vars_and_execute_tasks 79 | @cli.options[:vars] = { :foo => "bar", :baz => "bang" } 80 | @cli.options[:actions] = %w(first second) 81 | @config.expects(:set).with(:foo, "bar") 82 | @config.expects(:set).with(:baz, "bang") 83 | @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish) 84 | @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish) 85 | @cli.execute! 86 | end 87 | 88 | def test_execute_should_call_load_and_exit_triggers 89 | @cli.options[:actions] = %w(first second) 90 | @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish) 91 | @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish) 92 | @config.expects(:trigger).never 93 | @config.expects(:trigger).with(:load) 94 | @config.expects(:trigger).with(:exit) 95 | @cli.execute! 96 | end 97 | 98 | def test_execute_should_call_handle_error_when_exceptions_occur 99 | @config.expects(:load).raises(Exception, "boom") 100 | @cli.expects(:handle_error).with { |e,| Exception === e } 101 | @cli.execute! 102 | end 103 | 104 | def test_execute_should_return_config_instance 105 | assert_equal @config, @cli.execute! 106 | end 107 | 108 | def test_instantiate_configuration_should_return_new_configuration_instance 109 | assert_instance_of Capistrano::Configuration, MockCLI.new.instantiate_configuration 110 | end 111 | 112 | def test_handle_error_with_auth_error_should_abort_with_message_including_user_name 113 | @cli.expects(:abort).with { |s| s.include?("jamis") } 114 | @cli.handle_error(Net::SSH::AuthenticationFailed.new("jamis")) 115 | end 116 | 117 | def test_handle_error_with_cap_error_should_abort_with_message 118 | @cli.expects(:abort).with("Wish you were here") 119 | @cli.handle_error(Capistrano::Error.new("Wish you were here")) 120 | end 121 | 122 | def test_handle_error_with_other_errors_should_reraise_error 123 | other_error = Class.new(RuntimeError) 124 | assert_raises(other_error) { @cli.handle_error(other_error.new("boom")) } 125 | end 126 | 127 | def test_class_execute_method_should_call_parse_and_execute_with_ARGV 128 | cli = mock(:execute! => nil) 129 | MockCLI.expects(:parse).with(ARGV).returns(cli) 130 | MockCLI.execute 131 | end 132 | end -------------------------------------------------------------------------------- /test/cli/help_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/cli/help' 3 | 4 | class CLIHelpTest < Test::Unit::TestCase 5 | class MockCLI 6 | attr_reader :options, :called_original 7 | 8 | def initialize 9 | @options = {} 10 | @called_original = false 11 | end 12 | 13 | def execute_requested_actions(config) 14 | @called_original = config 15 | end 16 | 17 | include Capistrano::CLI::Help 18 | end 19 | 20 | def setup 21 | @cli = MockCLI.new 22 | @cli.options[:verbose] = 0 23 | @ui = stub("ui", :output_cols => 80, :output_rows => 20, :page_at= => nil) 24 | MockCLI.stubs(:ui).returns(@ui) 25 | end 26 | 27 | def test_execute_requested_actions_without_tasks_or_explain_should_call_original 28 | @cli.execute_requested_actions(:config) 29 | @cli.expects(:task_list).never 30 | @cli.expects(:explain_task).never 31 | assert_equal :config, @cli.called_original 32 | end 33 | 34 | def test_execute_requested_actions_with_tasks_should_call_task_list 35 | @cli.options[:tasks] = true 36 | @cli.expects(:task_list).with(:config) 37 | @cli.expects(:explain_task).never 38 | @cli.execute_requested_actions(:config) 39 | assert !@cli.called_original 40 | end 41 | 42 | def test_execute_requested_actions_with_explain_should_call_explain_task 43 | @cli.options[:explain] = "deploy_with_niftiness" 44 | @cli.expects(:task_list).never 45 | @cli.expects(:explain_task).with(:config, "deploy_with_niftiness") 46 | @cli.execute_requested_actions(:config) 47 | assert !@cli.called_original 48 | end 49 | 50 | def test_task_list_with_no_tasks_should_emit_warning 51 | config = mock("config", :task_list => []) 52 | @cli.expects(:warn) 53 | @cli.task_list(config) 54 | end 55 | 56 | def test_task_list_should_query_all_tasks_in_all_namespaces 57 | expected_max_len = 80 - 3 - MockCLI::LINE_PADDING 58 | task_list = [task("c"), task("g", "c:g"), task("b", "c:b"), task("a")] 59 | task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name) } 60 | 61 | config = mock("config") 62 | config.expects(:task_list).with(:all).returns(task_list) 63 | @cli.stubs(:puts) 64 | @cli.task_list(config) 65 | end 66 | 67 | def test_task_list_should_never_use_less_than_MIN_MAX_LEN_chars_for_descriptions 68 | @ui.stubs(:output_cols).returns(20) 69 | t = task("c") 70 | t.expects(:brief_description).with(30).returns("hello") 71 | config = mock("config", :task_list => [t]) 72 | @cli.stubs(:puts) 73 | @cli.task_list(config) 74 | end 75 | 76 | def test_task_list_should_not_include_tasks_with_blank_description_or_internal_by_default 77 | t1 = task("c") 78 | t1.expects(:brief_description).returns("hello") 79 | t2 = task("d", "d", "[internal] howdy") 80 | t2.expects(:brief_description).never 81 | t3 = task("e", "e", "") 82 | t3.expects(:brief_description).never 83 | 84 | config = mock("config", :task_list => [t1, t2, t3]) 85 | @cli.stubs(:puts) 86 | @cli.expects(:puts).never.with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ } 87 | @cli.task_list(config) 88 | end 89 | 90 | def test_task_list_should_include_tasks_with_blank_descriptions_and_internal_when_verbose 91 | t1 = task("c") 92 | t1.expects(:brief_description).returns("hello") 93 | t2 = task("d", "d", "[internal] howdy") 94 | t2.expects(:brief_description).returns("[internal] howdy") 95 | t3 = task("e", "e", "") 96 | t3.expects(:brief_description).returns("") 97 | 98 | config = mock("config", :task_list => [t1, t2, t3]) 99 | @cli.options[:verbose] = 1 100 | @cli.stubs(:puts) 101 | @cli.expects(:puts).with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ }.at_least_once 102 | @cli.task_list(config) 103 | end 104 | 105 | def test_explain_task_should_warn_if_task_does_not_exist 106 | config = mock("config", :find_task => nil) 107 | @cli.expects(:warn).with { |s,| s =~ /`deploy_with_niftiness'/ } 108 | @cli.explain_task(config, "deploy_with_niftiness") 109 | end 110 | 111 | def test_explain_task_with_task_that_has_no_description_should_emit_stub 112 | t = mock("task", :description => "") 113 | config = mock("config") 114 | config.expects(:find_task).with("deploy_with_niftiness").returns(t) 115 | @cli.stubs(:puts) 116 | @cli.expects(:puts).with("There is no description for this task.") 117 | @cli.explain_task(config, "deploy_with_niftiness") 118 | end 119 | 120 | def test_explain_task_with_task_should_format_description 121 | t = stub("task", :description => "line1\nline2\n\nline3") 122 | config = mock("config", :find_task => t) 123 | @cli.stubs(:puts) 124 | @cli.explain_task(config, "deploy_with_niftiness") 125 | end 126 | 127 | def test_long_help_should_load_and_format_help_txt_file 128 | File.expects(:dirname).returns "a/b/c" 129 | File.expects(:read).with("a/b/c/help.txt").returns("text") 130 | @ui.expects(:say).with("text\n") 131 | @cli.long_help 132 | end 133 | 134 | private 135 | 136 | def task(name, fqn=name, desc="a description") 137 | stub("task", :name => name, :fully_qualified_name => fqn, :description => desc) 138 | end 139 | end -------------------------------------------------------------------------------- /test/cli/ui_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/cli/ui' 3 | 4 | class CLIUITest < Test::Unit::TestCase 5 | class MockCLI 6 | include Capistrano::CLI::UI 7 | end 8 | 9 | def test_ui_should_return_highline_instance 10 | assert_instance_of HighLine, MockCLI.ui 11 | end 12 | 13 | def test_password_prompt_should_have_default_prompt_and_set_echo_false 14 | q = mock("question") 15 | q.expects(:echo=).with(false) 16 | ui = mock("ui") 17 | ui.expects(:ask).with("Password: ").yields(q).returns("sayuncle") 18 | MockCLI.expects(:ui).returns(ui) 19 | assert_equal "sayuncle", MockCLI.password_prompt 20 | end 21 | 22 | def test_password_prompt_with_custom_prompt_should_use_custom_prompt 23 | ui = mock("ui") 24 | ui.expects(:ask).with("Give the passphrase: ").returns("sayuncle") 25 | MockCLI.expects(:ui).returns(ui) 26 | assert_equal "sayuncle", MockCLI.password_prompt("Give the passphrase: ") 27 | end 28 | end -------------------------------------------------------------------------------- /test/cli_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/cli' 3 | 4 | class CLI_Test < Test::Unit::TestCase 5 | def test_options_ui_and_help_modules_should_integrate_successfully_with_configuration 6 | cli = Capistrano::CLI.parse(%w(-T)) 7 | cli.expects(:puts).at_least_once 8 | cli.execute! 9 | end 10 | 11 | def test_options_and_execute_modules_should_integrate_successfully_with_configuration 12 | path = "#{File.dirname(__FILE__)}/fixtures/cli_integration.rb" 13 | cli = Capistrano::CLI.parse(%W(-q -f #{path} testing)) 14 | config = cli.execute! 15 | assert config[:testing_occurred] 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/configuration/actions/file_transfer_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/configuration/actions/file_transfer' 3 | 4 | class ConfigurationActionsFileTransferTest < Test::Unit::TestCase 5 | class MockConfig 6 | include Capistrano::Configuration::Actions::FileTransfer 7 | end 8 | 9 | def setup 10 | @config = MockConfig.new 11 | @config.stubs(:logger).returns(stub_everything) 12 | end 13 | 14 | def test_put_should_pass_options_to_execute_on_servers 15 | @config.expects(:execute_on_servers).with(:foo => "bar") 16 | @config.put("some data", "test.txt", :foo => "bar") 17 | end 18 | 19 | def test_put_should_delegate_to_Upload_process 20 | @config.expects(:execute_on_servers).yields(%w(s1 s2 s3).map { |s| mock(:host => s) }) 21 | @config.expects(:sessions).times(3).returns(Hash.new{|h,k| h[k] = k.host.to_sym}) 22 | Capistrano::Upload.expects(:process).with([:s1,:s2,:s3], "test.txt", :data => "some data", :mode => 0777, :logger => @config.logger) 23 | @config.put("some data", "test.txt", :mode => 0777) 24 | end 25 | 26 | def test_get_should_pass_options_execute_on_servers_including_once 27 | @config.expects(:execute_on_servers).with(:foo => "bar", :once => true) 28 | @config.get("test.txt", "test.txt", :foo => "bar") 29 | end 30 | 31 | def test_get_should_use_sftp_get_file_to_local_path 32 | sftp = mock("sftp", :state => :closed, :connect => true) 33 | sftp.expects(:get_file).with("remote.txt", "local.txt") 34 | 35 | s = server("capistrano") 36 | @config.expects(:execute_on_servers).yields([s]) 37 | @config.expects(:sessions).returns(s => mock("session", :sftp => sftp)) 38 | @config.get("remote.txt", "local.txt") 39 | end 40 | end -------------------------------------------------------------------------------- /test/configuration/actions/inspect_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/configuration/actions/inspect' 3 | 4 | class ConfigurationActionsInspectTest < Test::Unit::TestCase 5 | class MockConfig 6 | include Capistrano::Configuration::Actions::Inspect 7 | end 8 | 9 | def setup 10 | @config = MockConfig.new 11 | @config.stubs(:logger).returns(stub_everything) 12 | end 13 | 14 | def test_stream_should_pass_options_through_to_run 15 | @config.expects(:invoke_command).with("tail -f foo.log", :once => true) 16 | @config.stream("tail -f foo.log", :once => true) 17 | end 18 | 19 | def test_stream_should_emit_stdout_via_puts 20 | @config.expects(:invoke_command).yields(mock("channel"), :out, "something streamed") 21 | @config.expects(:puts).with("something streamed") 22 | @config.expects(:warn).never 23 | @config.stream("tail -f foo.log") 24 | end 25 | 26 | def test_stream_should_emit_stderr_via_warn 27 | ch = mock("channel") 28 | ch.expects(:[]).with(:server).returns(server("capistrano")) 29 | @config.expects(:invoke_command).yields(ch, :err, "something streamed") 30 | @config.expects(:puts).never 31 | @config.expects(:warn).with("[err :: capistrano] something streamed") 32 | @config.stream("tail -f foo.log") 33 | end 34 | 35 | def test_capture_should_pass_options_merged_with_once_to_run 36 | @config.expects(:invoke_command).with("hostname", :foo => "bar", :once => true) 37 | @config.capture("hostname", :foo => "bar") 38 | end 39 | 40 | def test_capture_with_stderr_result_should_raise_capture_error 41 | @config.expects(:invoke_command).yields(mock("channel"), :err, "boom") 42 | assert_raises(Capistrano::CaptureError) { @config.capture("hostname") } 43 | end 44 | 45 | def test_capture_with_stdout_should_aggregate_and_return_stdout 46 | config_expects_invoke_command_to_loop_with(mock("channel"), "foo", "bar", "baz") 47 | assert_equal "foobarbaz", @config.capture("hostname") 48 | end 49 | 50 | private 51 | 52 | def config_expects_invoke_command_to_loop_with(channel, *output) 53 | class <<@config 54 | attr_accessor :script, :channel 55 | def invoke_command(*args) 56 | script.each { |item| yield channel, :out, item } 57 | end 58 | end 59 | @config.channel = channel 60 | @config.script = output 61 | end 62 | end -------------------------------------------------------------------------------- /test/configuration/loading_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/configuration/loading' 3 | 4 | class ConfigurationLoadingTest < Test::Unit::TestCase 5 | class MockConfig 6 | attr_accessor :ping 7 | attr_reader :original_initialize_called 8 | 9 | def initialize 10 | @original_initialize_called = true 11 | end 12 | 13 | def ping!(value) 14 | @ping = value 15 | end 16 | 17 | include Capistrano::Configuration::Loading 18 | end 19 | 20 | def setup 21 | @config = MockConfig.new 22 | end 23 | 24 | def teardown 25 | MockConfig.instance = nil 26 | $".delete "#{File.dirname(__FILE__)}/../fixtures/custom.rb" 27 | end 28 | 29 | def test_initialize_should_init_collections 30 | assert @config.original_initialize_called 31 | assert @config.load_paths.include?(".") 32 | assert @config.load_paths.detect { |v| v =~ /capistrano\/recipes$/ } 33 | end 34 | 35 | def test_load_with_options_and_block_should_raise_argument_error 36 | assert_raises(ArgumentError) do 37 | @config.load(:string => "foo") { something } 38 | end 39 | end 40 | 41 | def test_load_with_arguments_and_block_should_raise_argument_error 42 | assert_raises(ArgumentError) do 43 | @config.load("foo") { something } 44 | end 45 | end 46 | 47 | def test_load_from_string_should_eval_in_config_scope 48 | @config.load :string => "ping! :here" 49 | assert_equal :here, @config.ping 50 | end 51 | 52 | def test_load_from_file_shoudld_respect_load_path 53 | File.stubs(:file?).returns(false) 54 | File.stubs(:file?).with("custom/path/for/file.rb").returns(true) 55 | File.stubs(:read).with("custom/path/for/file.rb").returns("ping! :here") 56 | 57 | @config.load_paths << "custom/path/for" 58 | @config.load :file => "file.rb" 59 | 60 | assert_equal :here, @config.ping 61 | end 62 | 63 | def test_load_from_file_should_respect_load_path_and_appends_rb 64 | File.stubs(:file?).returns(false) 65 | File.stubs(:file?).with("custom/path/for/file.rb").returns(true) 66 | File.stubs(:read).with("custom/path/for/file.rb").returns("ping! :here") 67 | 68 | @config.load_paths << "custom/path/for" 69 | @config.load :file => "file" 70 | 71 | assert_equal :here, @config.ping 72 | end 73 | 74 | def test_load_from_file_should_raise_load_error_if_file_cannot_be_found 75 | File.stubs(:file?).returns(false) 76 | assert_raises(LoadError) do 77 | @config.load :file => "file" 78 | end 79 | end 80 | 81 | def test_load_from_proc_should_eval_proc_in_config_scope 82 | @config.load :proc => Proc.new { ping! :here } 83 | assert_equal :here, @config.ping 84 | end 85 | 86 | def test_load_with_block_should_treat_block_as_proc_parameter 87 | @config.load { ping! :here } 88 | assert_equal :here, @config.ping 89 | end 90 | 91 | def test_load_with_unrecognized_option_should_raise_argument_error 92 | assert_raises(ArgumentError) do 93 | @config.load :url => "http://www.load-this.test" 94 | end 95 | end 96 | 97 | def test_load_with_arguments_should_treat_arguments_as_files 98 | File.stubs(:file?).returns(false) 99 | File.stubs(:file?).with("./first.rb").returns(true) 100 | File.stubs(:file?).with("./second.rb").returns(true) 101 | File.stubs(:read).with("./first.rb").returns("ping! 'this'") 102 | File.stubs(:read).with("./second.rb").returns("ping << 'that'") 103 | assert_nothing_raised { @config.load "first", "second" } 104 | assert_equal "thisthat", @config.ping 105 | end 106 | 107 | def test_require_from_config_should_load_file_in_config_scope 108 | assert_nothing_raised do 109 | @config.require "#{File.dirname(__FILE__)}/../fixtures/custom" 110 | end 111 | assert_equal :custom, @config.ping 112 | end 113 | 114 | def test_require_without_config_should_raise_load_error 115 | assert_raises(LoadError) do 116 | require "#{File.dirname(__FILE__)}/../fixtures/custom" 117 | end 118 | end 119 | 120 | def test_require_in_multiple_instances_should_load_recipes_in_each_instance 121 | config2 = MockConfig.new 122 | @config.require "#{File.dirname(__FILE__)}/../fixtures/custom" 123 | config2.require "#{File.dirname(__FILE__)}/../fixtures/custom" 124 | assert_equal :custom, @config.ping 125 | assert_equal :custom, config2.ping 126 | end 127 | end -------------------------------------------------------------------------------- /test/configuration/roles_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/configuration/roles' 3 | require 'capistrano/server_definition' 4 | 5 | class ConfigurationRolesTest < Test::Unit::TestCase 6 | class MockConfig 7 | attr_reader :original_initialize_called 8 | 9 | def initialize 10 | @original_initialize_called = true 11 | end 12 | 13 | include Capistrano::Configuration::Roles 14 | end 15 | 16 | def setup 17 | @config = MockConfig.new 18 | end 19 | 20 | def test_initialize_should_initialize_roles_collection 21 | assert @config.original_initialize_called 22 | assert @config.roles.empty? 23 | end 24 | 25 | def test_role_should_allow_empty_list 26 | @config.role :app 27 | assert @config.roles[:app].empty? 28 | end 29 | 30 | def test_role_with_one_argument_should_add_to_roles_collection 31 | @config.role :app, "app1.capistrano.test" 32 | assert_equal [:app], @config.roles.keys 33 | assert_role_equals %w(app1.capistrano.test) 34 | end 35 | 36 | def test_role_block_returning_single_string_is_added_to_roles_collection 37 | @config.role :app do 38 | 'app1.capistrano.test' 39 | end 40 | assert_role_equals %w(app1.capistrano.test) 41 | end 42 | 43 | def test_role_with_multiple_arguments_should_add_each_to_roles_collection 44 | @config.role :app, "app1.capistrano.test", "app2.capistrano.test" 45 | assert_equal [:app], @config.roles.keys 46 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) 47 | end 48 | 49 | def test_role_with_block_and_strings_should_add_both_to_roles_collection 50 | @config.role :app, 'app1.capistrano.test' do 51 | 'app2.capistrano.test' 52 | end 53 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) 54 | end 55 | 56 | def test_role_block_returning_array_should_add_each_to_roles_collection 57 | @config.role :app do 58 | ['app1.capistrano.test', 'app2.capistrano.test'] 59 | end 60 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) 61 | end 62 | 63 | def test_role_with_options_should_apply_options_to_each_argument 64 | @config.role :app, "app1.capistrano.test", "app2.capistrano.test", :extra => :value 65 | @config.roles[:app].each do |server| 66 | assert_equal({:extra => :value}, server.options) 67 | end 68 | end 69 | 70 | def test_role_with_options_should_apply_options_to_block_results 71 | @config.role :app, :extra => :value do 72 | ['app1.capistrano.test', 'app2.capistrano.test'] 73 | end 74 | @config.roles[:app].each do |server| 75 | assert_equal({:extra => :value}, server.options) 76 | end 77 | end 78 | 79 | def test_options_should_apply_only_to_this_argument_set 80 | @config.role :app, 'app1.capistrano.test', 'app2.capistrano.test' do 81 | ['app3.capistrano.test', 'app4.capistrano.test'] 82 | end 83 | @config.role :app, 'app5.capistrano.test', 'app6.capistrano.test', :extra => :value do 84 | ['app7.capistrano.test', 'app8.capistrano.test'] 85 | end 86 | @config.role :app, 'app9.capistrano.test' 87 | 88 | option_hosts = ['app5.capistrano.test', 'app6.capistrano.test', 'app7.capistrano.test', 'app8.capistrano.test'] 89 | @config.roles[:app].each do |server| 90 | if (option_hosts.include? server.host) 91 | assert_equal({:extra => :value}, server.options) 92 | else 93 | assert_not_equal({:extra => :value}, server.options) 94 | end 95 | end 96 | end 97 | 98 | # Here, the source should be more readable than the method name 99 | def test_role_block_returns_options_hash_is_merged_with_role_options_argument 100 | @config.role :app, :first => :one, :second => :two do 101 | ['app1.capistrano.test', 'app2.capistrano.test', {:second => :please, :third => :three}] 102 | end 103 | @config.roles[:app].each do |server| 104 | assert_equal({:first => :one, :second => :please, :third => :three}, server.options) 105 | end 106 | end 107 | 108 | def test_role_block_can_override_role_options_argument 109 | @config.role :app, :value => :wrong do 110 | Capistrano::ServerDefinition.new('app.capistrano.test') 111 | end 112 | @config.roles[:app].servers 113 | @config.roles[:app].servers.each do |server| 114 | assert_not_equal({:value => :wrong}, server.options) 115 | end 116 | end 117 | 118 | def test_role_block_can_return_nil 119 | @config.role :app do 120 | nil 121 | end 122 | assert_role_equals ([]) 123 | end 124 | 125 | def test_role_block_can_return_empty_array 126 | @config.role :app do 127 | [] 128 | end 129 | assert_role_equals ([]) 130 | end 131 | 132 | def test_role_definitions_via_server_should_associate_server_with_roles 133 | @config.server "www.capistrano.test", :web, :app 134 | assert_equal %w(www.capistrano.test), @config.roles[:app].map { |s| s.host } 135 | assert_equal %w(www.capistrano.test), @config.roles[:web].map { |s| s.host } 136 | end 137 | 138 | private 139 | 140 | def assert_role_equals(list) 141 | assert_equal list, @config.roles[:app].map { |s| s.host } 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /test/configuration/servers_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/task_definition' 3 | require 'capistrano/configuration/servers' 4 | 5 | class ConfigurationServersTest < Test::Unit::TestCase 6 | class MockConfig 7 | attr_reader :roles 8 | 9 | def initialize 10 | @roles = {} 11 | end 12 | 13 | include Capistrano::Configuration::Servers 14 | end 15 | 16 | def setup 17 | @config = MockConfig.new 18 | role(@config, :app, "app1", :primary => true) 19 | role(@config, :app, "app2", "app3") 20 | role(@config, :web, "web1", "web2") 21 | role(@config, :report, "app2", :no_deploy => true) 22 | role(@config, :file, "file", :no_deploy => true) 23 | end 24 | 25 | def test_task_without_roles_should_apply_to_all_defined_hosts 26 | task = new_task(:testing) 27 | assert_equal %w(app1 app2 app3 web1 web2 file).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 28 | end 29 | 30 | def test_task_with_explicit_role_list_should_apply_only_to_those_roles 31 | task = new_task(:testing, @config, :roles => %w(app web)) 32 | assert_equal %w(app1 app2 app3 web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 33 | end 34 | 35 | def test_task_with_single_role_should_apply_only_to_that_role 36 | task = new_task(:testing, @config, :roles => :web) 37 | assert_equal %w(web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 38 | end 39 | 40 | def test_task_with_hosts_option_should_apply_only_to_those_hosts 41 | task = new_task(:testing, @config, :hosts => %w(foo bar)) 42 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 43 | end 44 | 45 | def test_task_with_single_hosts_option_should_apply_only_to_that_host 46 | task = new_task(:testing, @config, :hosts => "foo") 47 | assert_equal %w(foo).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 48 | end 49 | 50 | def test_task_with_roles_as_environment_variable_should_apply_only_to_that_role 51 | ENV['ROLES'] = "app,file" 52 | task = new_task(:testing) 53 | assert_equal %w(app1 app2 app3 file).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 54 | ensure 55 | ENV['ROLES'] = nil 56 | end 57 | 58 | def test_task_with_hosts_as_environment_variable_should_apply_only_to_those_hosts 59 | ENV['HOSTS'] = "foo,bar" 60 | task = new_task(:testing) 61 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 62 | ensure 63 | ENV['HOSTS'] = nil 64 | end 65 | 66 | def test_task_with_only_should_apply_only_to_matching_tasks 67 | task = new_task(:testing, @config, :roles => :app, :only => { :primary => true }) 68 | assert_equal %w(app1), @config.find_servers_for_task(task).map { |s| s.host } 69 | end 70 | 71 | def test_task_with_except_should_apply_only_to_matching_tasks 72 | task = new_task(:testing, @config, :except => { :no_deploy => true }) 73 | assert_equal %w(app1 app2 app3 web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort 74 | end 75 | 76 | def test_options_to_find_servers_for_task_should_override_options_in_task 77 | task = new_task(:testing, @config, :roles => :web) 78 | assert_equal %w(app1 app2 app3).sort, @config.find_servers_for_task(task, :roles => :app).map { |s| s.host }.sort 79 | end 80 | 81 | def test_find_servers_with_lambda_for_hosts_should_be_evaluated 82 | assert_equal %w(foo), @config.find_servers(:hosts => lambda { "foo" }).map { |s| s.host }.sort 83 | assert_equal %w(bar foo), @config.find_servers(:hosts => lambda { %w(foo bar) }).map { |s| s.host }.sort 84 | end 85 | 86 | def test_find_servers_with_lambda_for_roles_should_be_evaluated 87 | assert_equal %w(app1 app2 app3), @config.find_servers(:roles => lambda { :app }).map { |s| s.host }.sort 88 | assert_equal %w(app2 file), @config.find_servers(:roles => lambda { [:report, :file] }).map { |s| s.host }.sort 89 | end 90 | end -------------------------------------------------------------------------------- /test/configuration/variables_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../utils" 2 | require 'capistrano/configuration/variables' 3 | 4 | class ConfigurationVariablesTest < Test::Unit::TestCase 5 | class MockConfig 6 | attr_reader :original_initialize_called 7 | 8 | def initialize 9 | @original_initialize_called = true 10 | end 11 | 12 | include Capistrano::Configuration::Variables 13 | end 14 | 15 | def setup 16 | MockConfig.any_instance.stubs(:logger).returns(stub_everything) 17 | @config = MockConfig.new 18 | end 19 | 20 | def test_initialize_should_initialize_variables_hash 21 | assert @config.original_initialize_called 22 | assert_equal({:ssh_options => {}, :logger => @config.logger}, @config.variables) 23 | end 24 | 25 | def test_set_should_add_variable_to_hash 26 | @config.set :sample, :value 27 | assert_equal :value, @config.variables[:sample] 28 | end 29 | 30 | def test_set_should_convert_variable_name_to_symbol 31 | @config.set "sample", :value 32 | assert_equal :value, @config.variables[:sample] 33 | end 34 | 35 | def test_set_should_be_aliased_to_square_brackets 36 | @config[:sample] = :value 37 | assert_equal :value, @config.variables[:sample] 38 | end 39 | 40 | def test_variables_should_be_accessible_as_read_accessors 41 | @config[:sample] = :value 42 | assert_equal :value, @config.sample 43 | end 44 | 45 | def test_method_missing_should_raise_error_if_no_variable_matches 46 | assert_raises(NoMethodError) do 47 | @config.sample 48 | end 49 | end 50 | 51 | def test_respond_to_should_look_for_variables 52 | assert !@config.respond_to?(:sample) 53 | @config[:sample] = :value 54 | assert @config.respond_to?(:sample) 55 | end 56 | 57 | def test_set_should_require_value 58 | assert_raises(ArgumentError) do 59 | @config.set(:sample) 60 | end 61 | end 62 | 63 | def test_set_should_allow_value_to_be_omitted_if_block_is_given 64 | assert_nothing_raised do 65 | @config.set(:sample) { :value } 66 | end 67 | assert_instance_of Proc, @config.variables[:sample] 68 | end 69 | 70 | def test_set_should_not_allow_multiple_values 71 | assert_raises(ArgumentError) do 72 | @config.set(:sample, :value, :another) 73 | end 74 | end 75 | 76 | def test_set_should_not_allow_both_a_value_and_a_block 77 | assert_raises(ArgumentError) do 78 | @config.set(:sample, :value) { :block } 79 | end 80 | end 81 | 82 | def test_set_should_not_allow_capitalized_variables 83 | assert_raises(ArgumentError) do 84 | @config.set :Sample, :value 85 | end 86 | end 87 | 88 | def test_unset_should_remove_variable_from_hash 89 | @config.set :sample, :value 90 | assert @config.variables.key?(:sample) 91 | @config.unset :sample 92 | assert !@config.variables.key?(:sample) 93 | end 94 | 95 | def test_unset_should_clear_memory_of_original_proc 96 | @config.set(:sample) { :value } 97 | @config.fetch(:sample) 98 | @config.unset(:sample) 99 | assert_equal false, @config.reset!(:sample) 100 | end 101 | 102 | def test_exists_should_report_existance_of_variable_in_hash 103 | assert !@config.exists?(:sample) 104 | @config[:sample] = :value 105 | assert @config.exists?(:sample) 106 | end 107 | 108 | def test_reset_should_do_nothing_if_variable_does_not_exist 109 | assert_equal false, @config.reset!(:sample) 110 | assert !@config.variables.key?(:sample) 111 | end 112 | 113 | def test_reset_should_do_nothing_if_variable_is_not_a_proc 114 | @config.set(:sample, :value) 115 | assert_equal false, @config.reset!(:sample) 116 | assert_equal :value, @config.variables[:sample] 117 | end 118 | 119 | def test_reset_should_do_nothing_if_proc_variable_has_not_been_dereferenced 120 | @config.set(:sample) { :value } 121 | assert_equal false, @config.reset!(:sample) 122 | assert_instance_of Proc, @config.variables[:sample] 123 | end 124 | 125 | def test_reset_should_restore_variable_to_original_proc_value 126 | @config.set(:sample) { :value } 127 | assert_instance_of Proc, @config.variables[:sample] 128 | @config.fetch(:sample) 129 | assert_instance_of Symbol, @config.variables[:sample] 130 | assert @config.reset!(:sample) 131 | assert_instance_of Proc, @config.variables[:sample] 132 | end 133 | 134 | def test_fetch_should_return_stored_non_proc_value 135 | @config.set(:sample, :value) 136 | assert_equal :value, @config.fetch(:sample) 137 | end 138 | 139 | def test_fetch_should_raise_index_error_if_variable_does_not_exist 140 | assert_raises(IndexError) do 141 | @config.fetch(:sample) 142 | end 143 | end 144 | 145 | def test_fetch_should_return_default_if_variable_does_not_exist_and_default_is_given 146 | assert_nothing_raised do 147 | assert_equal :default_value, @config.fetch(:sample, :default_value) 148 | end 149 | end 150 | 151 | def test_fetch_should_invoke_block_if_variable_does_not_exist_and_block_is_given 152 | assert_nothing_raised do 153 | assert_equal :default_value, @config.fetch(:sample) { :default_value } 154 | end 155 | end 156 | 157 | def test_fetch_should_raise_argument_error_if_both_default_and_block_are_given 158 | assert_raises(ArgumentError) do 159 | @config.fetch(:sample, :default1) { :default2 } 160 | end 161 | end 162 | 163 | def test_fetch_should_dereference_proc_values 164 | @config.set(:sample) { :value } 165 | assert_instance_of Proc, @config.variables[:sample] 166 | assert_equal :value, @config.fetch(:sample) 167 | assert_instance_of Symbol, @config.variables[:sample] 168 | end 169 | 170 | def test_square_brackets_should_alias_fetch 171 | @config.set(:sample, :value) 172 | assert_equal :value, @config[:sample] 173 | end 174 | 175 | def test_square_brackets_should_return_nil_for_non_existant_variable 176 | assert_nothing_raised do 177 | assert_nil @config[:sample] 178 | end 179 | end 180 | end -------------------------------------------------------------------------------- /test/configuration_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/configuration' 3 | 4 | # These tests are only for testing the integration of the various components 5 | # of the Configuration class. To test specific features, please look at the 6 | # tests under test/configuration. 7 | 8 | class ConfigurationTest < Test::Unit::TestCase 9 | def setup 10 | @config = Capistrano::Configuration.new 11 | end 12 | 13 | def test_connections_execution_loading_namespaces_roles_and_variables_modules_should_integrate_correctly 14 | Capistrano::SSH.expects(:connect).with { |s,c| s.host == "www.capistrano.test" && c == @config }.returns(:session) 15 | Capistrano::Command.expects(:process).with("echo 'hello world'", [:session], :logger => @config.logger) 16 | 17 | @config.load do 18 | role :test, "www.capistrano.test" 19 | set :message, "hello world" 20 | namespace :testing do 21 | task :example, :roles => :test do 22 | run "echo '#{message}'" 23 | end 24 | end 25 | end 26 | 27 | @config.testing.example 28 | end 29 | 30 | def test_tasks_in_nested_namespace_should_be_able_to_call_tasks_in_same_namespace 31 | @config.namespace(:outer) do 32 | task(:first) { set :called_first, true } 33 | namespace(:inner) do 34 | task(:first) { set :called_inner_first, true } 35 | task(:second) { first } 36 | end 37 | end 38 | 39 | @config.outer.inner.second 40 | assert !@config[:called_first] 41 | assert @config[:called_inner_first] 42 | end 43 | 44 | def test_tasks_in_nested_namespace_should_be_able_to_call_tasks_in_parent_namespace 45 | @config.namespace(:outer) do 46 | task(:first) { set :called_first, true } 47 | namespace(:inner) do 48 | task(:second) { first } 49 | end 50 | end 51 | 52 | @config.outer.inner.second 53 | assert @config[:called_first] 54 | end 55 | 56 | def test_tasks_in_nested_namespace_should_be_able_to_call_shadowed_tasks_in_parent_namespace 57 | @config.namespace(:outer) do 58 | task(:first) { set :called_first, true } 59 | namespace(:inner) do 60 | task(:first) { set :called_inner_first, true } 61 | task(:second) { parent.first } 62 | end 63 | end 64 | 65 | @config.outer.inner.second 66 | assert @config[:called_first] 67 | assert !@config[:called_inner_first] 68 | end 69 | 70 | def test_hooks_for_default_task_should_be_found_if_named_after_the_namespace 71 | @config.namespace(:outer) do 72 | task(:default) { set :called_default, true } 73 | task(:before_outer) { set :called_before_outer, true } 74 | task(:after_outer) { set :called_after_outer, true } 75 | end 76 | @config.outer.default 77 | assert @config[:called_before_outer] 78 | assert @config[:called_default] 79 | assert @config[:called_after_outer] 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/deploy/scm/accurev_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/recipes/deploy/scm/accurev' 3 | 4 | class AccurevTest < Test::Unit::TestCase 5 | include Capistrano::Deploy::SCM 6 | 7 | def test_internal_revision_to_s 8 | assert_equal 'foo/1', Accurev::InternalRevision.new('foo', 1).to_s 9 | assert_equal 'foo/highest', Accurev::InternalRevision.new('foo', 'highest').to_s 10 | end 11 | 12 | def test_internal_revision_parse 13 | revision = Accurev::InternalRevision.parse('foo') 14 | assert_equal 'foo', revision.stream 15 | assert_equal 'highest', revision.transaction_id 16 | assert_equal 'foo/highest', revision.to_s 17 | 18 | revision = Accurev::InternalRevision.parse('foo/1') 19 | assert_equal 'foo', revision.stream 20 | assert_equal '1', revision.transaction_id 21 | assert_equal 'foo/1', revision.to_s 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/deploy/scm/base_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/recipes/deploy/scm/base' 3 | 4 | class DeploySCMBaseTest < Test::Unit::TestCase 5 | class TestSCM < Capistrano::Deploy::SCM::Base 6 | default_command "floopy" 7 | end 8 | 9 | def setup 10 | @config = { } 11 | def @config.exists?(name); key?(name); end 12 | 13 | @source = TestSCM.new(@config) 14 | end 15 | 16 | def test_command_should_default_to_default_command 17 | assert_equal "floopy", @source.command 18 | @source.local { assert_equal "floopy", @source.command } 19 | end 20 | 21 | def test_command_should_use_scm_command_if_available 22 | @config[:scm_command] = "/opt/local/bin/floopy" 23 | assert_equal "/opt/local/bin/floopy", @source.command 24 | end 25 | 26 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set 27 | @config[:scm_command] = "/opt/local/bin/floopy" 28 | @source.local { assert_equal "/opt/local/bin/floopy", @source.command } 29 | end 30 | 31 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set 32 | @config[:scm_command] = "/opt/local/bin/floopy" 33 | @config[:local_scm_command] = "/usr/local/bin/floopy" 34 | assert_equal "/opt/local/bin/floopy", @source.command 35 | @source.local { assert_equal "/usr/local/bin/floopy", @source.command } 36 | end 37 | 38 | def test_command_should_use_default_if_scm_command_is_default 39 | @config[:scm_command] = :default 40 | assert_equal "floopy", @source.command 41 | end 42 | 43 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default 44 | @config[:scm_command] = "/foo/bar/floopy" 45 | @config[:local_scm_command] = :default 46 | @source.local { assert_equal "floopy", @source.command } 47 | end 48 | 49 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode 50 | @config[:scm_command] = "/foo/bar/floopy" 51 | @config[:local_scm_command] = :default 52 | assert_equal "floopy", @source.local.command 53 | assert_equal "/foo/bar/floopy", @source.command 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/deploy/scm/git_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/recipes/deploy/scm/git' 3 | 4 | class DeploySCMGitTest < Test::Unit::TestCase 5 | class TestSCM < Capistrano::Deploy::SCM::Git 6 | default_command "git" 7 | end 8 | 9 | def setup 10 | @config = { } 11 | def @config.exists?(name); key?(name); end 12 | 13 | @source = TestSCM.new(@config) 14 | end 15 | 16 | def test_head 17 | assert_equal "HEAD", @source.head 18 | 19 | # With :branch 20 | @config[:branch] = "master" 21 | assert_equal "master", @source.head 22 | end 23 | 24 | def test_origin 25 | assert_equal "origin", @source.origin 26 | @config[:remote] = "username" 27 | assert_equal "username", @source.origin 28 | end 29 | 30 | def test_checkout 31 | @config[:repository] = "git@somehost.com:project.git" 32 | dest = "/var/www" 33 | rev = 'c2d9e79' 34 | assert_equal "git clone git@somehost.com:project.git /var/www && cd /var/www && git checkout -b deploy #{rev}", @source.checkout(rev, dest) 35 | 36 | # With :scm_command 37 | git = "/opt/local/bin/git" 38 | @config[:scm_command] = git 39 | assert_equal "#{git} clone git@somehost.com:project.git /var/www && cd /var/www && #{git} checkout -b deploy #{rev}", @source.checkout(rev, dest) 40 | end 41 | 42 | def test_diff 43 | assert_equal "git diff master", @source.diff('master') 44 | assert_equal "git diff master..branch", @source.diff('master', 'branch') 45 | end 46 | 47 | def test_log 48 | assert_equal "git log master", @source.log('master') 49 | assert_equal "git log master..branch", @source.log('master', 'branch') 50 | end 51 | 52 | def test_query_revision 53 | assert_equal "git rev-parse HEAD", @source.query_revision('HEAD') { |o| o } 54 | end 55 | 56 | def test_command_should_be_backwards_compatible 57 | # 1.x version of this module used ":git", not ":scm_command" 58 | @config[:git] = "/srv/bin/git" 59 | assert_equal "/srv/bin/git", @source.command 60 | end 61 | 62 | def test_sync 63 | dest = "/var/www" 64 | rev = 'c2d9e79' 65 | assert_equal "cd #{dest} && git fetch origin && git reset --hard #{rev}", @source.sync(rev, dest) 66 | 67 | # With :scm_command 68 | git = "/opt/local/bin/git" 69 | @config[:scm_command] = git 70 | assert_equal "cd #{dest} && #{git} fetch origin && #{git} reset --hard #{rev}", @source.sync(rev, dest) 71 | end 72 | 73 | def test_sync_with_remote 74 | dest = "/var/www" 75 | rev = 'c2d9e79' 76 | remote = "username" 77 | repository = "git@somehost.com:project.git" 78 | 79 | @config[:repository] = repository 80 | @config[:remote] = remote 81 | 82 | assert_equal "cd #{dest} && git config remote.#{remote}.url #{repository} && git config remote.#{remote}.fetch +refs/heads/*:refs/remotes/#{remote}/* && git fetch #{remote} && git reset --hard #{rev}", @source.sync(rev, dest) 83 | end 84 | 85 | def test_shallow_clone 86 | @config[:repository] = "git@somehost.com:project.git" 87 | @config[:git_shallow_clone] = 1 88 | dest = "/var/www" 89 | rev = 'c2d9e79' 90 | assert_equal "git clone --depth 1 git@somehost.com:project.git /var/www && cd /var/www && git checkout -b deploy #{rev}", @source.checkout(rev, dest) 91 | end 92 | 93 | def test_remote_clone 94 | @config[:repository] = "git@somehost.com:project.git" 95 | @config[:remote] = "username" 96 | dest = "/var/www" 97 | rev = 'c2d9e79' 98 | assert_equal "git clone -o username git@somehost.com:project.git /var/www && cd /var/www && git checkout -b deploy #{rev}", @source.checkout(rev, dest) 99 | end 100 | 101 | # Tests from base_test.rb, makin' sure we didn't break anything up there! 102 | def test_command_should_default_to_default_command 103 | assert_equal "git", @source.command 104 | @source.local { assert_equal "git", @source.command } 105 | end 106 | 107 | def test_command_should_use_scm_command_if_available 108 | @config[:scm_command] = "/opt/local/bin/git" 109 | assert_equal "/opt/local/bin/git", @source.command 110 | end 111 | 112 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set 113 | @config[:scm_command] = "/opt/local/bin/git" 114 | @source.local { assert_equal "/opt/local/bin/git", @source.command } 115 | end 116 | 117 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set 118 | @config[:scm_command] = "/opt/local/bin/git" 119 | @config[:local_scm_command] = "/usr/local/bin/git" 120 | assert_equal "/opt/local/bin/git", @source.command 121 | @source.local { assert_equal "/usr/local/bin/git", @source.command } 122 | end 123 | 124 | def test_command_should_use_default_if_scm_command_is_default 125 | @config[:scm_command] = :default 126 | assert_equal "git", @source.command 127 | end 128 | 129 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default 130 | @config[:scm_command] = "/foo/bar/git" 131 | @config[:local_scm_command] = :default 132 | @source.local { assert_equal "git", @source.command } 133 | end 134 | 135 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode 136 | @config[:scm_command] = "/foo/bar/git" 137 | @config[:local_scm_command] = :default 138 | assert_equal "git", @source.local.command 139 | assert_equal "/foo/bar/git", @source.command 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /test/deploy/scm/mercurial_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/../../utils" 2 | require 'capistrano/recipes/deploy/scm/mercurial' 3 | 4 | class DeploySCMMercurialTest < Test::Unit::TestCase 5 | class TestSCM < Capistrano::Deploy::SCM::Mercurial 6 | default_command "hg" 7 | end 8 | 9 | def setup 10 | @config = { } 11 | def @config.exists?(name); key?(name); end 12 | 13 | @source = TestSCM.new(@config) 14 | end 15 | 16 | def test_head 17 | assert_equal "tip", @source.head 18 | end 19 | 20 | def test_checkout 21 | @config[:repository] = "http://example.com/project-hg" 22 | dest = "/var/www" 23 | assert_equal "hg clone --noupdate http://example.com/project-hg /var/www && hg update --repository /var/www --clean 8a8e00b8f11b", @source.checkout('8a8e00b8f11b', dest) 24 | end 25 | 26 | def test_diff 27 | assert_equal "hg diff --rev tip", @source.diff('tip') 28 | assert_equal "hg diff --rev 1 --rev 2", @source.diff('1', '2') 29 | end 30 | 31 | def test_log 32 | assert_equal "hg log --rev 8a8e00b8f11b", @source.log('8a8e00b8f11b') 33 | assert_equal "hg log --rev 0:3", @source.log('0', '3') 34 | end 35 | 36 | def test_query_revision 37 | assert_equal "hg log -r 8a8e00b8f11b --template '{node|short}'", @source.query_revision('8a8e00b8f11b') { |o| o } 38 | end 39 | 40 | def test_username_should_be_backwards_compatible 41 | # older versions of this module required :scm_user var instead 42 | # of the currently preferred :scm_username 43 | require 'capistrano/logger' 44 | @config[:scm_user] = "fred" 45 | text = "user:" 46 | assert_equal "fred\n", @source.handle_data(:test_state, :test_stream, text) 47 | # :scm_username takes priority 48 | @config[:scm_username] = "wilma" 49 | assert_equal "wilma\n", @source.handle_data(:test_state, :test_stream, text) 50 | end 51 | 52 | def test_sync 53 | dest = "/var/www" 54 | assert_equal "hg pull --repository /var/www && hg update --repository /var/www --clean 8a8e00b8f11b", @source.sync('8a8e00b8f11b', dest) 55 | 56 | # With :scm_command 57 | @config[:scm_command] = "/opt/local/bin/hg" 58 | assert_equal "/opt/local/bin/hg pull --repository /var/www && /opt/local/bin/hg update --repository /var/www --clean 8a8e00b8f11b", @source.sync('8a8e00b8f11b', dest) 59 | end 60 | 61 | def test_export 62 | dest = "/var/www" 63 | assert_raise(NotImplementedError) { @source.export('8a8e00b8f11b', dest) } 64 | end 65 | 66 | def test_sends_password_if_set 67 | require 'capistrano/logger' 68 | text = "password:" 69 | @config[:scm_password] = "opensesame" 70 | assert_equal "opensesame\n", @source.handle_data(:test_state, :test_stream, text) 71 | end 72 | 73 | def test_prompts_for_password_if_preferred 74 | require 'capistrano/logger' 75 | require 'capistrano/cli' 76 | Capistrano::CLI.stubs(:password_prompt).with("hg password: ").returns("opensesame") 77 | @config[:scm_prefer_prompt] = true 78 | text = "password:" 79 | assert_equal "opensesame\n", @source.handle_data(:test_state, :test_stream, text) 80 | end 81 | 82 | 83 | # Tests from base_test.rb, makin' sure we didn't break anything up there! 84 | def test_command_should_default_to_default_command 85 | assert_equal "hg", @source.command 86 | @source.local { assert_equal "hg", @source.command } 87 | end 88 | 89 | def test_command_should_use_scm_command_if_available 90 | @config[:scm_command] = "/opt/local/bin/hg" 91 | assert_equal "/opt/local/bin/hg", @source.command 92 | end 93 | 94 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set 95 | @config[:scm_command] = "/opt/local/bin/hg" 96 | @source.local { assert_equal "/opt/local/bin/hg", @source.command } 97 | end 98 | 99 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set 100 | @config[:scm_command] = "/opt/local/bin/hg" 101 | @config[:local_scm_command] = "/usr/local/bin/hg" 102 | assert_equal "/opt/local/bin/hg", @source.command 103 | @source.local { assert_equal "/usr/local/bin/hg", @source.command } 104 | end 105 | 106 | def test_command_should_use_default_if_scm_command_is_default 107 | @config[:scm_command] = :default 108 | assert_equal "hg", @source.command 109 | end 110 | 111 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default 112 | @config[:scm_command] = "/foo/bar/hg" 113 | @config[:local_scm_command] = :default 114 | @source.local { assert_equal "hg", @source.command } 115 | end 116 | 117 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode 118 | @config[:scm_command] = "/foo/bar/hg" 119 | @config[:local_scm_command] = :default 120 | assert_equal "hg", @source.local.command 121 | assert_equal "/foo/bar/hg", @source.command 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/extensions_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano' 3 | 4 | class ExtensionsTest < Test::Unit::TestCase 5 | module CustomExtension 6 | def do_something(command) 7 | run(command) 8 | end 9 | end 10 | 11 | def setup 12 | @config = Capistrano::Configuration.new 13 | end 14 | 15 | def teardown 16 | Capistrano::EXTENSIONS.keys.each { |e| Capistrano.remove_plugin(e) } 17 | end 18 | 19 | def test_register_plugin_should_add_instance_method_on_configuration_and_return_true 20 | assert !@config.respond_to?(:custom_stuff) 21 | assert Capistrano.plugin(:custom_stuff, CustomExtension) 22 | assert @config.respond_to?(:custom_stuff) 23 | end 24 | 25 | def test_register_plugin_that_already_exists_should_return_false 26 | assert Capistrano.plugin(:custom_stuff, CustomExtension) 27 | assert !Capistrano.plugin(:custom_stuff, CustomExtension) 28 | end 29 | 30 | def test_register_plugin_with_public_method_name_should_fail 31 | method = Capistrano::Configuration.public_instance_methods.first 32 | assert_not_nil method, "need a public instance method for testing" 33 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } 34 | end 35 | 36 | def test_register_plugin_with_protected_method_name_should_fail 37 | method = Capistrano::Configuration.protected_instance_methods.first 38 | assert_not_nil method, "need a protected instance method for testing" 39 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } 40 | end 41 | 42 | def test_register_plugin_with_private_method_name_should_fail 43 | method = Capistrano::Configuration.private_instance_methods.first 44 | assert_not_nil method, "need a private instance method for testing" 45 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } 46 | end 47 | 48 | def test_unregister_plugin_that_does_not_exist_should_return_false 49 | assert !Capistrano.remove_plugin(:custom_stuff) 50 | end 51 | 52 | def test_unregister_plugin_should_remove_method_and_return_true 53 | assert Capistrano.plugin(:custom_stuff, CustomExtension) 54 | assert @config.respond_to?(:custom_stuff) 55 | assert Capistrano.remove_plugin(:custom_stuff) 56 | assert !@config.respond_to?(:custom_stuff) 57 | end 58 | 59 | def test_registered_plugin_proxy_should_return_proxy_object 60 | Capistrano.plugin(:custom_stuff, CustomExtension) 61 | assert_instance_of Capistrano::ExtensionProxy, @config.custom_stuff 62 | end 63 | 64 | def test_proxy_object_should_delegate_to_configuration 65 | Capistrano.plugin(:custom_stuff, CustomExtension) 66 | @config.expects(:run).with("hello") 67 | @config.custom_stuff.do_something("hello") 68 | end 69 | end -------------------------------------------------------------------------------- /test/fixtures/cli_integration.rb: -------------------------------------------------------------------------------- 1 | role :test, "www.capistrano.test" 2 | 3 | task :testing, :roles => :test do 4 | set :testing_occurred, true 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/config.rb: -------------------------------------------------------------------------------- 1 | set :application, "foo" 2 | set :repository, "1/2/#{application}" 3 | set :gateway, "#{__FILE__}.example.com" 4 | 5 | role :web, "www.example.com", :primary => true 6 | -------------------------------------------------------------------------------- /test/fixtures/custom.rb: -------------------------------------------------------------------------------- 1 | ConfigurationLoadingTest::MockConfig.instance(:must_exist).load do 2 | ping! :custom 3 | end 4 | -------------------------------------------------------------------------------- /test/logger_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/logger' 3 | require 'stringio' 4 | 5 | class LoggerTest < Test::Unit::TestCase 6 | def setup 7 | @io = StringIO.new 8 | @logger = Capistrano::Logger.new(:output => @io) 9 | end 10 | 11 | def test_logger_should_use_STDERR_by_default 12 | logger = Capistrano::Logger.new 13 | assert_equal STDERR, logger.device 14 | end 15 | 16 | def test_logger_should_use_output_option_if_output_responds_to_puts 17 | logger = Capistrano::Logger.new(:output => STDOUT) 18 | assert_equal STDOUT, logger.device 19 | end 20 | 21 | def test_logger_should_open_file_if_output_does_not_respond_to_puts 22 | File.expects(:open).with("logs/capistrano.log", "a").returns(:mock) 23 | logger = Capistrano::Logger.new(:output => "logs/capistrano.log") 24 | assert_equal :mock, logger.device 25 | end 26 | 27 | def test_close_should_not_close_device_if_device_is_default 28 | logger = Capistrano::Logger.new 29 | logger.device.expects(:close).never 30 | logger.close 31 | end 32 | 33 | def test_close_should_not_close_device_is_device_is_explicitly_given 34 | logger = Capistrano::Logger.new(:output => STDOUT) 35 | STDOUT.expects(:close).never 36 | logger.close 37 | end 38 | 39 | def test_close_should_close_device_when_device_was_implicitly_opened 40 | f = mock("file", :close => nil) 41 | File.expects(:open).with("logs/capistrano.log", "a").returns(f) 42 | logger = Capistrano::Logger.new(:output => "logs/capistrano.log") 43 | logger.close 44 | end 45 | 46 | def test_log_with_level_greater_than_threshold_should_ignore_message 47 | @logger.level = 3 48 | @logger.log(4, "message") 49 | assert @io.string.empty? 50 | end 51 | 52 | def test_log_with_level_equal_to_threshold_should_log_message 53 | @logger.level = 3 54 | @logger.log(3, "message") 55 | assert @io.string.include?("message") 56 | end 57 | 58 | def test_log_with_level_less_than_threshold_should_log_message 59 | @logger.level = 3 60 | @logger.log(2, "message") 61 | assert @io.string.include?("message") 62 | end 63 | 64 | def test_log_with_multiline_message_should_log_each_line_separately 65 | @logger.log(0, "first line\nsecond line") 66 | assert @io.string.include?("*** first line") 67 | assert @io.string.include?("*** second line") 68 | end 69 | 70 | def test_log_with_line_prefix_should_insert_line_prefix_before_message 71 | @logger.log(0, "message", "prefix") 72 | assert @io.string.include?("*** [prefix] message") 73 | end 74 | 75 | def test_log_with_level_0_should_have_strong_indent 76 | @logger.log(0, "message") 77 | assert @io.string.match(/^\*\*\* message/) 78 | end 79 | 80 | def test_log_with_level_1_should_have_weaker_indent 81 | @logger.level = 1 82 | @logger.log(1, "message") 83 | assert @io.string.match(/^ \*\* message/) 84 | end 85 | 86 | def test_log_with_level_2_should_have_weaker_indent 87 | @logger.level = 2 88 | @logger.log(2, "message") 89 | assert @io.string.match(/^ \* message/) 90 | end 91 | 92 | def test_log_with_level_3_should_have_weakest_indent 93 | @logger.level = 3 94 | @logger.log(3, "message") 95 | assert @io.string.match(/^ message/) 96 | end 97 | 98 | def test_important_should_delegate_to_log_with_level_IMPORTANT 99 | @logger.expects(:log).with(Capistrano::Logger::IMPORTANT, "message", "prefix") 100 | @logger.important("message", "prefix") 101 | end 102 | 103 | def test_info_should_delegate_to_log_with_level_INFO 104 | @logger.expects(:log).with(Capistrano::Logger::INFO, "message", "prefix") 105 | @logger.info("message", "prefix") 106 | end 107 | 108 | def test_debug_should_delegate_to_log_with_level_DEBUG 109 | @logger.expects(:log).with(Capistrano::Logger::DEBUG, "message", "prefix") 110 | @logger.debug("message", "prefix") 111 | end 112 | 113 | def test_trace_should_delegate_to_log_with_level_TRACE 114 | @logger.expects(:log).with(Capistrano::Logger::TRACE, "message", "prefix") 115 | @logger.trace("message", "prefix") 116 | end 117 | 118 | def test_ordering_of_levels 119 | assert Capistrano::Logger::IMPORTANT < Capistrano::Logger::INFO 120 | assert Capistrano::Logger::INFO < Capistrano::Logger::DEBUG 121 | assert Capistrano::Logger::DEBUG < Capistrano::Logger::TRACE 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /test/server_definition_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/server_definition' 3 | 4 | class ServerDefinitionTest < Test::Unit::TestCase 5 | def test_new_without_credentials_or_port_should_set_values_to_defaults 6 | server = Capistrano::ServerDefinition.new("www.capistrano.test") 7 | assert_equal "www.capistrano.test", server.host 8 | assert_nil server.user 9 | assert_nil server.port 10 | end 11 | 12 | def test_new_with_encoded_user_should_extract_user_and_use_default_port 13 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test") 14 | assert_equal "www.capistrano.test", server.host 15 | assert_equal "jamis", server.user 16 | assert_nil server.port 17 | end 18 | 19 | def test_new_with_encoded_port_should_extract_port_and_use_default_user 20 | server = Capistrano::ServerDefinition.new("www.capistrano.test:8080") 21 | assert_equal "www.capistrano.test", server.host 22 | assert_nil server.user 23 | assert_equal 8080, server.port 24 | end 25 | 26 | def test_new_with_encoded_user_and_port_should_extract_user_and_port 27 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test:8080") 28 | assert_equal "www.capistrano.test", server.host 29 | assert_equal "jamis", server.user 30 | assert_equal 8080, server.port 31 | end 32 | 33 | def test_new_with_user_as_option_should_use_given_user 34 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :user => "jamis") 35 | assert_equal "www.capistrano.test", server.host 36 | assert_equal "jamis", server.user 37 | assert_nil server.port 38 | end 39 | 40 | def test_new_with_port_as_option_should_use_given_user 41 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :port => 8080) 42 | assert_equal "www.capistrano.test", server.host 43 | assert_nil server.user 44 | assert_equal 8080, server.port 45 | end 46 | 47 | def test_encoded_value_should_override_hash_option 48 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test:8080", :user => "david", :port => 8081) 49 | assert_equal "www.capistrano.test", server.host 50 | assert_equal "jamis", server.user 51 | assert_equal 8080, server.port 52 | assert server.options.empty? 53 | end 54 | 55 | def test_new_with_option_should_dup_option_hash 56 | options = {} 57 | server = Capistrano::ServerDefinition.new("www.capistrano.test", options) 58 | assert_not_equal options.object_id, server.options.object_id 59 | end 60 | 61 | def test_new_with_options_should_keep_options 62 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :primary => true) 63 | assert_equal true, server.options[:primary] 64 | end 65 | 66 | def test_comparison_should_match_when_host_user_port_are_same 67 | s1 = server("jamis@www.capistrano.test:8080") 68 | s2 = server("www.capistrano.test", :user => "jamis", :port => 8080) 69 | assert_equal s1, s2 70 | assert_equal s1.hash, s2.hash 71 | assert s1.eql?(s2) 72 | end 73 | 74 | def test_servers_should_be_comparable 75 | s1 = server("jamis@www.capistrano.test:8080") 76 | s2 = server("www.alphabet.test:1234") 77 | s3 = server("jamis@www.capistrano.test:8075") 78 | s4 = server("billy@www.capistrano.test:8080") 79 | 80 | assert s2 < s1 81 | assert s3 < s1 82 | assert s4 < s1 83 | assert s2 < s3 84 | assert s2 < s4 85 | assert s3 < s4 86 | end 87 | 88 | def test_comparison_should_not_match_when_any_of_host_user_port_differ 89 | s1 = server("jamis@www.capistrano.test:8080") 90 | s2 = server("bob@www.capistrano.test:8080") 91 | s3 = server("jamis@www.capistrano.test:8081") 92 | s4 = server("jamis@app.capistrano.test:8080") 93 | assert_not_equal s1, s2 94 | assert_not_equal s1, s3 95 | assert_not_equal s1, s4 96 | assert_not_equal s2, s3 97 | assert_not_equal s2, s4 98 | assert_not_equal s3, s4 99 | end 100 | 101 | def test_to_s 102 | assert_equal "www.capistrano.test", server("www.capistrano.test").to_s 103 | assert_equal "www.capistrano.test", server("www.capistrano.test:22").to_s 104 | assert_equal "www.capistrano.test:1234", server("www.capistrano.test:1234").to_s 105 | assert_equal "jamis@www.capistrano.test", server("jamis@www.capistrano.test").to_s 106 | assert_equal "jamis@www.capistrano.test:1234", server("jamis@www.capistrano.test:1234").to_s 107 | end 108 | end -------------------------------------------------------------------------------- /test/shell_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/configuration' 3 | require 'capistrano/shell' 4 | 5 | class ShellTest < Test::Unit::TestCase 6 | def setup 7 | @config = Capistrano::Configuration.new 8 | @shell = Capistrano::Shell.new(@config) 9 | @shell.stubs(:puts) 10 | end 11 | 12 | def test_readline_fallback_prompt_should_write_to_stdout_and_read_from_stdin 13 | STDOUT.expects(:print).with("prompt> ") 14 | STDOUT.expects(:flush) 15 | STDIN.expects(:gets).returns("hi\n") 16 | assert_equal "hi\n", Capistrano::Shell::ReadlineFallback.readline("prompt> ") 17 | end 18 | 19 | def test_question_mark_as_input_should_trigger_help 20 | @shell.expects(:read_line).returns("?") 21 | @shell.expects(:help) 22 | assert @shell.read_and_execute 23 | end 24 | 25 | def test_help_as_input_should_trigger_help 26 | @shell.expects(:read_line).returns("help") 27 | @shell.expects(:help) 28 | assert @shell.read_and_execute 29 | end 30 | 31 | def test_quit_as_input_should_cause_read_and_execute_to_return_false 32 | @shell.expects(:read_line).returns("quit") 33 | assert !@shell.read_and_execute 34 | end 35 | 36 | def test_exit_as_input_should_cause_read_and_execute_to_return_false 37 | @shell.expects(:read_line).returns("exit") 38 | assert !@shell.read_and_execute 39 | end 40 | 41 | def test_set_should_parse_flag_and_value_and_call_set_option 42 | @shell.expects(:read_line).returns("set -v 5") 43 | @shell.expects(:set_option).with("v", "5") 44 | assert @shell.read_and_execute 45 | end 46 | 47 | def test_text_without_with_or_on_gets_processed_verbatim 48 | @shell.expects(:read_line).returns("hello world") 49 | @shell.expects(:process_command).with(nil, nil, "hello world") 50 | assert @shell.read_and_execute 51 | end 52 | 53 | def test_text_with_with_gets_processed_with_with # lol 54 | @shell.expects(:read_line).returns("with app,db hello world") 55 | @shell.expects(:process_command).with("with", "app,db", "hello world") 56 | assert @shell.read_and_execute 57 | end 58 | 59 | def test_text_with_on_gets_processed_with_on 60 | @shell.expects(:read_line).returns("on app,db hello world") 61 | @shell.expects(:process_command).with("on", "app,db", "hello world") 62 | assert @shell.read_and_execute 63 | end 64 | 65 | def test_task_command_with_bang_gets_processed_by_exec_tasks 66 | while_testing_post_exec_commands do 67 | @shell.expects(:read_line).returns("!deploy") 68 | @shell.expects(:exec_tasks).with(["deploy"]) 69 | assert @shell.read_and_execute 70 | end 71 | end 72 | 73 | def test_normal_command_gets_processed_by_exec_command 74 | while_testing_post_exec_commands do 75 | @shell.expects(:read_line).returns("uptime") 76 | @shell.expects(:exec_command).with("uptime",nil) 77 | @shell.expects(:connect) 78 | assert @shell.read_and_execute 79 | end 80 | end 81 | 82 | 83 | private 84 | 85 | def while_testing_post_exec_commands(&block) 86 | @shell.instance_variable_set(:@mutex,Mutex.new) 87 | yield 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /test/ssh_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/ssh' 3 | 4 | class SSHTest < Test::Unit::TestCase 5 | def setup 6 | @options = { :username => nil, 7 | :password => nil, 8 | :port => 22, 9 | :auth_methods => %w(publickey hostbased) } 10 | @server = server("capistrano") 11 | end 12 | 13 | def test_connect_with_bare_server_without_options_or_config_with_public_key_succeeding_should_only_loop_once 14 | Net::SSH.expects(:start).with(@server.host, @options).returns(success = Object.new) 15 | assert_equal success, Capistrano::SSH.connect(@server) 16 | end 17 | 18 | def test_connect_with_bare_server_without_options_with_public_key_failing_should_try_password 19 | Net::SSH.expects(:start).with(@server.host, @options).raises(Net::SSH::AuthenticationFailed) 20 | Net::SSH.expects(:start).with(@server.host, @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) 21 | assert_equal success, Capistrano::SSH.connect(@server, :password => "f4b13n") 22 | end 23 | 24 | def test_connect_with_bare_server_without_options_public_key_and_password_failing_should_raise_error 25 | Net::SSH.expects(:start).with(@server.host, @options).raises(Net::SSH::AuthenticationFailed) 26 | Net::SSH.expects(:start).with(@server.host, @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).raises(Net::SSH::AuthenticationFailed) 27 | assert_raises(Net::SSH::AuthenticationFailed) do 28 | Capistrano::SSH.connect(@server, :password => "f4b13n") 29 | end 30 | end 31 | 32 | def test_connect_with_bare_server_and_user_via_public_key_should_pass_user_to_net_ssh 33 | Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis")).returns(success = Object.new) 34 | assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis") 35 | end 36 | 37 | def test_connect_with_bare_server_and_user_via_password_should_pass_user_to_net_ssh 38 | Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis")).raises(Net::SSH::AuthenticationFailed) 39 | Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis", :password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) 40 | assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis", :password => "f4b13n") 41 | end 42 | 43 | def test_connect_with_bare_server_with_explicit_port_should_pass_port_to_net_ssh 44 | Net::SSH.expects(:start).with(@server.host, @options.merge(:port => 1234)).returns(success = Object.new) 45 | assert_equal success, Capistrano::SSH.connect(@server, :port => 1234) 46 | end 47 | 48 | def test_connect_with_server_with_user_should_pass_user_to_net_ssh 49 | server = server("jamis@capistrano") 50 | Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis")).returns(success = Object.new) 51 | assert_equal success, Capistrano::SSH.connect(server) 52 | end 53 | 54 | def test_connect_with_server_with_port_should_pass_port_to_net_ssh 55 | server = server("capistrano:1235") 56 | Net::SSH.expects(:start).with(server.host, @options.merge(:port => 1235)).returns(success = Object.new) 57 | assert_equal success, Capistrano::SSH.connect(server) 58 | end 59 | 60 | def test_connect_with_server_with_user_and_port_should_pass_user_and_port_to_net_ssh 61 | server = server("jamis@capistrano:1235") 62 | Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis", :port => 1235)).returns(success = Object.new) 63 | assert_equal success, Capistrano::SSH.connect(server) 64 | end 65 | 66 | def test_connect_with_ssh_options_should_use_ssh_options 67 | ssh_options = { :username => "JamisMan", :port => 8125 } 68 | Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "JamisMan", :port => 8125)).returns(success = Object.new) 69 | assert_equal success, Capistrano::SSH.connect(@server, {:ssh_options => ssh_options}) 70 | end 71 | 72 | def test_connect_with_options_and_ssh_options_should_see_options_override_ssh_options 73 | ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } 74 | Net::SSH.expects(:start).with(@server.host, @options.merge(:username => "jamis", :port => 1235, :forward_agent => true)).returns(success = Object.new) 75 | assert_equal success, Capistrano::SSH.connect(@server, {:ssh_options => ssh_options, :user => "jamis", :port => 1235}) 76 | end 77 | 78 | def test_connect_with_ssh_options_should_see_server_options_override_ssh_options 79 | ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } 80 | server = server("jamis@capistrano:1235") 81 | Net::SSH.expects(:start).with(server.host, @options.merge(:username => "jamis", :port => 1235, :forward_agent => true)).returns(success = Object.new) 82 | assert_equal success, Capistrano::SSH.connect(server, {:ssh_options => ssh_options}) 83 | end 84 | 85 | def test_connect_should_add_xserver_accessor_to_connection 86 | Net::SSH.expects(:start).with(@server.host, @options).returns(success = Object.new) 87 | assert_equal success, Capistrano::SSH.connect(@server) 88 | assert success.respond_to?(:xserver) 89 | assert success.respond_to?(:xserver) 90 | assert_equal success.xserver, @server 91 | end 92 | 93 | def test_connect_should_not_retry_if_custom_auth_methods_are_given 94 | Net::SSH.expects(:start).with(@server.host, @options.merge(:auth_methods => %w(publickey))).raises(Net::SSH::AuthenticationFailed) 95 | assert_raises(Net::SSH::AuthenticationFailed) { Capistrano::SSH.connect(@server, :ssh_options => { :auth_methods => %w(publickey) }) } 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/task_definition_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/task_definition' 3 | 4 | class TaskDefinitionTest < Test::Unit::TestCase 5 | def setup 6 | @namespace = namespace 7 | end 8 | 9 | def test_fqn_at_top_level_should_be_task_name 10 | task = new_task(:testing) 11 | assert_equal "testing", task.fully_qualified_name 12 | end 13 | 14 | def test_fqn_in_namespace_should_include_namespace_fqn 15 | ns = namespace("outer:inner") 16 | task = new_task(:testing, ns) 17 | assert_equal "outer:inner:testing", task.fully_qualified_name 18 | end 19 | 20 | def test_fqn_at_top_level_when_default_should_be_default 21 | task = new_task(:default) 22 | assert_equal "default", task.fully_qualified_name 23 | end 24 | 25 | def test_fqn_in_namespace_when_default_should_be_namespace_fqn 26 | ns = namespace("outer:inner") 27 | task = new_task(:default, ns) 28 | ns.stubs(:default_task => task) 29 | assert_equal "outer:inner", task.fully_qualified_name 30 | end 31 | 32 | def test_task_should_require_block 33 | assert_raises(ArgumentError) do 34 | Capistrano::TaskDefinition.new(:testing, @namespace) 35 | end 36 | end 37 | 38 | def test_description_should_return_empty_string_if_not_given 39 | assert_equal "", new_task(:testing).description 40 | end 41 | 42 | def test_description_should_return_desc_attribute 43 | assert_equal "something", new_task(:testing, @namespace, :desc => "something").description 44 | end 45 | 46 | def test_description_should_strip_leading_and_trailing_whitespace 47 | assert_equal "something", new_task(:testing, @namespace, :desc => " something ").description 48 | end 49 | 50 | def test_description_should_normalize_newlines 51 | assert_equal "a\nb\nc", new_task(:testing, @namespace, :desc => "a\nb\r\nc").description 52 | end 53 | 54 | def test_description_should_detect_and_remove_indentation 55 | desc = <<-DESC 56 | Here is some indented text \ 57 | and I want all of this to \ 58 | run together on a single line, \ 59 | without any extraneous spaces. 60 | 61 | additional indentation will 62 | be preserved. 63 | DESC 64 | 65 | task = new_task(:testing, @namespace, :desc => desc) 66 | assert_equal "Here is some indented text and I want all of this to run together on a single line, without any extraneous spaces.\n\n additional indentation will\n be preserved.", task.description 67 | end 68 | 69 | def test_description_munging_should_be_sensitive_to_code_blocks 70 | desc = <<-DESC 71 | Here is a line \ 72 | wrapped with spacing in it. 73 | 74 | foo bar 75 | baz bang 76 | DESC 77 | 78 | task = new_task(:testing, @namespace, :desc => desc) 79 | assert_equal "Here is a line wrapped with spacing in it.\n\n foo bar\n baz bang", task.description 80 | end 81 | 82 | def test_brief_description_should_return_first_sentence_in_description 83 | desc = "This is the task. It does all kinds of things." 84 | task = new_task(:testing, @namespace, :desc => desc) 85 | assert_equal "This is the task.", task.brief_description 86 | end 87 | 88 | def test_brief_description_should_truncate_if_length_given 89 | desc = "This is the task that does all kinds of things. And then some." 90 | task = new_task(:testing, @namespace, :desc => desc) 91 | assert_equal "This is the task ...", task.brief_description(20) 92 | end 93 | 94 | def test_brief_description_should_not_break_at_period_in_middle_of_sentence 95 | task = new_task(:testing, @namespace, :desc => "Take file.txt and copy it.") 96 | assert_equal "Take file.txt and copy it.", task.brief_description 97 | 98 | task = new_task(:testing, @namespace, :desc => "Take file.txt and copy it. Then do something else.") 99 | assert_equal "Take file.txt and copy it.", task.brief_description 100 | end 101 | end -------------------------------------------------------------------------------- /test/upload_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/upload' 3 | 4 | class UploadTest < Test::Unit::TestCase 5 | def setup 6 | @mode = IO::WRONLY | IO::CREAT | IO::TRUNC 7 | end 8 | 9 | def test_initialize_should_raise_error_if_data_is_missing 10 | assert_raises(ArgumentError) do 11 | Capistrano::Upload.new([], "test.txt", :foo => "bar") 12 | end 13 | end 14 | 15 | def test_initialize_should_get_sftp_for_each_session 16 | new_sftp = Proc.new do |state| 17 | sftp = mock("sftp", :state => state, :open => nil) 18 | sftp.expects(:connect) unless state == :open 19 | sftp.stubs(:channel).returns({}) 20 | sftp 21 | end 22 | 23 | sessions = [mock("session", :xserver => server("a"), :sftp => new_sftp[:closed]), 24 | mock("session", :xserver => server("b"), :sftp => new_sftp[:closed]), 25 | mock("session", :xserver => server("c"), :sftp => new_sftp[:open])] 26 | Capistrano::Upload.new(sessions, "test.txt", :data => "data") 27 | end 28 | 29 | def test_self_process_should_instantiate_uploader_and_start_process 30 | Capistrano::Upload.expects(:new).with([:s1, :s2], "test.txt", :data => "data").returns(mock(:process! => nil)) 31 | Capistrano::Upload.process([:s1, :s2], "test.txt", :data => "data") 32 | end 33 | 34 | def test_process_when_sftp_open_fails_should_raise_error 35 | sftp = mock_sftp 36 | sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status", :code => "bad status", :message => "bad status"), :file_handle) 37 | session = mock("session", :sftp => sftp, :xserver => server("capistrano")) 38 | upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) 39 | assert_raises(Capistrano::UploadError) { upload.process! } 40 | assert_equal 1, upload.failed 41 | assert_equal 1, upload.completed 42 | end 43 | 44 | def test_process_when_sftp_write_fails_should_raise_error 45 | sftp = mock_sftp 46 | sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) 47 | sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status")) 48 | session = mock("session", :sftp => sftp, :xserver => server("capistrano")) 49 | upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) 50 | assert_raises(Capistrano::UploadError) { upload.process! } 51 | assert_equal 1, upload.failed 52 | assert_equal 1, upload.completed 53 | end 54 | 55 | def test_upload_error_should_include_accessor_with_host_array 56 | sftp = mock_sftp 57 | sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) 58 | sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => "bad status", :message => "bad status")) 59 | session = mock("session", :sftp => sftp, :xserver => server("capistrano")) 60 | upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) 61 | 62 | begin 63 | upload.process! 64 | flunk "expected an exception to be raised" 65 | rescue Capistrano::UploadError => e 66 | assert e.respond_to?(:hosts) 67 | assert_equal %w(capistrano), e.hosts.map { |h| h.to_s } 68 | end 69 | end 70 | 71 | def test_process_when_sftp_succeeds_should_raise_nothing 72 | sftp = mock_sftp 73 | sftp.expects(:open).with("test.txt", @mode, 0664).yields(mock("status1", :code => Net::SFTP::Session::FX_OK), :file_handle) 74 | sftp.expects(:write).with(:file_handle, "data").yields(mock("status2", :code => Net::SFTP::Session::FX_OK)) 75 | sftp.expects(:close_handle).with(:file_handle).yields 76 | session = mock("session", :sftp => sftp, :xserver => server("capistrano")) 77 | upload = Capistrano::Upload.new([session], "test.txt", :data => "data", :logger => stub_everything) 78 | assert_nothing_raised { upload.process! } 79 | assert_equal 0, upload.failed 80 | assert_equal 1, upload.completed 81 | end 82 | 83 | def test_process_should_loop_while_running 84 | con = mock("connection") 85 | con.expects(:process).with(true).times(10) 86 | channel = {} 87 | channel.expects(:connection).returns(con).times(10) 88 | sftp = mock("sftp", :state => :open, :open => nil) 89 | sftp.stubs(:channel).returns(channel) 90 | session = mock("session", :sftp => sftp, :xserver => server("capistrano")) 91 | upload = Capistrano::Upload.new([session], "test.txt", :data => "data") 92 | upload.expects(:running?).times(11).returns(*([true]*10 + [false])) 93 | upload.process! 94 | end 95 | 96 | def test_process_should_loop_but_not_process_done_channels 97 | new_sftp = Proc.new do |done| 98 | channel = {} 99 | channel[:needs_done] = done 100 | 101 | if !done 102 | con = mock("connection") 103 | con.expects(:process).with(true).times(10) 104 | channel.expects(:connection).returns(con).times(10) 105 | end 106 | 107 | sftp = mock("sftp", :state => :open, :open => nil) 108 | sftp.stubs(:channel).returns(channel) 109 | sftp 110 | end 111 | 112 | sessions = [stub("session", :sftp => new_sftp[true], :xserver => server("capistrano")), 113 | stub("session", :sftp => new_sftp[false], :xserver => server("cap2"))] 114 | upload = Capistrano::Upload.new(sessions, "test.txt", :data => "data") 115 | 116 | # make sure the sftp channels we wanted to be done, start as done 117 | # (Upload.new marks each channel as not-done, so we have to do it here) 118 | sessions.each { |s| s.sftp.channel[:done] = true if s.sftp.channel[:needs_done] } 119 | upload.expects(:running?).times(11).returns(*([true]*10 + [false])) 120 | upload.process! 121 | end 122 | 123 | private 124 | 125 | def mock_sftp 126 | sftp = mock("sftp", :state => :open) 127 | sftp.stubs(:channel).returns(Hash.new) 128 | yield sftp if block_given? 129 | sftp 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /test/utils.rb: -------------------------------------------------------------------------------- 1 | unless defined?(TestExtensions) 2 | $:.unshift "#{File.dirname(__FILE__)}/../lib" 3 | 4 | begin 5 | require 'rubygems' 6 | gem 'mocha' 7 | rescue LoadError 8 | end 9 | 10 | require 'test/unit' 11 | require 'mocha' 12 | require 'capistrano/server_definition' 13 | 14 | module TestExtensions 15 | def server(host, options={}) 16 | Capistrano::ServerDefinition.new(host, options) 17 | end 18 | 19 | def namespace(fqn=nil) 20 | space = stub(:roles => {}, :fully_qualified_name => fqn, :default_task => nil) 21 | yield(space) if block_given? 22 | space 23 | end 24 | 25 | def role(space, name, *args) 26 | opts = args.last.is_a?(Hash) ? args.pop : {} 27 | space.roles[name] ||= [] 28 | space.roles[name].concat(args.map { |h| Capistrano::ServerDefinition.new(h, opts) }) 29 | end 30 | 31 | def new_task(name, namespace=@namespace, options={}, &block) 32 | block ||= Proc.new {} 33 | task = Capistrano::TaskDefinition.new(name, namespace, options, &block) 34 | assert_equal block, task.body 35 | return task 36 | end 37 | end 38 | 39 | class Test::Unit::TestCase 40 | include TestExtensions 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/version_test.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/utils" 2 | require 'capistrano/version' 3 | 4 | class VersionTest < Test::Unit::TestCase 5 | def test_check_should_return_true_for_matching_parameters 6 | assert Capistrano::Version.check([2], [2]) 7 | assert Capistrano::Version.check([2,1], [2,1]) 8 | assert Capistrano::Version.check([2,1,5], [2,1,5]) 9 | end 10 | 11 | def test_check_should_return_true_if_first_is_less_than_second 12 | assert Capistrano::Version.check([2], [3]) 13 | assert Capistrano::Version.check([2], [2,1]) 14 | assert Capistrano::Version.check([2,1], [2,2]) 15 | assert Capistrano::Version.check([2,1], [2,1,1]) 16 | end 17 | 18 | def test_check_should_return_false_if_first_is_greater_than_second 19 | assert !Capistrano::Version.check([3], [2]) 20 | assert !Capistrano::Version.check([3,1], [3]) 21 | assert !Capistrano::Version.check([3,2], [3,1]) 22 | assert !Capistrano::Version.check([3,2,1], [3,2]) 23 | end 24 | end 25 | --------------------------------------------------------------------------------