diff options
author | Carlhuda <carlhuda@engineyard.com> | 2010-01-11 15:21:45 -0800 |
---|---|---|
committer | Carlhuda <carlhuda@engineyard.com> | 2010-01-11 15:21:45 -0800 |
commit | 67859e42a04e8f77a1d62779b1f2f2e34e4b62e8 (patch) | |
tree | d7cb6be0051d10bf8c36aa2a265e286b402beb09 | |
parent | 2f424171ced5b852c2ab87336fa512df25998446 (diff) | |
download | bundler-67859e42a04e8f77a1d62779b1f2f2e34e4b62e8.tar.gz |
Moving in thor
45 files changed, 4901 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..62521fedc6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +tmp +pkg
\ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..41decca113 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2009 Engine Yard + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000000..957e6a0adf --- /dev/null +++ b/Rakefile @@ -0,0 +1,46 @@ +$:.unshift File.expand_path("../lib", __FILE__) + +require 'rubygems' +require 'rubygems/specification' +require 'bubble' + +spec = Gem::Specification.new do |s| + s.name = "bubble" + s.version = Bubble::VERSION + s.authors = ["Carl Lerche", "Yehuda Katz"] + s.email = ["carlhuda@engineyard.com"] + s.homepage = "http://github.com/carlhuda/bubble" + s.summary = "Bubbles are fun" + + s.platform = Gem::Platform::RUBY + + s.required_rubygems_version = ">= 1.3.5" + + s.files = Dir.glob("{bin,lib}/**/*") + %w(LICENSE README) + s.executables = ['bbl'] + s.require_path = 'lib' +end + +begin + require 'spec/rake/spectask' +rescue LoadError + task :spec do + $stderr.puts '`gem install rspec` to run specs' + end +else + desc "Run specs" + Spec::Rake::SpecTask.new do |t| + t.spec_files = FileList['spec/**/*_spec.rb'] + t.spec_opts = %w(-fs --color) + t.warning = true + end +end + +desc "create a gemspec file" +task :gemspec do + File.open("#{spec.name}.gemspec", 'w') do |file| + file.puts spec.to_ruby + end +end + +task :default => :spec
\ No newline at end of file diff --git a/lib/bubble.rb b/lib/bubble.rb new file mode 100644 index 0000000000..fa055f9864 --- /dev/null +++ b/lib/bubble.rb @@ -0,0 +1,20 @@ +module Bubble + VERSION = "0.9.0.pre" + + autoload :Definition, 'bubble/definition' + autoload :Dependency, 'bubble/dependency' + autoload :Dsl, 'bubble/dsl' + autoload :Environment, 'bubble/environment' + + class GemfileNotFound < StandardError; end + class GemNotFound < StandardError; end + class VersionConflict < StandardError; end + + def self.load(gemfile = nil) + Environment.new(definition(gemfile)) + end + + def self.definition(gemfile = nil) + Definition.from_gemfile(gemfile) + end +end
\ No newline at end of file diff --git a/lib/bubble/definition.rb b/lib/bubble/definition.rb new file mode 100644 index 0000000000..68b442babf --- /dev/null +++ b/lib/bubble/definition.rb @@ -0,0 +1,33 @@ +module Bubble + class Definition + def self.from_gemfile(gemfile) + gemfile = Pathname.new(gemfile || default_gemfile).expand_path + + unless gemfile.file? + raise GemfileNotFound, "`#{gemfile}` not found" + end + + definition = new + Dsl.evaluate(gemfile, definition) + definition + end + + def self.default_gemfile + current = Pathname.new(Dir.pwd) + + until current.root? + filename = current.join("Gemfile") + return filename if filename.exist? + current = current.parent + end + + raise GemfileNotFound, "the default Gemfile was not found" + end + + attr_reader :dependencies + + def initialize + @dependencies = [] + end + end +end
\ No newline at end of file diff --git a/lib/bubble/dependency.rb b/lib/bubble/dependency.rb new file mode 100644 index 0000000000..80fe2292e7 --- /dev/null +++ b/lib/bubble/dependency.rb @@ -0,0 +1,13 @@ +require 'rubygems/dependency' + +module Bubble + class Dependency < Gem::Dependency + def initialize(name, version, options = {}, &blk) + options.each do |k, v| + options[k.to_s] = v + end + + super(name, version) + end + end +end
\ No newline at end of file diff --git a/lib/bubble/dsl.rb b/lib/bubble/dsl.rb new file mode 100644 index 0000000000..e900532f11 --- /dev/null +++ b/lib/bubble/dsl.rb @@ -0,0 +1,20 @@ +module Bubble + class Dsl + def self.evaluate(gemfile, definition) + builder = new(definition) + builder.instance_eval(File.read(gemfile.to_s), gemfile.to_s, 1) + definition + end + + def initialize(definition) + @definition = definition + end + + def gem(name, *args) + options = Hash === args.last ? args.pop : {} + version = args.last || ">= 0" + + @definition.dependencies << Dependency.new(name, version, options) + end + end +end
\ No newline at end of file diff --git a/lib/bubble/environment.rb b/lib/bubble/environment.rb new file mode 100644 index 0000000000..e271c8a832 --- /dev/null +++ b/lib/bubble/environment.rb @@ -0,0 +1,15 @@ +module Bubble + class Environment + def initialize(definition) + @definition = definition + end + + def dependencies + @definition.dependencies + end + + def gems + [] + end + end +end
\ No newline at end of file diff --git a/lib/bubble/resolver.rb b/lib/bubble/resolver.rb new file mode 100644 index 0000000000..431ff80ce1 --- /dev/null +++ b/lib/bubble/resolver.rb @@ -0,0 +1,246 @@ +# This is the latest iteration of the gem dependency resolving algorithm. As of now, +# it can resolve (as a success of failure) any set of gem dependencies we throw at it +# in a reasonable amount of time. The most iterations I've seen it take is about 150. +# The actual implementation of the algorithm is not as good as it could be yet, but that +# can come later. + +# Extending Gem classes to add necessary tracking information +module Gem + class Dependency + def required_by + @required_by ||= [] + end + end + class Specification + def required_by + @required_by ||= [] + end + end +end + +module Bubble + class Resolver + + attr_reader :errors + + # Figures out the best possible configuration of gems that satisfies + # the list of passed dependencies and any child dependencies without + # causing any gem activation errors. + # + # ==== Parameters + # *dependencies<Gem::Dependency>:: The list of dependencies to resolve + # + # ==== Returns + # <GemBundle>,nil:: If the list of dependencies can be resolved, a + # collection of gemspecs is returned. Otherwise, nil is returned. + def self.resolve(requirements, sources) + source_requirements = {} + + requirements.each do |r| + next unless r.source + source_requirements[r.name] = r.source + end + + resolver = new(sources, source_requirements) + result = catch(:success) do + resolver.resolve(requirements, {}) + output = resolver.errors.inject("") do |o, (conflict, (origin, requirement))| + o << " Conflict on: #{conflict.inspect}:\n" + o << " * #{conflict} (#{origin.version}) activated by #{origin.required_by.first}\n" + o << " * #{requirement} required by #{requirement.required_by.first}\n" + o << " All possible versions of origin requirements conflict." + end + raise VersionConflict, "No compatible versions could be found for required dependencies:\n #{output}" + nil + end + if result + # Order gems in order of dependencies. Every gem's dependency is at + # a smaller index in the array. + ordered = [] + result.values.each do |spec1| + index = nil + place = ordered.detect do |spec2| + spec1.dependencies.any? { |d| d.name == spec2.name } + end + place ? + ordered.insert(ordered.index(place), spec1) : + ordered << spec1 + end + ordered.reverse + end + end + + def initialize(sources, source_requirements) + @errors = {} + @stack = [] + @specs = Hash.new { |h,k| h[k] = [] } + @by_gem = source_requirements + @cache = {} + @index = {} + + sources.each do |source| + source.gems.each do |name, specs| + # Hack to work with a regular Gem::SourceIndex + specs = [specs] unless specs.is_a?(Array) + specs.compact.each do |spec| + next if @specs[spec.name].any? { |s| s.version == spec.version && s.platform == spec.platform } + @specs[spec.name] << spec + end + end + end + end + + def debug + puts yield if defined?($debug) && $debug + end + + def resolve(reqs, activated) + # If the requirements are empty, then we are in a success state. Aka, all + # gem dependencies have been resolved. + throw :success, activated if reqs.empty? + + debug { STDIN.gets ; print "\e[2J\e[f" ; "==== Iterating ====\n\n" } + + # Sort dependencies so that the ones that are easiest to resolve are first. + # Easiest to resolve is defined by: + # 1) Is this gem already activated? + # 2) Do the version requirements include prereleased gems? + # 3) Sort by number of gems available in the source. + reqs = reqs.sort_by do |a| + [ activated[a.name] ? 0 : 1, + a.version_requirements.prerelease? ? 0 : 1, + @errors[a.name] ? 0 : 1, + activated[a.name] ? 0 : search(a).size ] + end + + debug { "Activated:\n" + activated.values.map { |a| " #{a.name} (#{a.version})" }.join("\n") } + debug { "Requirements:\n" + reqs.map { |r| " #{r.name} (#{r.version_requirements})"}.join("\n") } + + activated = activated.dup + # Pull off the first requirement so that we can resolve it + current = reqs.shift + + debug { "Attempting:\n #{current.name} (#{current.version_requirements})"} + + # Check if the gem has already been activated, if it has, we will make sure + # that the currently activated gem satisfies the requirement. + if existing = activated[current.name] + if current.version_requirements.satisfied_by?(existing.version) + debug { " * [SUCCESS] Already activated" } + @errors.delete(existing.name) + # Since the current requirement is satisfied, we can continue resolving + # the remaining requirements. + resolve(reqs, activated) + else + debug { " * [FAIL] Already activated" } + @errors[existing.name] = [existing, current] + debug { current.required_by.map {|d| " * #{d.name} (#{d.version_requirements})" }.join("\n") } + # debug { " * All current conflicts:\n" + @errors.keys.map { |c| " - #{c}" }.join("\n") } + # Since the current requirement conflicts with an activated gem, we need + # to backtrack to the current requirement's parent and try another version + # of it (maybe the current requirement won't be present anymore). If the + # current requirement is a root level requirement, we need to jump back to + # where the conflicting gem was activated. + parent = current.required_by.last || existing.required_by.last + # We track the spot where the current gem was activated because we need + # to keep a list of every spot a failure happened. + debug { " -> Jumping to: #{parent.name}" } + throw parent.name, existing.required_by.last.name + end + else + # There are no activated gems for the current requirement, so we are going + # to find all gems that match the current requirement and try them in decending + # order. We also need to keep a set of all conflicts that happen while trying + # this gem. This is so that if no versions work, we can figure out the best + # place to backtrack to. + conflicts = Set.new + + # Fetch all gem versions matching the requirement + # + # TODO: Warn / error when no matching versions are found. + matching_versions = search(current) + + if matching_versions.empty? + if current.required_by.empty? + location = @by_gem[current.name] ? @by_gem[current.name] : "any of the sources" + raise GemNotFound, "Could not find gem '#{current}' in #{location}" + end + Bundler.logger.warn "Could not find gem '#{current}' (required by '#{current.required_by.last}') in any of the sources" + end + + matching_versions.reverse_each do |spec| + conflict = resolve_requirement(spec, current, reqs.dup, activated.dup) + conflicts << conflict if conflict + end + # If the current requirement is a root level gem and we have conflicts, we + # can figure out the best spot to backtrack to. + if current.required_by.empty? && !conflicts.empty? + # Check the current "catch" stack for the first one that is included in the + # conflicts set. That is where the parent of the conflicting gem was required. + # By jumping back to this spot, we can try other version of the parent of + # the conflicting gem, hopefully finding a combination that activates correctly. + @stack.reverse_each do |savepoint| + if conflicts.include?(savepoint) + debug { " -> Jumping to: #{savepoint}" } + throw savepoint + end + end + end + end + end + + def resolve_requirement(spec, requirement, reqs, activated) + # We are going to try activating the spec. We need to keep track of stack of + # requirements that got us to the point of activating this gem. + spec.required_by.replace requirement.required_by + spec.required_by << requirement + + activated[spec.name] = spec + debug { " Activating: #{spec.name} (#{spec.version})" } + debug { spec.required_by.map { |d| " * #{d.name} (#{d.version_requirements})" }.join("\n") } + + # Now, we have to loop through all child dependencies and add them to our + # array of requirements. + debug { " Dependencies"} + spec.dependencies.each do |dep| + next if dep.type == :development + debug { " * #{dep.name} (#{dep.version_requirements})" } + dep.required_by.replace(requirement.required_by) + dep.required_by << requirement + reqs << dep + end + + # We create a savepoint and mark it by the name of the requirement that caused + # the gem to be activated. If the activated gem ever conflicts, we are able to + # jump back to this point and try another version of the gem. + length = @stack.length + @stack << requirement.name + retval = catch(requirement.name) do + resolve(reqs, activated) + end + # Since we're doing a lot of throw / catches. A push does not necessarily match + # up to a pop. So, we simply slice the stack back to what it was before the catch + # block. + @stack.slice!(length..-1) + retval + end + + def search(dependency) + @cache[dependency.hash] ||= begin + pinned = @by_gem[dependency.name].gems if @by_gem[dependency.name] + specs = (pinned || @specs)[dependency.name] + + wants_prerelease = dependency.version_requirements.prerelease? + only_prerelease = specs.all? {|spec| spec.version.prerelease? } + + found = specs.select { |spec| dependency =~ spec } + + unless wants_prerelease || (pinned && only_prerelease) + found.reject! { |spec| spec.version.prerelease? } + end + + found.sort_by {|s| [s.version, s.platform.to_s == 'ruby' ? "\0" : s.platform.to_s] } + end + end + end +end diff --git a/lib/bubble/vendor/thor.rb b/lib/bubble/vendor/thor.rb new file mode 100644 index 0000000000..d4d8fbd64d --- /dev/null +++ b/lib/bubble/vendor/thor.rb @@ -0,0 +1,240 @@ +require 'thor/base' +require 'thor/group' +require 'thor/actions' + +class Thor + class << self + # Sets the default task when thor is executed without an explicit task to be called. + # + # ==== Parameters + # meth<Symbol>:: name of the defaut task + # + def default_task(meth=nil) + case meth + when :none + @default_task = 'help' + when nil + @default_task ||= from_superclass(:default_task, 'help') + else + @default_task = meth.to_s + end + end + + # Defines the usage and the description of the next task. + # + # ==== Parameters + # usage<String> + # description<String> + # + def desc(usage, description, options={}) + if options[:for] + task = find_and_refresh_task(options[:for]) + task.usage = usage if usage + task.description = description if description + else + @usage, @desc = usage, description + end + end + + # Maps an input to a task. If you define: + # + # map "-T" => "list" + # + # Running: + # + # thor -T + # + # Will invoke the list task. + # + # ==== Parameters + # Hash[String|Array => Symbol]:: Maps the string or the strings in the array to the given task. + # + def map(mappings=nil) + @map ||= from_superclass(:map, {}) + + if mappings + mappings.each do |key, value| + if key.respond_to?(:each) + key.each {|subkey| @map[subkey] = value} + else + @map[key] = value + end + end + end + + @map + end + + # Declares the options for the next task to be declared. + # + # ==== Parameters + # Hash[Symbol => Object]:: The hash key is the name of the option and the value + # is the type of the option. Can be :string, :array, :hash, :boolean, :numeric + # or :required (string). If you give a value, the type of the value is used. + # + def method_options(options=nil) + @method_options ||= {} + build_options(options, @method_options) if options + @method_options + end + + # Adds an option to the set of method options. If :for is given as option, + # it allows you to change the options from a previous defined task. + # + # def previous_task + # # magic + # end + # + # method_option :foo => :bar, :for => :previous_task + # + # def next_task + # # magic + # end + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. It cannot be required and have default values. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :banner - String to show on usage notes. + # + def method_option(name, options={}) + scope = if options[:for] + find_and_refresh_task(options[:for]).options + else + method_options + end + + build_option(name, options, scope) + end + + # Parses the task and options from the given args, instantiate the class + # and invoke the task. This method is used when the arguments must be parsed + # from an array. If you are inside Ruby and want to use a Thor class, you + # can simply initialize it: + # + # script = MyScript.new(args, options, config) + # script.invoke(:task, first_arg, second_arg, third_arg) + # + def start(given_args=ARGV, config={}) + super do + meth = normalize_task_name(given_args.shift) + task = all_tasks[meth] + + if task + args, opts = Thor::Options.split(given_args) + config.merge!(:task_options => task.options) + else + args, opts = given_args, {} + end + + task ||= Thor::Task::Dynamic.new(meth) + trailing = args[Range.new(arguments.size, -1)] + new(args, opts, config).invoke(task, trailing || []) + end + end + + # Prints help information for the given task. + # + # ==== Parameters + # shell<Thor::Shell> + # task_name<String> + # + def task_help(shell, task_name) + task = all_tasks[task_name] + raise UndefinedTaskError, "task '#{task_name}' could not be found in namespace '#{self.namespace}'" unless task + + shell.say "Usage:" + shell.say " #{banner(task)}" + shell.say + class_options_help(shell, nil => task.options.map { |_, o| o }) + shell.say task.description + end + + # Prints help information for this class. + # + # ==== Parameters + # shell<Thor::Shell> + # + def help(shell) + list = printable_tasks + Thor::Util.thor_classes_in(self).each do |klass| + list += klass.printable_tasks(false) + end + list.sort!{ |a,b| a[0] <=> b[0] } + + shell.say "Tasks:" + shell.print_table(list, :ident => 2, :truncate => true) + shell.say + class_options_help(shell) + end + + # Returns tasks ready to be printed. + def printable_tasks(all=true) + (all ? all_tasks : tasks).map do |_, task| + item = [] + item << banner(task) + item << (task.description ? "# #{task.description.gsub(/\s+/m,' ')}" : "") + item + end + end + + protected + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. It receives + # the task that is going to be invoked and a boolean which indicates if + # the namespace should be displayed as arguments. + # + def banner(task) + "thor " + task.formatted_usage(self) + end + + def baseclass #:nodoc: + Thor + end + + def create_task(meth) #:nodoc: + if @usage && @desc + tasks[meth.to_s] = Thor::Task.new(meth, @desc, @usage, method_options) + @usage, @desc, @method_options = nil + true + elsif self.all_tasks[meth.to_s] || meth.to_sym == :method_missing + true + else + puts "[WARNING] Attempted to create task #{meth.inspect} without usage or description. " << + "Call desc if you want this method to be available as task or declare it inside a " << + "no_tasks{} block. Invoked from #{caller[1].inspect}." + false + end + end + + def initialize_added #:nodoc: + class_options.merge!(method_options) + @method_options = nil + end + + # Receives a task name (can be nil), and try to get a map from it. + # If a map can't be found use the sent name or the default task. + # + def normalize_task_name(meth) #:nodoc: + mapping = map[meth.to_s] + meth = mapping || meth || default_task + meth.to_s.gsub('-','_') # treat foo-bar > foo_bar + end + end + + include Thor::Base + + map HELP_MAPPINGS => :help + + desc "help [TASK]", "Describe available tasks or one specific task" + def help(task=nil) + task ? self.class.task_help(shell, task) : self.class.help(shell) + end +end diff --git a/lib/bubble/vendor/thor/actions.rb b/lib/bubble/vendor/thor/actions.rb new file mode 100644 index 0000000000..da98444bf2 --- /dev/null +++ b/lib/bubble/vendor/thor/actions.rb @@ -0,0 +1,274 @@ +require 'fileutils' +require 'thor/core_ext/file_binary_read' + +Dir[File.join(File.dirname(__FILE__), "actions", "*.rb")].each do |action| + require action +end + +class Thor + module Actions + attr_accessor :behavior + + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # Hold source paths for one Thor instance. source_paths_for_search is the + # method responsible to gather source_paths from this current class, + # inherited paths and the source root. + # + def source_paths + @source_paths ||= [] + end + + # Returns the source paths in the following order: + # + # 1) This class source paths + # 2) Source root + # 3) Parents source paths + # + def source_paths_for_search + paths = [] + paths += self.source_paths + paths << self.source_root if self.respond_to?(:source_root) + paths += from_superclass(:source_paths, []) + paths + end + + # Add runtime options that help actions execution. + # + def add_runtime_options! + class_option :force, :type => :boolean, :aliases => "-f", :group => :runtime, + :desc => "Overwrite files that already exist" + + class_option :pretend, :type => :boolean, :aliases => "-p", :group => :runtime, + :desc => "Run but do not make any changes" + + class_option :quiet, :type => :boolean, :aliases => "-q", :group => :runtime, + :desc => "Supress status output" + + class_option :skip, :type => :boolean, :aliases => "-s", :group => :runtime, + :desc => "Skip files that already exist" + end + end + + # Extends initializer to add more configuration options. + # + # ==== Configuration + # behavior<Symbol>:: The actions default behavior. Can be :invoke or :revoke. + # It also accepts :force, :skip and :pretend to set the behavior + # and the respective option. + # + # destination_root<String>:: The root directory needed for some actions. + # + def initialize(args=[], options={}, config={}) + self.behavior = case config[:behavior].to_s + when "force", "skip" + _cleanup_options_and_set(options, config[:behavior]) + :invoke + when "revoke" + :revoke + else + :invoke + end + + super + self.destination_root = config[:destination_root] + end + + # Wraps an action object and call it accordingly to the thor class behavior. + # + def action(instance) #:nodoc: + if behavior == :revoke + instance.revoke! + else + instance.invoke! + end + end + + # Returns the root for this thor class (also aliased as destination root). + # + def destination_root + @destination_stack.last + end + + # Sets the root for this thor class. Relatives path are added to the + # directory where the script was invoked and expanded. + # + def destination_root=(root) + @destination_stack ||= [] + @destination_stack[0] = File.expand_path(root || '') + end + + # Returns the given path relative to the absolute root (ie, root where + # the script started). + # + def relative_to_original_destination_root(path, remove_dot=true) + path = path.gsub(@destination_stack[0], '.') + remove_dot ? (path[2..-1] || '') : path + end + + # Holds source paths in instance so they can be manipulated. + # + def source_paths + @source_paths ||= self.class.source_paths_for_search + end + + # Receives a file or directory and search for it in the source paths. + # + def find_in_source_paths(file) + relative_root = relative_to_original_destination_root(destination_root, false) + + source_paths.each do |source| + source_file = File.expand_path(file, File.join(source, relative_root)) + return source_file if File.exists?(source_file) + end + + if source_paths.empty? + raise Error, "You don't have any source path defined for class #{self.class.name}. To fix this, " << + "you can define a source_root in your class." + else + raise Error, "Could not find #{file.inspect} in source paths." + end + end + + # Do something in the root or on a provided subfolder. If a relative path + # is given it's referenced from the current root. The full path is yielded + # to the block you provide. The path is set back to the previous path when + # the method exits. + # + # ==== Parameters + # dir<String>:: the directory to move to. + # config<Hash>:: give :verbose => true to log and use padding. + # + def inside(dir='', config={}, &block) + verbose = config.fetch(:verbose, false) + + say_status :inside, dir, verbose + shell.padding += 1 if verbose + @destination_stack.push File.expand_path(dir, destination_root) + + FileUtils.mkdir_p(destination_root) unless File.exist?(destination_root) + FileUtils.cd(destination_root) { block.arity == 1 ? yield(destination_root) : yield } + + @destination_stack.pop + shell.padding -= 1 if verbose + end + + # Goes to the root and execute the given block. + # + def in_root + inside(@destination_stack.first) { yield } + end + + # Loads an external file and execute it in the instance binding. + # + # ==== Parameters + # path<String>:: The path to the file to execute. Can be a web address or + # a relative path from the source root. + # + # ==== Examples + # + # apply "http://gist.github.com/103208" + # + # apply "recipes/jquery.rb" + # + def apply(path, config={}) + verbose = config.fetch(:verbose, true) + path = find_in_source_paths(path) unless path =~ /^http\:\/\// + + say_status :apply, path, verbose + shell.padding += 1 if verbose + instance_eval(open(path).read) + shell.padding -= 1 if verbose + end + + # Executes a command. + # + # ==== Parameters + # command<String>:: the command to be executed. + # config<Hash>:: give :verbose => false to not log the status. Specify :with + # to append an executable to command executation. + # + # ==== Example + # + # inside('vendor') do + # run('ln -s ~/edge rails') + # end + # + def run(command, config={}) + return unless behavior == :invoke + + destination = relative_to_original_destination_root(destination_root, false) + desc = "#{command} from #{destination.inspect}" + + if config[:with] + desc = "#{File.basename(config[:with].to_s)} #{desc}" + command = "#{config[:with]} #{command}" + end + + say_status :run, desc, config.fetch(:verbose, true) + system(command) unless options[:pretend] + end + + # Executes a ruby script (taking into account WIN32 platform quirks). + # + # ==== Parameters + # command<String>:: the command to be executed. + # config<Hash>:: give :verbose => false to not log the status. + # + def run_ruby_script(command, config={}) + return unless behavior == :invoke + run "#{command}", config.merge(:with => Thor::Util.ruby_command) + end + + # Run a thor command. A hash of options can be given and it's converted to + # switches. + # + # ==== Parameters + # task<String>:: the task to be invoked + # args<Array>:: arguments to the task + # config<Hash>:: give :verbose => false to not log the status. Other options + # are given as parameter to Thor. + # + # ==== Examples + # + # thor :install, "http://gist.github.com/103208" + # #=> thor install http://gist.github.com/103208 + # + # thor :list, :all => true, :substring => 'rails' + # #=> thor list --all --substring=rails + # + def thor(task, *args) + config = args.last.is_a?(Hash) ? args.pop : {} + verbose = config.key?(:verbose) ? config.delete(:verbose) : true + + args.unshift task + args.push Thor::Options.to_switches(config) + command = args.join(' ').strip + + run command, :with => :thor, :verbose => verbose + end + + protected + + # Allow current root to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:destination_root => self.destination_root) + end + + def _cleanup_options_and_set(options, key) #:nodoc: + case options + when Array + %w(--force -f --skip -s).each { |i| options.delete(i) } + options << "--#{key}" + when Hash + [:force, :skip, "force", "skip"].each { |i| options.delete(i) } + options.merge!(key => true) + end + end + + end +end diff --git a/lib/bubble/vendor/thor/actions/create_file.rb b/lib/bubble/vendor/thor/actions/create_file.rb new file mode 100644 index 0000000000..6e0eeb43e2 --- /dev/null +++ b/lib/bubble/vendor/thor/actions/create_file.rb @@ -0,0 +1,103 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Create a new file relative to the destination root with the given data, + # which is the return value of a block or a data string. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # data<String|NilClass>:: the data to append to the file. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # create_file "lib/fun_party.rb" do + # hostname = ask("What is the virtual hostname I should use?") + # "vhost.name = #{hostname}" + # end + # + # create_file "config/apach.conf", "your apache config" + # + def create_file(destination, data=nil, config={}, &block) + action CreateFile.new(self, destination, block || data.to_s, config) + end + alias :add_file :create_file + + # AddFile is a subset of Template, which instead of rendering a file with + # ERB, it gets the content from the user. + # + class CreateFile < EmptyDirectory #:nodoc: + attr_reader :data + + def initialize(base, destination, data, config={}) + @data = data + super(base, destination, config) + end + + # Checks if the content of the file at the destination is identical to the rendered result. + # + # ==== Returns + # Boolean:: true if it is identical, false otherwise. + # + def identical? + exists? && File.binread(destination) == render + end + + # Holds the content to be added to the file. + # + def render + @render ||= if data.is_a?(Proc) + data.call + else + data + end + end + + def invoke! + invoke_with_conflict_check do + FileUtils.mkdir_p(File.dirname(destination)) + File.open(destination, 'wb') { |f| f.write render } + end + given_destination + end + + protected + + # Now on conflict we check if the file is identical or not. + # + def on_conflict_behavior(&block) + if identical? + say_status :identical, :blue + else + options = base.options.merge(config) + force_or_skip_or_conflict(options[:force], options[:skip], &block) + end + end + + # If force is true, run the action, otherwise check if it's not being + # skipped. If both are false, show the file_collision menu, if the menu + # returns true, force it, otherwise skip. + # + def force_or_skip_or_conflict(force, skip, &block) + if force + say_status :force, :yellow + block.call unless pretend? + elsif skip + say_status :skip, :yellow + else + say_status :conflict, :red + force_or_skip_or_conflict(force_on_collision?, true, &block) + end + end + + # Shows the file collision menu to the user and gets the result. + # + def force_on_collision? + base.shell.file_collision(destination){ render } + end + + end + end +end diff --git a/lib/bubble/vendor/thor/actions/directory.rb b/lib/bubble/vendor/thor/actions/directory.rb new file mode 100644 index 0000000000..2e0b459fa3 --- /dev/null +++ b/lib/bubble/vendor/thor/actions/directory.rb @@ -0,0 +1,91 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Copies recursively the files from source directory to root directory. + # If any of the files finishes with .tt, it's considered to be a template + # and is placed in the destination without the extension .tt. If any + # empty directory is found, it's copied and all .empty_directory files are + # ignored. Remember that file paths can also be encoded, let's suppose a doc + # directory with the following files: + # + # doc/ + # components/.empty_directory + # README + # rdoc.rb.tt + # %app_name%.rb + # + # When invoked as: + # + # directory "doc" + # + # It will create a doc directory in the destination with the following + # files (assuming that the app_name is "blog"): + # + # doc/ + # components/ + # README + # rdoc.rb + # blog.rb + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # If :recursive => false, does not look for paths recursively. + # + # ==== Examples + # + # directory "doc" + # directory "doc", "docs", :recursive => false + # + def directory(source, destination=nil, config={}, &block) + action Directory.new(self, source, destination || source, config, &block) + end + + class Directory < EmptyDirectory #:nodoc: + attr_reader :source + + def initialize(base, source, destination=nil, config={}, &block) + @source = File.expand_path(base.find_in_source_paths(source.to_s)) + @block = block + super(base, destination, { :recursive => true }.merge(config)) + end + + def invoke! + base.empty_directory given_destination, config + execute! + end + + def revoke! + execute! + end + + protected + + def execute! + lookup = config[:recursive] ? File.join(source, '**') : source + lookup = File.join(lookup, '{*,.[a-z]*}') + + Dir[lookup].each do |file_source| + next if File.directory?(file_source) + file_destination = File.join(given_destination, file_source.gsub(source, '.')) + file_destination.gsub!('/./', '/') + + case file_source + when /\.empty_directory$/ + dirname = File.dirname(file_destination).gsub(/\/\.$/, '') + next if dirname == given_destination + base.empty_directory(dirname, config) + when /\.tt$/ + destination = base.template(file_source, file_destination[0..-4], config, &@block) + else + destination = base.copy_file(file_source, file_destination, config, &@block) + end + end + end + + end + end +end diff --git a/lib/bubble/vendor/thor/actions/empty_directory.rb b/lib/bubble/vendor/thor/actions/empty_directory.rb new file mode 100644 index 0000000000..484cb820f8 --- /dev/null +++ b/lib/bubble/vendor/thor/actions/empty_directory.rb @@ -0,0 +1,134 @@ +class Thor + module Actions + + # Creates an empty directory. + # + # ==== Parameters + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # empty_directory "doc" + # + def empty_directory(destination, config={}) + action EmptyDirectory.new(self, destination, config) + end + + # Class which holds create directory logic. This is the base class for + # other actions like create_file and directory. + # + # This implementation is based in Templater actions, created by Jonas Nicklas + # and Michael S. Klishin under MIT LICENSE. + # + class EmptyDirectory #:nodoc: + attr_reader :base, :destination, :given_destination, :relative_destination, :config + + # Initializes given the source and destination. + # + # ==== Parameters + # base<Thor::Base>:: A Thor::Base instance + # source<String>:: Relative path to the source of this file + # destination<String>:: Relative path to the destination of this file + # config<Hash>:: give :verbose => false to not log the status. + # + def initialize(base, destination, config={}) + @base, @config = base, { :verbose => true }.merge(config) + self.destination = destination + end + + # Checks if the destination file already exists. + # + # ==== Returns + # Boolean:: true if the file exists, false otherwise. + # + def exists? + ::File.exists?(destination) + end + + def invoke! + invoke_with_conflict_check do + ::FileUtils.mkdir_p(destination) + end + end + + def revoke! + say_status :remove, :red + ::FileUtils.rm_rf(destination) if !pretend? && exists? + given_destination + end + + protected + + # Shortcut for pretend. + # + def pretend? + base.options[:pretend] + end + + # Sets the absolute destination value from a relative destination value. + # It also stores the given and relative destination. Let's suppose our + # script is being executed on "dest", it sets the destination root to + # "dest". The destination, given_destination and relative_destination + # are related in the following way: + # + # inside "bar" do + # empty_directory "baz" + # end + # + # destination #=> dest/bar/baz + # relative_destination #=> bar/baz + # given_destination #=> baz + # + def destination=(destination) + if destination + @given_destination = convert_encoded_instructions(destination.to_s) + @destination = ::File.expand_path(@given_destination, base.destination_root) + @relative_destination = base.relative_to_original_destination_root(@destination) + end + end + + # Filenames in the encoded form are converted. If you have a file: + # + # %class_name%.rb + # + # It gets the class name from the base and replace it: + # + # user.rb + # + def convert_encoded_instructions(filename) + filename.gsub(/%(.*?)%/) do |string| + instruction = $1.strip + base.respond_to?(instruction) ? base.send(instruction) : string + end + end + + # Receives a hash of options and just execute the block if some + # conditions are met. + # + def invoke_with_conflict_check(&block) + if exists? + on_conflict_behavior(&block) + else + say_status :create, :green + block.call unless pretend? + end + + destination + end + + # What to do when the destination file already exists. + # + def on_conflict_behavior(&block) + say_status :exist, :blue + end + + # Shortcut to say_status shell method. + # + def say_status(status, color) + base.shell.say_status status, relative_destination, color if config[:verbose] + end + + end + end +end diff --git a/lib/bubble/vendor/thor/actions/file_manipulation.rb b/lib/bubble/vendor/thor/actions/file_manipulation.rb new file mode 100644 index 0000000000..44d6836c10 --- /dev/null +++ b/lib/bubble/vendor/thor/actions/file_manipulation.rb @@ -0,0 +1,223 @@ +require 'erb' +require 'open-uri' + +class Thor + module Actions + + # Copies the file from the relative source to the relative destination. If + # the destination is not given it's assumed to be equal to the source. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # copy_file "README", "doc/README" + # + # copy_file "doc/README" + # + def copy_file(source, destination=nil, config={}, &block) + destination ||= source + source = File.expand_path(find_in_source_paths(source.to_s)) + + create_file destination, nil, config do + content = File.binread(source) + content = block.call(content) if block + content + end + end + + # Gets the content at the given address and places it at the given relative + # destination. If a block is given instead of destination, the content of + # the url is yielded and used as location. + # + # ==== Parameters + # source<String>:: the address of the given content. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # get "http://gist.github.com/103208", "doc/README" + # + # get "http://gist.github.com/103208" do |content| + # content.split("\n").first + # end + # + def get(source, destination=nil, config={}, &block) + source = File.expand_path(find_in_source_paths(source.to_s)) unless source =~ /^http\:\/\// + render = File.binread(source) + + destination ||= if block_given? + block.arity == 1 ? block.call(render) : block.call + else + File.basename(source) + end + + create_file destination, render, config + end + + # Gets an ERB template at the relative source, executes it and makes a copy + # at the relative destination. If the destination is not given it's assumed + # to be equal to the source removing .tt from the filename. + # + # ==== Parameters + # source<String>:: the relative path to the source root. + # destination<String>:: the relative path to the destination root. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # template "README", "doc/README" + # + # template "doc/README" + # + def template(source, destination=nil, config={}, &block) + destination ||= source + source = File.expand_path(find_in_source_paths(source.to_s)) + context = instance_eval('binding') + + create_file destination, nil, config do + content = ERB.new(::File.binread(source), nil, '-').result(context) + content = block.call(content) if block + content + end + end + + # Changes the mode of the given file or directory. + # + # ==== Parameters + # mode<Integer>:: the file mode + # path<String>:: the name of the file to change mode + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # chmod "script/*", 0755 + # + def chmod(path, mode, config={}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + say_status :chmod, relative_to_original_destination_root(path), config.fetch(:verbose, true) + FileUtils.chmod_R(mode, path) unless options[:pretend] + end + + # Prepend text to a file. Since it depends on inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to prepend to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # prepend_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # prepend_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def prepend_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:after => /\A/) + inject_into_file(path, *(args << config), &block) + end + + # Append text to a file. Since it depends on inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # data<String>:: the data to append to the file, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # append_file 'config/environments/test.rb', 'config.gem "rspec"' + # + # append_file 'config/environments/test.rb' do + # 'config.gem "rspec"' + # end + # + def append_file(path, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:before => /\z/) + inject_into_file(path, *(args << config), &block) + end + + # Injects text right after the class definition. Since it depends on + # inject_into_file, it's reversible. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # klass<String|Class>:: the class to be manipulated + # data<String>:: the data to append to the class, can be also given as a block. + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Examples + # + # inject_into_class "app/controllers/application_controller.rb", " filter_parameter :password\n" + # + # inject_into_class "app/controllers/application_controller.rb", ApplicationController do + # " filter_parameter :password\n" + # end + # + def inject_into_class(path, klass, *args, &block) + config = args.last.is_a?(Hash) ? args.pop : {} + config.merge!(:after => /class #{klass}\n|class #{klass} .*\n/) + inject_into_file(path, *(args << config), &block) + end + + # Run a regular expression replacement on a file. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # flag<Regexp|String>:: the regexp or string to be replaced + # replacement<String>:: the replacement, can be also given as a block + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # gsub_file 'app/controllers/application_controller.rb', /#\s*(filter_parameter_logging :password)/, '\1' + # + # gsub_file 'README', /rake/, :green do |match| + # match << " no more. Use thor!" + # end + # + def gsub_file(path, flag, *args, &block) + return unless behavior == :invoke + config = args.last.is_a?(Hash) ? args.pop : {} + + path = File.expand_path(path, destination_root) + say_status :gsub, relative_to_original_destination_root(path), config.fetch(:verbose, true) + + unless options[:pretend] + content = File.binread(path) + content.gsub!(flag, *args, &block) + File.open(path, 'wb') { |file| file.write(content) } + end + end + + # Removes a file at the given location. + # + # ==== Parameters + # path<String>:: path of the file to be changed + # config<Hash>:: give :verbose => false to not log the status. + # + # ==== Example + # + # remove_file 'README' + # remove_file 'app/controllers/application_controller.rb' + # + def remove_file(path, config={}) + return unless behavior == :invoke + path = File.expand_path(path, destination_root) + + say_status :remove, relative_to_original_destination_root(path), config.fetch(:verbose, true) + ::FileUtils.rm_rf(path) if !options[:pretend] && File.exists?(path) + end + alias :remove_dir :remove_file + + end +end diff --git a/lib/bubble/vendor/thor/actions/inject_into_file.rb b/lib/bubble/vendor/thor/actions/inject_into_file.rb new file mode 100644 index 0000000000..350ab73862 --- /dev/null +++ b/lib/bubble/vendor/thor/actions/inject_into_file.rb @@ -0,0 +1,101 @@ +require 'thor/actions/empty_directory' + +class Thor + module Actions + + # Injects the given content into a file. Different from gsub_file, this + # method is reversible. + # + # ==== Parameters + # destination<String>:: Relative path to the destination root + # data<String>:: Data to add to the file. Can be given as a block. + # config<Hash>:: give :verbose => false to not log the status and the flag + # for injection (:after or :before). + # + # ==== Examples + # + # inject_into_file "config/environment.rb", "config.gem :thor", :after => "Rails::Initializer.run do |config|\n" + # + # inject_into_file "config/environment.rb", :after => "Rails::Initializer.run do |config|\n" do + # gems = ask "Which gems would you like to add?" + # gems.split(" ").map{ |gem| " config.gem :#{gem}" }.join("\n") + # end + # + def inject_into_file(destination, *args, &block) + if block_given? + data, config = block, args.shift + else + data, config = args.shift, args.shift + end + action InjectIntoFile.new(self, destination, data, config) + end + + class InjectIntoFile < EmptyDirectory #:nodoc: + attr_reader :replacement, :flag, :behavior + + def initialize(base, destination, data, config) + super(base, destination, { :verbose => true }.merge(config)) + + @behavior, @flag = if @config.key?(:after) + [:after, @config.delete(:after)] + else + [:before, @config.delete(:before)] + end + + @replacement = data.is_a?(Proc) ? data.call : data + @flag = Regexp.escape(@flag) unless @flag.is_a?(Regexp) + end + + def invoke! + say_status :invoke + + content = if @behavior == :after + '\0' + replacement + else + replacement + '\0' + end + + replace!(/#{flag}/, content) + end + + def revoke! + say_status :revoke + + regexp = if @behavior == :after + content = '\1\2' + /(#{flag})(.*)(#{Regexp.escape(replacement)})/m + else + content = '\2\3' + /(#{Regexp.escape(replacement)})(.*)(#{flag})/m + end + + replace!(regexp, content) + end + + protected + + def say_status(behavior) + status = if flag == /\A/ + behavior == :invoke ? :prepend : :unprepend + elsif flag == /\z/ + behavior == :invoke ? :append : :unappend + else + behavior == :invoke ? :inject : :deinject + end + + super(status, config[:verbose]) + end + + # Adds the content to the file. + # + def replace!(regexp, string) + unless base.options[:pretend] + content = File.binread(destination) + content.gsub!(regexp, string) + File.open(destination, 'wb') { |file| file.write(content) } + end + end + + end + end +end diff --git a/lib/bubble/vendor/thor/base.rb b/lib/bubble/vendor/thor/base.rb new file mode 100644 index 0000000000..aae4cdb89c --- /dev/null +++ b/lib/bubble/vendor/thor/base.rb @@ -0,0 +1,515 @@ +require 'thor/core_ext/hash_with_indifferent_access' +require 'thor/core_ext/ordered_hash' +require 'thor/error' +require 'thor/shell' +require 'thor/invocation' +require 'thor/parser' +require 'thor/task' +require 'thor/util' + +class Thor + # Shortcuts for help. + HELP_MAPPINGS = %w(-h -? --help -D) + + # Thor methods that should not be overwritten by the user. + THOR_RESERVED_WORDS = %w(invoke shell options behavior root destination_root relative_root + action add_file create_file in_root inside run run_ruby_script) + + module Base + attr_accessor :options + + # It receives arguments in an Array and two hashes, one for options and + # other for configuration. + # + # Notice that it does not check if all required arguments were supplied. + # It should be done by the parser. + # + # ==== Parameters + # args<Array[Object]>:: An array of objects. The objects are applied to their + # respective accessors declared with <tt>argument</tt>. + # + # options<Hash>:: An options hash that will be available as self.options. + # The hash given is converted to a hash with indifferent + # access, magic predicates (options.skip?) and then frozen. + # + # config<Hash>:: Configuration for this Thor class. + # + def initialize(args=[], options={}, config={}) + Thor::Arguments.parse(self.class.arguments, args).each do |key, value| + send("#{key}=", value) + end + + parse_options = self.class.class_options + + if options.is_a?(Array) + task_options = config.delete(:task_options) # hook for start + parse_options = parse_options.merge(task_options) if task_options + array_options, hash_options = options, {} + else + array_options, hash_options = [], options + end + + options = Thor::Options.parse(parse_options, array_options) + self.options = Thor::CoreExt::HashWithIndifferentAccess.new(options).merge!(hash_options) + self.options.freeze + end + + class << self + def included(base) #:nodoc: + base.send :extend, ClassMethods + base.send :include, Invocation + base.send :include, Shell + end + + # Returns the classes that inherits from Thor or Thor::Group. + # + # ==== Returns + # Array[Class] + # + def subclasses + @subclasses ||= [] + end + + # Returns the files where the subclasses are kept. + # + # ==== Returns + # Hash[path<String> => Class] + # + def subclass_files + @subclass_files ||= Hash.new{ |h,k| h[k] = [] } + end + + # Whenever a class inherits from Thor or Thor::Group, we should track the + # class and the file on Thor::Base. This is the method responsable for it. + # + def register_klass_file(klass) #:nodoc: + file = caller[1].match(/(.*):\d+/)[1] + Thor::Base.subclasses << klass unless Thor::Base.subclasses.include?(klass) + + file_subclasses = Thor::Base.subclass_files[File.expand_path(file)] + file_subclasses << klass unless file_subclasses.include?(klass) + end + end + + module ClassMethods + attr_accessor :debugging + + # Adds an argument to the class and creates an attr_accessor for it. + # + # Arguments are different from options in several aspects. The first one + # is how they are parsed from the command line, arguments are retrieved + # from position: + # + # thor task NAME + # + # Instead of: + # + # thor task --name=NAME + # + # Besides, arguments are used inside your code as an accessor (self.argument), + # while options are all kept in a hash (self.options). + # + # Finally, arguments cannot have type :default or :boolean but can be + # optional (supplying :optional => :true or :required => false), although + # you cannot have a required argument after a non-required argument. If you + # try it, an error is raised. + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :optional - If the argument is optional or not. + # :type - The type of the argument, can be :string, :hash, :array, :numeric. + # :default - Default value for this argument. It cannot be required and have default values. + # :banner - String to show on usage notes. + # + # ==== Errors + # ArgumentError:: Raised if you supply a required argument after a non required one. + # + def argument(name, options={}) + is_thor_reserved_word?(name, :argument) + no_tasks { attr_accessor name } + + required = if options.key?(:optional) + !options[:optional] + elsif options.key?(:required) + options[:required] + else + options[:default].nil? + end + + remove_argument name + + arguments.each do |argument| + next if argument.required? + raise ArgumentError, "You cannot have #{name.to_s.inspect} as required argument after " << + "the non-required argument #{argument.human_name.inspect}." + end if required + + arguments << Thor::Argument.new(name, options[:desc], required, options[:type], + options[:default], options[:banner]) + end + + # Returns this class arguments, looking up in the ancestors chain. + # + # ==== Returns + # Array[Thor::Argument] + # + def arguments + @arguments ||= from_superclass(:arguments, []) + end + + # Adds a bunch of options to the set of class options. + # + # class_options :foo => false, :bar => :required, :baz => :string + # + # If you prefer more detailed declaration, check class_option. + # + # ==== Parameters + # Hash[Symbol => Object] + # + def class_options(options=nil) + @class_options ||= from_superclass(:class_options, {}) + build_options(options, @class_options) if options + @class_options + end + + # Adds an option to the set of class options + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described below. + # + # ==== Options + # :desc - Description for the argument. + # :required - If the argument is required or not. + # :default - Default value for this argument. + # :group - The group for this options. Use by class options to output options in different levels. + # :aliases - Aliases for this option. + # :type - The type of the argument, can be :string, :hash, :array, :numeric or :boolean. + # :banner - String to show on usage notes. + # + def class_option(name, options={}) + build_option(name, options, class_options) + end + + # Removes a previous defined argument. If :undefine is given, undefine + # accessors as well. + # + # ==== Paremeters + # names<Array>:: Arguments to be removed + # + # ==== Examples + # + # remove_argument :foo + # remove_argument :foo, :bar, :baz, :undefine => true + # + def remove_argument(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + arguments.delete_if { |a| a.name == name.to_s } + undef_method name, "#{name}=" if options[:undefine] + end + end + + # Removes a previous defined class option. + # + # ==== Paremeters + # names<Array>:: Class options to be removed + # + # ==== Examples + # + # remove_class_option :foo + # remove_class_option :foo, :bar, :baz + # + def remove_class_option(*names) + names.each do |name| + class_options.delete(name) + end + end + + # Defines the group. This is used when thor list is invoked so you can specify + # that only tasks from a pre-defined group will be shown. Defaults to standard. + # + # ==== Parameters + # name<String|Symbol> + # + def group(name=nil) + case name + when nil + @group ||= from_superclass(:group, 'standard') + else + @group = name.to_s + end + end + + # Returns the tasks for this Thor class. + # + # ==== Returns + # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # objects as values. + # + def tasks + @tasks ||= Thor::CoreExt::OrderedHash.new + end + + # Returns the tasks for this Thor class and all subclasses. + # + # ==== Returns + # OrderedHash:: An ordered hash with tasks names as keys and Thor::Task + # objects as values. + # + def all_tasks + @all_tasks ||= from_superclass(:all_tasks, Thor::CoreExt::OrderedHash.new) + @all_tasks.merge(tasks) + end + + # Removes a given task from this Thor class. This is usually done if you + # are inheriting from another class and don't want it to be available + # anymore. + # + # By default it only remove the mapping to the task. But you can supply + # :undefine => true to undefine the method from the class as well. + # + # ==== Parameters + # name<Symbol|String>:: The name of the task to be removed + # options<Hash>:: You can give :undefine => true if you want tasks the method + # to be undefined from the class as well. + # + def remove_task(*names) + options = names.last.is_a?(Hash) ? names.pop : {} + + names.each do |name| + tasks.delete(name.to_s) + all_tasks.delete(name.to_s) + undef_method name if options[:undefine] + end + end + + # All methods defined inside the given block are not added as tasks. + # + # So you can do: + # + # class MyScript < Thor + # no_tasks do + # def this_is_not_a_task + # end + # end + # end + # + # You can also add the method and remove it from the task list: + # + # class MyScript < Thor + # def this_is_not_a_task + # end + # remove_task :this_is_not_a_task + # end + # + def no_tasks + @no_tasks = true + yield + @no_tasks = false + end + + # Sets the namespace for the Thor or Thor::Group class. By default the + # namespace is retrieved from the class name. If your Thor class is named + # Scripts::MyScript, the help method, for example, will be called as: + # + # thor scripts:my_script -h + # + # If you change the namespace: + # + # namespace :my_scripts + # + # You change how your tasks are invoked: + # + # thor my_scripts -h + # + # Finally, if you change your namespace to default: + # + # namespace :default + # + # Your tasks can be invoked with a shortcut. Instead of: + # + # thor :my_task + # + def namespace(name=nil) + case name + when nil + @namespace ||= Thor::Util.namespace_from_thor_class(self, false) + else + @namespace = name.to_s + end + end + + # Default way to start generators from the command line. + # + def start(given_args=ARGV, config={}) + self.debugging = given_args.include?("--debug") + config[:shell] ||= Thor::Base.shell.new + yield + rescue Thor::Error => e + if debugging + raise e + else + config[:shell].error e.message + end + exit(1) if exit_on_failure? + end + + protected + + # Prints the class options per group. If an option does not belong to + # any group, it's printed as Class option. + # + def class_options_help(shell, groups={}) #:nodoc: + # Group options by group + class_options.each do |_, value| + groups[value.group] ||= [] + groups[value.group] << value + end + + # Deal with default group + global_options = groups.delete(nil) || [] + print_options(shell, global_options) + + # Print all others + groups.each do |group_name, options| + print_options(shell, options, group_name) + end + end + + # Receives a set of options and print them. + def print_options(shell, options, group_name=nil) + return if options.empty? + + list = [] + padding = options.collect{ |o| o.aliases.size }.max.to_i * 4 + + options.each do |option| + item = [ option.usage(padding) ] + item.push(option.description ? "# #{option.description}" : "") + + list << item + list << [ "", "# Default: #{option.default}" ] if option.show_default? + end + + shell.say(group_name ? "#{group_name} options:" : "Options:") + shell.print_table(list, :ident => 2) + shell.say "" + end + + # Raises an error if the word given is a Thor reserved word. + # + def is_thor_reserved_word?(word, type) #:nodoc: + return false unless THOR_RESERVED_WORDS.include?(word.to_s) + raise "#{word.inspect} is a Thor reserved word and cannot be defined as #{type}" + end + + # Build an option and adds it to the given scope. + # + # ==== Parameters + # name<Symbol>:: The name of the argument. + # options<Hash>:: Described in both class_option and method_option. + # + def build_option(name, options, scope) #:nodoc: + scope[name] = Thor::Option.new(name, options[:desc], options[:required], + options[:type], options[:default], options[:banner], + options[:group], options[:aliases]) + end + + # Receives a hash of options, parse them and add to the scope. This is a + # fast way to set a bunch of options: + # + # build_options :foo => true, :bar => :required, :baz => :string + # + # ==== Parameters + # Hash[Symbol => Object] + # + def build_options(options, scope) #:nodoc: + options.each do |key, value| + scope[key] = Thor::Option.parse(key, value) + end + end + + # Finds a task with the given name. If the task belongs to the current + # class, just return it, otherwise dup it and add the fresh copy to the + # current task hash. + # + def find_and_refresh_task(name) #:nodoc: + task = if task = tasks[name.to_s] + task + elsif task = all_tasks[name.to_s] + tasks[name.to_s] = task.clone + else + raise ArgumentError, "You supplied :for => #{name.inspect}, but the task #{name.inspect} could not be found." + end + end + + # Everytime someone inherits from a Thor class, register the klass + # and file into baseclass. + # + def inherited(klass) + Thor::Base.register_klass_file(klass) + end + + # Fire this callback whenever a method is added. Added methods are + # tracked as tasks by invoking the create_task method. + # + def method_added(meth) + meth = meth.to_s + + if meth == "initialize" + initialize_added + return + end + + # Return if it's not a public instance method + return unless public_instance_methods.include?(meth) || + public_instance_methods.include?(meth.to_sym) + + return if @no_tasks || !create_task(meth) + + is_thor_reserved_word?(meth, :task) + Thor::Base.register_klass_file(self) + end + + # Retrieves a value from superclass. If it reaches the baseclass, + # returns default. + # + def from_superclass(method, default=nil) + if self == baseclass || !superclass.respond_to?(method, true) + default + else + value = superclass.send(method) + value.dup if value + end + end + + # A flag that makes the process exit with status 1 if any error happens. + # + def exit_on_failure? + false + end + + # SIGNATURE: Sets the baseclass. This is where the superclass lookup + # finishes. + def baseclass #:nodoc: + end + + # SIGNATURE: Creates a new task if valid_task? is true. This method is + # called when a new method is added to the class. + def create_task(meth) #:nodoc: + end + + # SIGNATURE: Defines behavior when the initialize method is added to the + # class. + def initialize_added #:nodoc: + end + end + end +end diff --git a/lib/bubble/vendor/thor/core_ext/file_binary_read.rb b/lib/bubble/vendor/thor/core_ext/file_binary_read.rb new file mode 100644 index 0000000000..d6af7e44b0 --- /dev/null +++ b/lib/bubble/vendor/thor/core_ext/file_binary_read.rb @@ -0,0 +1,9 @@ +class File #:nodoc: + + unless File.respond_to?(:binread) + def self.binread(file) + File.open(file, 'rb') { |f| f.read } + end + end + +end diff --git a/lib/bubble/vendor/thor/core_ext/hash_with_indifferent_access.rb b/lib/bubble/vendor/thor/core_ext/hash_with_indifferent_access.rb new file mode 100644 index 0000000000..78bc5cf4bf --- /dev/null +++ b/lib/bubble/vendor/thor/core_ext/hash_with_indifferent_access.rb @@ -0,0 +1,75 @@ +class Thor + module CoreExt #:nodoc: + + # A hash with indifferent access and magic predicates. + # + # hash = Thor::CoreExt::HashWithIndifferentAccess.new 'foo' => 'bar', 'baz' => 'bee', 'force' => true + # + # hash[:foo] #=> 'bar' + # hash['foo'] #=> 'bar' + # hash.foo? #=> true + # + class HashWithIndifferentAccess < ::Hash #:nodoc: + + def initialize(hash={}) + super() + hash.each do |key, value| + self[convert_key(key)] = value + end + end + + def [](key) + super(convert_key(key)) + end + + def []=(key, value) + super(convert_key(key), value) + end + + def delete(key) + super(convert_key(key)) + end + + def values_at(*indices) + indices.collect { |key| self[convert_key(key)] } + end + + def merge(other) + dup.merge!(other) + end + + def merge!(other) + other.each do |key, value| + self[convert_key(key)] = value + end + self + end + + protected + + def convert_key(key) + key.is_a?(Symbol) ? key.to_s : key + end + + # Magic predicates. For instance: + # + # options.force? # => !!options['force'] + # options.shebang # => "/usr/lib/local/ruby" + # options.test_framework?(:rspec) # => options[:test_framework] == :rspec + # + def method_missing(method, *args, &block) + method = method.to_s + if method =~ /^(\w+)\?$/ + if args.empty? + !!self[$1] + else + self[$1] == args.first + end + else + self[method] + end + end + + end + end +end diff --git a/lib/bubble/vendor/thor/core_ext/ordered_hash.rb b/lib/bubble/vendor/thor/core_ext/ordered_hash.rb new file mode 100644 index 0000000000..27fea5bb35 --- /dev/null +++ b/lib/bubble/vendor/thor/core_ext/ordered_hash.rb @@ -0,0 +1,100 @@ +class Thor + module CoreExt #:nodoc: + + if RUBY_VERSION >= '1.9' + class OrderedHash < ::Hash + end + else + # This class is based on the Ruby 1.9 ordered hashes. + # + # It keeps the semantics and most of the efficiency of normal hashes + # while also keeping track of the order in which elements were set. + # + class OrderedHash #:nodoc: + include Enumerable + + Node = Struct.new(:key, :value, :next, :prev) + + def initialize + @hash = {} + end + + def [](key) + @hash[key] && @hash[key].value + end + + def []=(key, value) + if node = @hash[key] + node.value = value + else + node = Node.new(key, value) + + if @first.nil? + @first = @last = node + else + node.prev = @last + @last.next = node + @last = node + end + end + + @hash[key] = node + value + end + + def delete(key) + if node = @hash[key] + prev_node = node.prev + next_node = node.next + + next_node.prev = prev_node if next_node + prev_node.next = next_node if prev_node + + @first = next_node if @first == node + @last = prev_node if @last == node + + value = node.value + end + + @hash.delete(key) + value + end + + def keys + self.map { |k, v| k } + end + + def values + self.map { |k, v| v } + end + + def each + return unless @first + yield [@first.key, @first.value] + node = @first + yield [node.key, node.value] while node = node.next + self + end + + def merge(other) + hash = self.class.new + + self.each do |key, value| + hash[key] = value + end + + other.each do |key, value| + hash[key] = value + end + + hash + end + + def empty? + @hash.empty? + end + end + end + + end +end diff --git a/lib/bubble/vendor/thor/error.rb b/lib/bubble/vendor/thor/error.rb new file mode 100644 index 0000000000..f9b31a35d1 --- /dev/null +++ b/lib/bubble/vendor/thor/error.rb @@ -0,0 +1,27 @@ +class Thor + # Thor::Error is raised when it's caused by wrong usage of thor classes. Those + # errors have their backtrace supressed and are nicely shown to the user. + # + # Errors that are caused by the developer, like declaring a method which + # overwrites a thor keyword, it SHOULD NOT raise a Thor::Error. This way, we + # ensure that developer errors are shown with full backtrace. + # + class Error < StandardError + end + + # Raised when a task was not found. + # + class UndefinedTaskError < Error + end + + # Raised when a task was found, but not invoked properly. + # + class InvocationError < Error + end + + class RequiredArgumentMissingError < InvocationError + end + + class MalformattedArgumentError < InvocationError + end +end diff --git a/lib/bubble/vendor/thor/group.rb b/lib/bubble/vendor/thor/group.rb new file mode 100644 index 0000000000..a585b37b73 --- /dev/null +++ b/lib/bubble/vendor/thor/group.rb @@ -0,0 +1,267 @@ +# Thor has a special class called Thor::Group. The main difference to Thor class +# is that it invokes all tasks at once. It also include some methods that allows +# invocations to be done at the class method, which are not available to Thor +# tasks. +# +class Thor::Group + class << self + # The descrition for this Thor::Group. If none is provided, but a source root + # exists, tries to find the USAGE one folder above it, otherwise searches + # in the superclass. + # + # ==== Parameters + # description<String>:: The description for this Thor::Group. + # + def desc(description=nil) + case description + when nil + @desc ||= from_superclass(:desc, nil) + else + @desc = description + end + end + + # Start works differently in Thor::Group, it simply invokes all tasks + # inside the class. + # + def start(given_args=ARGV, config={}) + super do + if Thor::HELP_MAPPINGS.include?(given_args.first) + help(config[:shell]) + return + end + + args, opts = Thor::Options.split(given_args) + new(args, opts, config).invoke + end + end + + # Prints help information. + # + # ==== Options + # short:: When true, shows only usage. + # + def help(shell) + shell.say "Usage:" + shell.say " #{banner}\n" + shell.say + class_options_help(shell) + shell.say self.desc if self.desc + end + + # Stores invocations for this class merging with superclass values. + # + def invocations #:nodoc: + @invocations ||= from_superclass(:invocations, {}) + end + + # Stores invocation blocks used on invoke_from_option. + # + def invocation_blocks #:nodoc: + @invocation_blocks ||= from_superclass(:invocation_blocks, {}) + end + + # Invoke the given namespace or class given. It adds an instance + # method that will invoke the klass and task. You can give a block to + # configure how it will be invoked. + # + # The namespace/class given will have its options showed on the help + # usage. Check invoke_from_option for more information. + # + def invoke(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, true) + + names.each do |name| + invocations[name] = false + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_#{name.to_s.gsub(/\W/, '_')} + klass, task = self.class.prepare_for_invocation(nil, #{name.inspect}) + + if klass + say_status :invoke, #{name.inspect}, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, task, &block + else + say_status :error, %(#{name.inspect} [not found]), :red + end + end + METHOD + end + end + + # Invoke a thor class based on the value supplied by the user to the + # given option named "name". A class option must be created before this + # method is invoked for each name given. + # + # ==== Examples + # + # class GemGenerator < Thor::Group + # class_option :test_framework, :type => :string + # invoke_from_option :test_framework + # end + # + # ==== Boolean options + # + # In some cases, you want to invoke a thor class if some option is true or + # false. This is automatically handled by invoke_from_option. Then the + # option name is used to invoke the generator. + # + # ==== Preparing for invocation + # + # In some cases you want to customize how a specified hook is going to be + # invoked. You can do that by overwriting the class method + # prepare_for_invocation. The class method must necessarily return a klass + # and an optional task. + # + # ==== Custom invocations + # + # You can also supply a block to customize how the option is giong to be + # invoked. The block receives two parameters, an instance of the current + # class and the klass to be invoked. + # + def invoke_from_option(*names, &block) + options = names.last.is_a?(Hash) ? names.pop : {} + verbose = options.fetch(:verbose, :white) + + names.each do |name| + unless class_options.key?(name) + raise ArgumentError, "You have to define the option #{name.inspect} " << + "before setting invoke_from_option." + end + + invocations[name] = true + invocation_blocks[name] = block if block_given? + + class_eval <<-METHOD, __FILE__, __LINE__ + def _invoke_from_option_#{name.to_s.gsub(/\W/, '_')} + return unless options[#{name.inspect}] + + value = options[#{name.inspect}] + value = #{name.inspect} if TrueClass === value + klass, task = self.class.prepare_for_invocation(#{name.inspect}, value) + + if klass + say_status :invoke, value, #{verbose.inspect} + block = self.class.invocation_blocks[#{name.inspect}] + _invoke_for_class_method klass, task, &block + else + say_status :error, %(\#{value} [not found]), :red + end + end + METHOD + end + end + + # Remove a previously added invocation. + # + # ==== Examples + # + # remove_invocation :test_framework + # + def remove_invocation(*names) + names.each do |name| + remove_task(name) + remove_class_option(name) + invocations.delete(name) + invocation_blocks.delete(name) + end + end + + # Overwrite class options help to allow invoked generators options to be + # shown recursively when invoking a generator. + # + def class_options_help(shell, groups={}) #:nodoc: + get_options_from_invocations(groups, class_options) do |klass| + klass.send(:get_options_from_invocations, groups, class_options) + end + super(shell, groups) + end + + # Get invocations array and merge options from invocations. Those + # options are added to group_options hash. Options that already exists + # in base_options are not added twice. + # + def get_options_from_invocations(group_options, base_options) #:nodoc: + invocations.each do |name, from_option| + value = if from_option + option = class_options[name] + option.type == :boolean ? name : option.default + else + name + end + next unless value + + klass, task = prepare_for_invocation(name, value) + next unless klass && klass.respond_to?(:class_options) + + value = value.to_s + human_name = value.respond_to?(:classify) ? value.classify : value + + group_options[human_name] ||= [] + group_options[human_name] += klass.class_options.values.select do |option| + base_options[option.name.to_sym].nil? && option.group.nil? && + !group_options.values.flatten.any? { |i| i.name == option.name } + end + + yield klass if block_given? + end + end + + # Returns tasks ready to be printed. + def printable_tasks(*) + item = [] + item << banner + item << (desc ? "# #{desc.gsub(/\s+/m,' ')}" : "") + [item] + end + + protected + + # The banner for this class. You can customize it if you are invoking the + # thor class by another ways which is not the Thor::Runner. + # + def banner + "thor #{self_task.formatted_usage(self, false)}" + end + + # Represents the whole class as a task. + def self_task #:nodoc: + Thor::Task::Dynamic.new(self.namespace, class_options) + end + + def baseclass #:nodoc: + Thor::Group + end + + def create_task(meth) #:nodoc: + tasks[meth.to_s] = Thor::Task.new(meth, nil, nil, nil) + true + end + end + + include Thor::Base + + protected + + # Shortcut to invoke with padding and block handling. Use internally by + # invoke and invoke_from_option class methods. + def _invoke_for_class_method(klass, task=nil, *args, &block) #:nodoc: + shell.padding += 1 + + result = if block_given? + if block.arity == 2 + block.call(self, klass) + else + block.call(self, klass, task) + end + else + invoke klass, task, *args + end + + shell.padding -= 1 + result + end +end diff --git a/lib/bubble/vendor/thor/invocation.rb b/lib/bubble/vendor/thor/invocation.rb new file mode 100644 index 0000000000..32e6a72454 --- /dev/null +++ b/lib/bubble/vendor/thor/invocation.rb @@ -0,0 +1,178 @@ +class Thor + module Invocation + def self.included(base) #:nodoc: + base.extend ClassMethods + end + + module ClassMethods + # Prepare for class methods invocations. This method must return a klass to + # have the invoked class options showed in help messages in generators. + # + def prepare_for_invocation(key, name) #:nodoc: + case name + when Symbol, String + Thor::Util.namespace_to_thor_class_and_task(name.to_s, false) + else + name + end + end + end + + # Make initializer aware of invocations and the initializer proc. + # + def initialize(args=[], options={}, config={}, &block) #:nodoc: + @_invocations = config[:invocations] || Hash.new { |h,k| h[k] = [] } + @_initializer = [ args, options, config ] + super + end + + # Receives a name and invokes it. The name can be a string (either "task" or + # "namespace:task"), a Thor::Task, a Class or a Thor instance. If the task + # cannot be guessed by name, it can also be supplied as second argument. + # + # You can also supply the arguments, options and configuration values for + # the task to be invoked, if none is given, the same values used to + # initialize the invoker are used to initialize the invoked. + # + # ==== Examples + # + # class A < Thor + # def foo + # invoke :bar + # invoke "b:hello", ["José"] + # end + # + # def bar + # invoke "b:hello", ["José"] + # end + # end + # + # class B < Thor + # def hello(name) + # puts "hello #{name}" + # end + # end + # + # You can notice that the method "foo" above invokes two tasks: "bar", + # which belongs to the same class and "hello" which belongs to the class B. + # + # By using an invocation system you ensure that a task is invoked only once. + # In the example above, invoking "foo" will invoke "b:hello" just once, even + # if it's invoked later by "bar" method. + # + # When class A invokes class B, all arguments used on A initialization are + # supplied to B. This allows lazy parse of options. Let's suppose you have + # some rspec tasks: + # + # class Rspec < Thor::Group + # class_option :mock_framework, :type => :string, :default => :rr + # + # def invoke_mock_framework + # invoke "rspec:#{options[:mock_framework]}" + # end + # end + # + # As you noticed, it invokes the given mock framework, which might have its + # own options: + # + # class Rspec::RR < Thor::Group + # class_option :style, :type => :string, :default => :mock + # end + # + # Since it's not rspec concern to parse mock framework options, when RR + # is invoked all options are parsed again, so RR can extract only the options + # that it's going to use. + # + # If you want Rspec::RR to be initialized with its own set of options, you + # have to do that explicitely: + # + # invoke "rspec:rr", [], :style => :foo + # + # Besides giving an instance, you can also give a class to invoke: + # + # invoke Rspec::RR, [], :style => :foo + # + def invoke(name=nil, task=nil, args=nil, opts=nil, config=nil) + task, args, opts, config = nil, task, args, opts if task.nil? || task.is_a?(Array) + args, opts, config = nil, args, opts if args.is_a?(Hash) + + object, task = _prepare_for_invocation(name, task) + klass, instance = _initialize_klass_with_initializer(object, args, opts, config) + + method_args = [] + current = @_invocations[klass] + + iterator = proc do |_, task| + unless current.include?(task.name) + current << task.name + task.run(instance, method_args) + end + end + + if task + args ||= [] + method_args = args[Range.new(klass.arguments.size, -1)] || [] + iterator.call(nil, task) + else + klass.all_tasks.map(&iterator) + end + end + + protected + + # Configuration values that are shared between invocations. + # + def _shared_configuration #:nodoc: + { :invocations => @_invocations } + end + + # Prepare for invocation in the instance level. In this case, we have to + # take into account that a just a task name from the current class was + # given or even a Thor::Task object. + # + def _prepare_for_invocation(name, sent_task=nil) #:nodoc: + if name.is_a?(Thor::Task) + task = name + elsif task = self.class.all_tasks[name.to_s] + object = self + else + object, task = self.class.prepare_for_invocation(nil, name) + task ||= sent_task + end + + # If the object was not set, use self and use the name as task. + object, task = self, name unless object + return object, _validate_task(object, task) + end + + # Check if the object given is a Thor class object and get a task object + # for it. + # + def _validate_task(object, task) #:nodoc: + klass = object.is_a?(Class) ? object : object.class + raise "Expected Thor class, got #{klass}" unless klass <= Thor::Base + + task ||= klass.default_task if klass <= Thor + task = klass.all_tasks[task.to_s] || Thor::Task::Dynamic.new(task) if task && !task.is_a?(Thor::Task) + task + end + + # Initialize klass using values stored in the @_initializer. + # + def _initialize_klass_with_initializer(object, args, opts, config) #:nodoc: + if object.is_a?(Class) + klass = object + + stored_args, stored_opts, stored_config = @_initializer + args ||= stored_args.dup + opts ||= stored_opts.dup + + config ||= {} + config = stored_config.merge(_shared_configuration).merge!(config) + [ klass, klass.new(args, opts, config) ] + else + [ object.class, object ] + end + end + end +end diff --git a/lib/bubble/vendor/thor/parser.rb b/lib/bubble/vendor/thor/parser.rb new file mode 100644 index 0000000000..57a3f6e1a5 --- /dev/null +++ b/lib/bubble/vendor/thor/parser.rb @@ -0,0 +1,4 @@ +require 'thor/parser/argument' +require 'thor/parser/arguments' +require 'thor/parser/option' +require 'thor/parser/options' diff --git a/lib/bubble/vendor/thor/parser/argument.rb b/lib/bubble/vendor/thor/parser/argument.rb new file mode 100644 index 0000000000..aa8ace4719 --- /dev/null +++ b/lib/bubble/vendor/thor/parser/argument.rb @@ -0,0 +1,67 @@ +class Thor + class Argument #:nodoc: + VALID_TYPES = [ :numeric, :hash, :array, :string ] + + attr_reader :name, :description, :required, :type, :default, :banner + alias :human_name :name + + def initialize(name, description=nil, required=true, type=:string, default=nil, banner=nil) + class_name = self.class.name.split("::").last + + raise ArgumentError, "#{class_name} name can't be nil." if name.nil? + raise ArgumentError, "Type :#{type} is not valid for #{class_name.downcase}s." if type && !valid_type?(type) + + @name = name.to_s + @description = description + @required = required || false + @type = (type || :string).to_sym + @default = default + @banner = banner || default_banner + + validate! # Trigger specific validations + end + + def usage + required? ? banner : "[#{banner}]" + end + + def required? + required + end + + def show_default? + case default + when Array, String, Hash + !default.empty? + else + default + end + end + + protected + + def validate! + raise ArgumentError, "An argument cannot be required and have default value." if required? && !default.nil? + end + + def valid_type?(type) + VALID_TYPES.include?(type.to_sym) + end + + def default_banner + case type + when :boolean + nil + when :string, :default + human_name.upcase + when :numeric + "N" + when :hash + "key:value" + when :array + "one two three" + end + end + + end +end diff --git a/lib/bubble/vendor/thor/parser/arguments.rb b/lib/bubble/vendor/thor/parser/arguments.rb new file mode 100644 index 0000000000..fb5d965e06 --- /dev/null +++ b/lib/bubble/vendor/thor/parser/arguments.rb @@ -0,0 +1,145 @@ +class Thor + class Arguments #:nodoc: + NUMERIC = /(\d*\.\d+|\d+)/ + + # Receives an array of args and returns two arrays, one with arguments + # and one with switches. + # + def self.split(args) + arguments = [] + + args.each do |item| + break if item =~ /^-/ + arguments << item + end + + return arguments, args[Range.new(arguments.size, -1)] + end + + def self.parse(base, args) + new(base).parse(args) + end + + # Takes an array of Thor::Argument objects. + # + def initialize(arguments=[]) + @assigns, @non_assigned_required = {}, [] + @switches = arguments + + arguments.each do |argument| + if argument.default + @assigns[argument.human_name] = argument.default + elsif argument.required? + @non_assigned_required << argument + end + end + end + + def parse(args) + @pile = args.dup + + @switches.each do |argument| + break unless peek + @non_assigned_required.delete(argument) + @assigns[argument.human_name] = send(:"parse_#{argument.type}", argument.human_name) + end + + check_requirement! + @assigns + end + + private + + def peek + @pile.first + end + + def shift + @pile.shift + end + + def unshift(arg) + unless arg.kind_of?(Array) + @pile.unshift(arg) + else + @pile = arg + @pile + end + end + + def current_is_value? + peek && peek.to_s !~ /^-/ + end + + # Runs through the argument array getting strings that contains ":" and + # mark it as a hash: + # + # [ "name:string", "age:integer" ] + # + # Becomes: + # + # { "name" => "string", "age" => "integer" } + # + def parse_hash(name) + return shift if peek.is_a?(Hash) + hash = {} + + while current_is_value? && peek.include?(?:) + key, value = shift.split(':') + hash[key] = value + end + hash + end + + # Runs through the argument array getting all strings until no string is + # found or a switch is found. + # + # ["a", "b", "c"] + # + # And returns it as an array: + # + # ["a", "b", "c"] + # + def parse_array(name) + return shift if peek.is_a?(Array) + array = [] + + while current_is_value? + array << shift + end + array + end + + # Check if the peel is numeric ofrmat and return a Float or Integer. + # Otherwise raises an error. + # + def parse_numeric(name) + return shift if peek.is_a?(Numeric) + + unless peek =~ NUMERIC && $& == peek + raise MalformattedArgumentError, "expected numeric value for '#{name}'; got #{peek.inspect}" + end + + $&.index('.') ? shift.to_f : shift.to_i + end + + # Parse string, i.e., just return the current value in the pile. + # + def parse_string(name) + shift + end + + # Raises an error if @non_assigned_required array is not empty. + # + def check_requirement! + unless @non_assigned_required.empty? + names = @non_assigned_required.map do |o| + o.respond_to?(:switch_name) ? o.switch_name : o.human_name + end.join("', '") + + class_name = self.class.name.split('::').last.downcase + raise RequiredArgumentMissingError, "no value provided for required #{class_name} '#{names}'" + end + end + + end +end diff --git a/lib/bubble/vendor/thor/parser/option.rb b/lib/bubble/vendor/thor/parser/option.rb new file mode 100644 index 0000000000..9e40ec73fa --- /dev/null +++ b/lib/bubble/vendor/thor/parser/option.rb @@ -0,0 +1,132 @@ +class Thor + class Option < Argument #:nodoc: + attr_reader :aliases, :group + + VALID_TYPES = [:boolean, :numeric, :hash, :array, :string] + + def initialize(name, description=nil, required=nil, type=nil, default=nil, banner=nil, group=nil, aliases=nil) + super(name, description, required, type, default, banner) + @aliases = [*aliases].compact + @group = group.to_s.capitalize if group + end + + # This parse quick options given as method_options. It makes several + # assumptions, but you can be more specific using the option method. + # + # parse :foo => "bar" + # #=> Option foo with default value bar + # + # parse [:foo, :baz] => "bar" + # #=> Option foo with default value bar and alias :baz + # + # parse :foo => :required + # #=> Required option foo without default value + # + # parse :foo => 2 + # #=> Option foo with default value 2 and type numeric + # + # parse :foo => :numeric + # #=> Option foo without default value and type numeric + # + # parse :foo => true + # #=> Option foo with default value true and type boolean + # + # The valid types are :boolean, :numeric, :hash, :array and :string. If none + # is given a default type is assumed. This default type accepts arguments as + # string (--foo=value) or booleans (just --foo). + # + # By default all options are optional, unless :required is given. + # + def self.parse(key, value) + if key.is_a?(Array) + name, *aliases = key + else + name, aliases = key, [] + end + + name = name.to_s + default = value + + type = case value + when Symbol + default = nil + + if VALID_TYPES.include?(value) + value + elsif required = (value == :required) + :string + elsif value == :optional + # TODO Remove this warning in the future. + warn "Optional type is deprecated. Choose :boolean or :string instead. Assumed to be :boolean." + :boolean + end + when TrueClass, FalseClass + :boolean + when Numeric + :numeric + when Hash, Array, String + value.class.name.downcase.to_sym + end + + self.new(name.to_s, nil, required, type, default, nil, nil, aliases) + end + + def switch_name + @switch_name ||= dasherized? ? name : dasherize(name) + end + + def human_name + @human_name ||= dasherized? ? undasherize(name) : name + end + + def usage(padding=0) + sample = if banner && !banner.to_s.empty? + "#{switch_name}=#{banner}" + else + switch_name + end + + sample = "[#{sample}]" unless required? + + if aliases.empty? + (" " * padding) << sample + else + "#{aliases.join(', ')}, #{sample}" + end + end + + # Allow some type predicates as: boolean?, string? and etc. + # + def method_missing(method, *args, &block) + given = method.to_s.sub(/\?$/, '').to_sym + if valid_type?(given) + self.type == given + else + super + end + end + + protected + + def validate! + raise ArgumentError, "An option cannot be boolean and required." if boolean? && required? + end + + def valid_type?(type) + VALID_TYPES.include?(type.to_sym) + end + + def dasherized? + name.index('-') == 0 + end + + def undasherize(str) + str.sub(/^-{1,2}/, '') + end + + def dasherize(str) + (str.length > 1 ? "--" : "-") + str.gsub('_', '-') + end + + end +end diff --git a/lib/bubble/vendor/thor/parser/options.rb b/lib/bubble/vendor/thor/parser/options.rb new file mode 100644 index 0000000000..75092308b5 --- /dev/null +++ b/lib/bubble/vendor/thor/parser/options.rb @@ -0,0 +1,142 @@ +class Thor + # This is a modified version of Daniel Berger's Getopt::Long class, licensed + # under Ruby's license. + # + class Options < Arguments #:nodoc: + LONG_RE = /^(--\w+[-\w+]*)$/ + SHORT_RE = /^(-[a-z])$/i + EQ_RE = /^(--\w+[-\w+]*|-[a-z])=(.*)$/i + SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args + SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i + + # Receives a hash and makes it switches. + # + def self.to_switches(options) + options.map do |key, value| + case value + when true + "--#{key}" + when Array + "--#{key} #{value.map{ |v| v.inspect }.join(' ')}" + when Hash + "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}" + when nil, false + "" + else + "--#{key} #{value.inspect}" + end + end.join(" ") + end + + # Takes a hash of Thor::Option objects. + # + def initialize(options={}) + options = options.values + super(options) + @shorts, @switches = {}, {} + + options.each do |option| + @switches[option.switch_name] = option + + option.aliases.each do |short| + @shorts[short.to_s] ||= option.switch_name + end + end + end + + def parse(args) + @pile = args.dup + + while peek + if current_is_switch? + case shift + when SHORT_SQ_RE + unshift($1.split('').map { |f| "-#{f}" }) + next + when EQ_RE, SHORT_NUM + unshift($2) + switch = $1 + when LONG_RE, SHORT_RE + switch = $1 + end + + switch = normalize_switch(switch) + next unless option = switch_option(switch) + + @assigns[option.human_name] = parse_peek(switch, option) + else + shift + end + end + + check_requirement! + @assigns + end + + protected + + # Returns true if the current value in peek is a registered switch. + # + def current_is_switch? + case peek + when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM + switch?($1) + when SHORT_SQ_RE + $1.split('').any? { |f| switch?("-#{f}") } + end + end + + def switch?(arg) + switch_option(arg) || @shorts.key?(arg) + end + + def switch_option(arg) + if match = no_or_skip?(arg) + @switches[arg] || @switches["--#{match}"] + else + @switches[arg] + end + end + + def no_or_skip?(arg) + arg =~ /^--(no|skip)-([-\w]+)$/ + $2 + end + + # Check if the given argument is actually a shortcut. + # + def normalize_switch(arg) + @shorts.key?(arg) ? @shorts[arg] : arg + end + + # Parse boolean values which can be given as --foo=true, --foo or --no-foo. + # + def parse_boolean(switch) + if current_is_value? + ["true", "TRUE", "t", "T", true].include?(shift) + else + @switches.key?(switch) || !no_or_skip?(switch) + end + end + + # Parse the value at the peek analyzing if it requires an input or not. + # + def parse_peek(switch, option) + unless current_is_value? + if option.boolean? + # No problem for boolean types + elsif no_or_skip?(switch) + return nil # User set value to nil + elsif option.string? && !option.required? + return option.human_name # Return the option name + else + raise MalformattedArgumentError, "no value provided for option '#{switch}'" + end + end + + @non_assigned_required.delete(option) + send(:"parse_#{option.type}", switch) + end + + end +end diff --git a/lib/bubble/vendor/thor/rake_compat.rb b/lib/bubble/vendor/thor/rake_compat.rb new file mode 100644 index 0000000000..0d0757fdda --- /dev/null +++ b/lib/bubble/vendor/thor/rake_compat.rb @@ -0,0 +1,66 @@ +require 'rake' + +class Thor + # Adds a compatibility layer to your Thor classes which allows you to use + # rake package tasks. For example, to use rspec rake tasks, one can do: + # + # require 'thor/rake_compat' + # + # class Default < Thor + # include Thor::RakeCompat + # + # Spec::Rake::SpecTask.new(:spec) do |t| + # t.spec_opts = ['--options', "spec/spec.opts"] + # t.spec_files = FileList['spec/**/*_spec.rb'] + # end + # end + # + module RakeCompat + def self.rake_classes + @rake_classes ||= [] + end + + def self.included(base) + # Hack. Make rakefile point to invoker, so rdoc task is generated properly. + rakefile = File.basename(caller[0].match(/(.*):\d+/)[1]) + Rake.application.instance_variable_set(:@rakefile, rakefile) + self.rake_classes << base + end + end +end + +class Object #:nodoc: + alias :rake_task :task + alias :rake_namespace :namespace + + def task(*args, &block) + task = rake_task(*args, &block) + + if klass = Thor::RakeCompat.rake_classes.last + non_namespaced_name = task.name.split(':').last + + description = non_namespaced_name + description << task.arg_names.map{ |n| n.to_s.upcase }.join(' ') + description.strip! + + klass.desc description, task.comment || non_namespaced_name + klass.send :define_method, non_namespaced_name do |*args| + Rake::Task[task.name.to_sym].invoke(*args) + end + end + + task + end + + def namespace(name, &block) + if klass = Thor::RakeCompat.rake_classes.last + const_name = Thor::Util.camel_case(name.to_s).to_sym + klass.const_set(const_name, Class.new(Thor)) + new_klass = klass.const_get(const_name) + Thor::RakeCompat.rake_classes << new_klass + end + + rake_namespace(name, &block) + Thor::RakeCompat.rake_classes.pop + end +end diff --git a/lib/bubble/vendor/thor/runner.rb b/lib/bubble/vendor/thor/runner.rb new file mode 100644 index 0000000000..f197081e3f --- /dev/null +++ b/lib/bubble/vendor/thor/runner.rb @@ -0,0 +1,303 @@ +require 'fileutils' +require 'open-uri' +require 'yaml' +require 'digest/md5' +require 'pathname' + +class Thor::Runner < Thor #:nodoc: + map "-T" => :list, "-i" => :install, "-u" => :update + + # Override Thor#help so it can give information about any class and any method. + # + def help(meth=nil) + if meth && !self.respond_to?(meth) + initialize_thorfiles(meth) + klass, task = Thor::Util.namespace_to_thor_class_and_task(meth) + # Send mapping -h because it works with Thor::Group too + klass.start(["-h", task].compact, :shell => self.shell) + else + super + end + end + + # If a task is not found on Thor::Runner, method missing is invoked and + # Thor::Runner is then responsable for finding the task in all classes. + # + def method_missing(meth, *args) + meth = meth.to_s + initialize_thorfiles(meth) + klass, task = Thor::Util.namespace_to_thor_class_and_task(meth) + args.unshift(task) if task + klass.start(args, :shell => shell) + end + + desc "install NAME", "Install an optionally named Thor file into your system tasks" + method_options :as => :string, :relative => :boolean + def install(name) + initialize_thorfiles + + # If a directory name is provided as the argument, look for a 'main.thor' + # task in said directory. + begin + if File.directory?(File.expand_path(name)) + base, package = File.join(name, "main.thor"), :directory + contents = open(base).read + else + base, package = name, :file + contents = open(name).read + end + rescue OpenURI::HTTPError + raise Error, "Error opening URI '#{name}'" + rescue Errno::ENOENT + raise Error, "Error opening file '#{name}'" + end + + say "Your Thorfile contains:" + say contents + + return false if no?("Do you wish to continue [y/N]?") + + as = options["as"] || begin + first_line = contents.split("\n")[0] + (match = first_line.match(/\s*#\s*module:\s*([^\n]*)/)) ? match[1].strip : nil + end + + unless as + basename = File.basename(name) + as = ask("Please specify a name for #{name} in the system repository [#{basename}]:") + as = basename if as.empty? + end + + location = if options[:relative] || name =~ /^http:\/\// + name + else + File.expand_path(name) + end + + thor_yaml[as] = { + :filename => Digest::MD5.hexdigest(name + as), + :location => location, + :namespaces => Thor::Util.namespaces_in_content(contents, base) + } + + save_yaml(thor_yaml) + say "Storing thor file in your system repository" + destination = File.join(thor_root, thor_yaml[as][:filename]) + + if package == :file + File.open(destination, "w") { |f| f.puts contents } + else + FileUtils.cp_r(name, destination) + end + + thor_yaml[as][:filename] # Indicate success + end + + desc "uninstall NAME", "Uninstall a named Thor module" + def uninstall(name) + raise Error, "Can't find module '#{name}'" unless thor_yaml[name] + say "Uninstalling #{name}." + FileUtils.rm_rf(File.join(thor_root, "#{thor_yaml[name][:filename]}")) + + thor_yaml.delete(name) + save_yaml(thor_yaml) + + puts "Done." + end + + desc "update NAME", "Update a Thor file from its original location" + def update(name) + raise Error, "Can't find module '#{name}'" if !thor_yaml[name] || !thor_yaml[name][:location] + + say "Updating '#{name}' from #{thor_yaml[name][:location]}" + + old_filename = thor_yaml[name][:filename] + self.options = self.options.merge("as" => name) + filename = install(thor_yaml[name][:location]) + + unless filename == old_filename + File.delete(File.join(thor_root, old_filename)) + end + end + + desc "installed", "List the installed Thor modules and tasks" + method_options :internal => :boolean + def installed + initialize_thorfiles(nil, true) + display_klasses(true, options["internal"]) + end + + desc "list [SEARCH]", "List the available thor tasks (--substring means .*SEARCH)" + method_options :substring => :boolean, :group => :string, :all => :boolean + def list(search="") + initialize_thorfiles + + search = ".*#{search}" if options["substring"] + search = /^#{search}.*/i + group = options[:group] || "standard" + + klasses = Thor::Base.subclasses.select do |k| + (options[:all] || k.group == group) && k.namespace =~ search + end + + display_klasses(false, false, klasses) + end + + private + + def self.banner(task) + "thor " + task.formatted_usage(self, false) + end + + def thor_root + Thor::Util.thor_root + end + + def thor_yaml + @thor_yaml ||= begin + yaml_file = File.join(thor_root, "thor.yml") + yaml = YAML.load_file(yaml_file) if File.exists?(yaml_file) + yaml || {} + end + end + + # Save the yaml file. If none exists in thor root, creates one. + # + def save_yaml(yaml) + yaml_file = File.join(thor_root, "thor.yml") + + unless File.exists?(yaml_file) + FileUtils.mkdir_p(thor_root) + yaml_file = File.join(thor_root, "thor.yml") + FileUtils.touch(yaml_file) + end + + File.open(yaml_file, "w") { |f| f.puts yaml.to_yaml } + end + + def self.exit_on_failure? + true + end + + # Load the thorfiles. If relevant_to is supplied, looks for specific files + # in the thor_root instead of loading them all. + # + # By default, it also traverses the current path until find Thor files, as + # described in thorfiles. This look up can be skipped by suppliying + # skip_lookup true. + # + def initialize_thorfiles(relevant_to=nil, skip_lookup=false) + thorfiles(relevant_to, skip_lookup).each do |f| + Thor::Util.load_thorfile(f) unless Thor::Base.subclass_files.keys.include?(File.expand_path(f)) + end + end + + # Finds Thorfiles by traversing from your current directory down to the root + # directory of your system. If at any time we find a Thor file, we stop. + # + # We also ensure that system-wide Thorfiles are loaded first, so local + # Thorfiles can override them. + # + # ==== Example + # + # If we start at /Users/wycats/dev/thor ... + # + # 1. /Users/wycats/dev/thor + # 2. /Users/wycats/dev + # 3. /Users/wycats <-- we find a Thorfile here, so we stop + # + # Suppose we start at c:\Documents and Settings\james\dev\thor ... + # + # 1. c:\Documents and Settings\james\dev\thor + # 2. c:\Documents and Settings\james\dev + # 3. c:\Documents and Settings\james + # 4. c:\Documents and Settings + # 5. c:\ <-- no Thorfiles found! + # + def thorfiles(relevant_to=nil, skip_lookup=false) + thorfiles = [] + + unless skip_lookup + Pathname.pwd.ascend do |path| + thorfiles = Thor::Util.globs_for(path).map { |g| Dir[g] }.flatten + break unless thorfiles.empty? + end + end + + files = (relevant_to ? thorfiles_relevant_to(relevant_to) : Thor::Util.thor_root_glob) + files += thorfiles + files -= ["#{thor_root}/thor.yml"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Load thorfiles relevant to the given method. If you provide "foo:bar" it + # will load all thor files in the thor.yaml that has "foo" e "foo:bar" + # namespaces registered. + # + def thorfiles_relevant_to(meth) + lookup = [ meth, meth.split(":")[0...-1].join(":") ] + + files = thor_yaml.select do |k, v| + v[:namespaces] && !(v[:namespaces] & lookup).empty? + end + + files.map { |k, v| File.join(thor_root, "#{v[:filename]}") } + end + + # Display information about the given klasses. If with_module is given, + # it shows a table with information extracted from the yaml file. + # + def display_klasses(with_modules=false, show_internal=false, klasses=Thor::Base.subclasses) + klasses -= [Thor, Thor::Runner, Thor::Group] unless show_internal + + raise Error, "No Thor tasks available" if klasses.empty? + show_modules if with_modules && !thor_yaml.empty? + + # Remove subclasses + klasses.dup.each do |klass| + klasses -= Thor::Util.thor_classes_in(klass) + end + + list = Hash.new { |h,k| h[k] = [] } + groups = klasses.select { |k| k.ancestors.include?(Thor::Group) } + + # Get classes which inherit from Thor + (klasses - groups).each { |k| list[k.namespace] += k.printable_tasks(false) } + + # Get classes which inherit from Thor::Base + groups.map! { |k| k.printable_tasks(false).first } + list["root"] = groups + + # Order namespaces with default coming first + list = list.sort{ |a,b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') } + list.each { |n, tasks| display_tasks(n, tasks) unless tasks.empty? } + end + + def display_tasks(namespace, list) #:nodoc: + list.sort!{ |a,b| a[0] <=> b[0] } + + say shell.set_color(namespace, :blue, true) + say "-" * namespace.size + + print_table(list, :truncate => true) + say + end + + def show_modules #:nodoc: + info = [] + labels = ["Modules", "Namespaces"] + + info << labels + info << [ "-" * labels[0].size, "-" * labels[1].size ] + + thor_yaml.each do |name, hash| + info << [ name, hash[:namespaces].join(", ") ] + end + + print_table info + say "" + end +end diff --git a/lib/bubble/vendor/thor/shell.rb b/lib/bubble/vendor/thor/shell.rb new file mode 100644 index 0000000000..64a173de83 --- /dev/null +++ b/lib/bubble/vendor/thor/shell.rb @@ -0,0 +1,78 @@ +require 'rbconfig' +require 'thor/shell/color' + +class Thor + module Base + # Returns the shell used in all Thor classes. If you are in a Unix platform + # it will use a colored log, otherwise it will use a basic one without color. + # + def self.shell + @shell ||= if Config::CONFIG['host_os'] =~ /mswin|mingw/ + Thor::Shell::Basic + else + Thor::Shell::Color + end + end + + # Sets the shell used in all Thor classes. + # + def self.shell=(klass) + @shell = klass + end + end + + module Shell + SHELL_DELEGATED_METHODS = [:ask, :yes?, :no?, :say, :say_status, :print_table] + + # Add shell to initialize config values. + # + # ==== Configuration + # shell<Object>:: An instance of the shell to be used. + # + # ==== Examples + # + # class MyScript < Thor + # argument :first, :type => :numeric + # end + # + # MyScript.new [1.0], { :foo => :bar }, :shell => Thor::Shell::Basic.new + # + def initialize(args=[], options={}, config={}) + super + self.shell = config[:shell] + self.shell.base ||= self if self.shell.respond_to?(:base) + end + + # Holds the shell for the given Thor instance. If no shell is given, + # it gets a default shell from Thor::Base.shell. + # + def shell + @shell ||= Thor::Base.shell.new + end + + # Sets the shell for this thor class. + # + def shell=(shell) + @shell = shell + end + + # Common methods that are delegated to the shell. + # + SHELL_DELEGATED_METHODS.each do |method| + module_eval <<-METHOD, __FILE__, __LINE__ + def #{method}(*args) + shell.#{method}(*args) + end + METHOD + end + + protected + + # Allow shell to be shared between invocations. + # + def _shared_configuration #:nodoc: + super.merge!(:shell => self.shell) + end + + end +end diff --git a/lib/bubble/vendor/thor/shell/basic.rb b/lib/bubble/vendor/thor/shell/basic.rb new file mode 100644 index 0000000000..a11f45b4e9 --- /dev/null +++ b/lib/bubble/vendor/thor/shell/basic.rb @@ -0,0 +1,239 @@ +require 'tempfile' + +class Thor + module Shell + class Basic + attr_accessor :base, :padding + + # Initialize base and padding to nil. + # + def initialize #:nodoc: + @base, @padding = nil, 0 + end + + # Sets the output padding, not allowing less than zero values. + # + def padding=(value) + @padding = [0, value].max + end + + # Ask something to the user and receives a response. + # + # ==== Example + # ask("What is your name?") + # + def ask(statement, color=nil) + say("#{statement} ", color) + $stdin.gets.strip + end + + # Say (print) something to the user. If the sentence ends with a whitespace + # or tab character, a new line is not appended (print + flush). Otherwise + # are passed straight to puts (behavior got from Highline). + # + # ==== Example + # say("I know you knew that.") + # + def say(message="", color=nil, force_new_line=(message.to_s !~ /( |\t)$/)) + message = message.to_s + message = set_color(message, color) if color + + if force_new_line + $stdout.puts(message) + else + $stdout.print(message) + $stdout.flush + end + end + + # Say a status with the given color and appends the message. Since this + # method is used frequently by actions, it allows nil or false to be given + # in log_status, avoiding the message from being shown. If a Symbol is + # given in log_status, it's used as the color. + # + def say_status(status, message, log_status=true) + return if quiet? || log_status == false + spaces = " " * (padding + 1) + color = log_status.is_a?(Symbol) ? log_status : :green + + status = status.to_s.rjust(12) + status = set_color status, color, true if color + say "#{status}#{spaces}#{message}", nil, true + end + + # Make a question the to user and returns true if the user replies "y" or + # "yes". + # + def yes?(statement, color=nil) + ask(statement, color) =~ is?(:yes) + end + + # Make a question the to user and returns true if the user replies "n" or + # "no". + # + def no?(statement, color=nil) + !yes?(statement, color) + end + + # Prints a table. + # + # ==== Parameters + # Array[Array[String, String, ...]] + # + # ==== Options + # ident<Integer>:: Ident the first column by ident value. + # + def print_table(table, options={}) + return if table.empty? + + formats, ident = [], options[:ident].to_i + options[:truncate] = terminal_width if options[:truncate] == true + + 0.upto(table.first.length - 2) do |i| + maxima = table.max{ |a,b| a[i].size <=> b[i].size }[i].size + formats << "%-#{maxima + 2}s" + end + + formats[0] = formats[0].insert(0, " " * ident) + formats << "%s" + + table.each do |row| + sentence = "" + + row.each_with_index do |column, i| + sentence << formats[i] % column.to_s + end + + sentence = truncate(sentence, options[:truncate]) if options[:truncate] + $stdout.puts sentence + end + end + + # Deals with file collision and returns true if the file should be + # overwriten and false otherwise. If a block is given, it uses the block + # response as the content for the diff. + # + # ==== Parameters + # destination<String>:: the destination file to solve conflicts + # block<Proc>:: an optional block that returns the value to be used in diff + # + def file_collision(destination) + return true if @always_force + options = block_given? ? "[Ynaqdh]" : "[Ynaqh]" + + while true + answer = ask %[Overwrite #{destination}? (enter "h" for help) #{options}] + + case answer + when is?(:yes), is?(:force), "" + return true + when is?(:no), is?(:skip) + return false + when is?(:always) + return @always_force = true + when is?(:quit) + say 'Aborting...' + raise SystemExit + when is?(:diff) + show_diff(destination, yield) if block_given? + say 'Retrying...' + else + say file_collision_help + end + end + end + + # Called if something goes wrong during the execution. This is used by Thor + # internally and should not be used inside your scripts. If someone went + # wrong, you can always raise an exception. If you raise a Thor::Error, it + # will be rescued and wrapped in the method below. + # + def error(statement) + $stderr.puts statement + end + + # Apply color to the given string with optional bold. Disabled in the + # Thor::Shell::Basic class. + # + def set_color(string, color, bold=false) #:nodoc: + string + end + + protected + + def is?(value) #:nodoc: + value = value.to_s + + if value.size == 1 + /\A#{value}\z/i + else + /\A(#{value}|#{value[0,1]})\z/i + end + end + + def file_collision_help #:nodoc: +<<HELP +Y - yes, overwrite +n - no, do not overwrite +a - all, overwrite this and all others +q - quit, abort +d - diff, show the differences between the old and the new +h - help, show this help +HELP + end + + def show_diff(destination, content) #:nodoc: + diff_cmd = ENV['THOR_DIFF'] || ENV['RAILS_DIFF'] || 'diff -u' + + Tempfile.open(File.basename(destination), File.dirname(destination)) do |temp| + temp.write content + temp.rewind + system %(#{diff_cmd} "#{destination}" "#{temp.path}") + end + end + + def quiet? #:nodoc: + base && base.options[:quiet] + end + + # This code was copied from Rake, available under MIT-LICENSE + # Copyright (c) 2003, 2004 Jim Weirich + def terminal_width + if ENV['THOR_COLUMNS'] + result = ENV['THOR_COLUMNS'].to_i + else + result = unix? ? dynamic_width : 80 + end + (result < 10) ? 80 : result + rescue + 80 + end + + # Calculate the dynamic width of the terminal + def dynamic_width + @dynamic_width ||= (dynamic_width_stty.nonzero? || dynamic_width_tput) + end + + def dynamic_width_stty + %x{stty size 2>/dev/null}.split[1].to_i + end + + def dynamic_width_tput + %x{tput cols 2>/dev/null}.to_i + end + + def unix? + RUBY_PLATFORM =~ /(aix|darwin|linux|(net|free|open)bsd|cygwin|solaris|irix|hpux)/i + end + + def truncate(string, width) + if string.length <= width + string + else + ( string[0, width-3] || "" ) + "..." + end + end + + end + end +end diff --git a/lib/bubble/vendor/thor/shell/color.rb b/lib/bubble/vendor/thor/shell/color.rb new file mode 100644 index 0000000000..b2bc66dfba --- /dev/null +++ b/lib/bubble/vendor/thor/shell/color.rb @@ -0,0 +1,108 @@ +require 'thor/shell/basic' + +class Thor + module Shell + # Inherit from Thor::Shell::Basic and add set_color behavior. Check + # Thor::Shell::Basic to see all available methods. + # + class Color < Basic + # Embed in a String to clear all previous ANSI sequences. + CLEAR = "\e[0m" + # The start of an ANSI bold sequence. + BOLD = "\e[1m" + + # Set the terminal's foreground ANSI color to black. + BLACK = "\e[30m" + # Set the terminal's foreground ANSI color to red. + RED = "\e[31m" + # Set the terminal's foreground ANSI color to green. + GREEN = "\e[32m" + # Set the terminal's foreground ANSI color to yellow. + YELLOW = "\e[33m" + # Set the terminal's foreground ANSI color to blue. + BLUE = "\e[34m" + # Set the terminal's foreground ANSI color to magenta. + MAGENTA = "\e[35m" + # Set the terminal's foreground ANSI color to cyan. + CYAN = "\e[36m" + # Set the terminal's foreground ANSI color to white. + WHITE = "\e[37m" + + # Set the terminal's background ANSI color to black. + ON_BLACK = "\e[40m" + # Set the terminal's background ANSI color to red. + ON_RED = "\e[41m" + # Set the terminal's background ANSI color to green. + ON_GREEN = "\e[42m" + # Set the terminal's background ANSI color to yellow. + ON_YELLOW = "\e[43m" + # Set the terminal's background ANSI color to blue. + ON_BLUE = "\e[44m" + # Set the terminal's background ANSI color to magenta. + ON_MAGENTA = "\e[45m" + # Set the terminal's background ANSI color to cyan. + ON_CYAN = "\e[46m" + # Set the terminal's background ANSI color to white. + ON_WHITE = "\e[47m" + + # Set color by using a string or one of the defined constants. If a third + # option is set to true, it also adds bold to the string. This is based + # on Highline implementation and it automatically appends CLEAR to the end + # of the returned String. + # + def set_color(string, color, bold=false) + color = self.class.const_get(color.to_s.upcase) if color.is_a?(Symbol) + bold = bold ? BOLD : "" + "#{bold}#{color}#{string}#{CLEAR}" + end + + protected + + # Overwrite show_diff to show diff with colors if Diff::LCS is + # available. + # + def show_diff(destination, content) #:nodoc: + if diff_lcs_loaded? && ENV['THOR_DIFF'].nil? && ENV['RAILS_DIFF'].nil? + actual = File.binread(destination).to_s.split("\n") + content = content.to_s.split("\n") + + Diff::LCS.sdiff(actual, content).each do |diff| + output_diff_line(diff) + end + else + super + end + end + + def output_diff_line(diff) #:nodoc: + case diff.action + when '-' + say "- #{diff.old_element.chomp}", :red, true + when '+' + say "+ #{diff.new_element.chomp}", :green, true + when '!' + say "- #{diff.old_element.chomp}", :red, true + say "+ #{diff.new_element.chomp}", :green, true + else + say " #{diff.old_element.chomp}", nil, true + end + end + + # Check if Diff::LCS is loaded. If it is, use it to create pretty output + # for diff. + # + def diff_lcs_loaded? #:nodoc: + return true if defined?(Diff::LCS) + return @diff_lcs_loaded unless @diff_lcs_loaded.nil? + + @diff_lcs_loaded = begin + require 'diff/lcs' + true + rescue LoadError + false + end + end + + end + end +end diff --git a/lib/bubble/vendor/thor/task.rb b/lib/bubble/vendor/thor/task.rb new file mode 100644 index 0000000000..5c8877591b --- /dev/null +++ b/lib/bubble/vendor/thor/task.rb @@ -0,0 +1,111 @@ +class Thor + class Task < Struct.new(:name, :description, :usage, :options) + FILE_REGEXP = /^#{Regexp.escape(File.expand_path(__FILE__))}:[\w:]+ `run'$/ + + # A dynamic task that handles method missing scenarios. + class Dynamic < Task + def initialize(name, options=nil) + super(name.to_s, "A dynamically-generated task", name.to_s, options) + end + + def run(instance, args=[]) + unless (instance.methods & [name.to_s, name.to_sym]).empty? + raise Error, "could not find Thor class or task '#{name}'" + end + super + end + end + + def initialize(name, description, usage, options=nil) + super(name.to_s, description, usage, options || {}) + end + + def initialize_copy(other) #:nodoc: + super(other) + self.options = other.options.dup if other.options + end + + # By default, a task invokes a method in the thor class. You can change this + # implementation to create custom tasks. + def run(instance, args=[]) + raise UndefinedTaskError, "the '#{name}' task of #{instance.class} is private" unless public_method?(instance) + instance.send(name, *args) + rescue ArgumentError => e + raise e if instance.class.respond_to?(:debugging) && instance.class.debugging + parse_argument_error(instance, e, caller) + rescue NoMethodError => e + raise e if instance.class.respond_to?(:debugging) && instance.class.debugging + parse_no_method_error(instance, e) + end + + # Returns the formatted usage by injecting given required arguments + # and required options into the given usage. + def formatted_usage(klass, namespace=nil) + namespace = klass.namespace if namespace.nil? + + # Add namespace + formatted = if namespace + "#{namespace.gsub(/^(default|thor:runner:)/,'')}:" + else + "" + end + + # Add usage with required arguments + formatted << if klass && !klass.arguments.empty? + usage.to_s.gsub(/^#{name}/) do |match| + match << " " << klass.arguments.map{ |a| a.usage }.compact.join(' ') + end + else + usage.to_s + end + + # Add required options + formatted << " #{required_options}" + + # Strip and go! + formatted.strip + end + + protected + + def required_options + @required_options ||= options.map{ |_, o| o.usage if o.required? }.compact.sort.join(" ") + end + + # Given a target, checks if this class name is not a private/protected method. + def public_method?(instance) #:nodoc: + collection = instance.private_methods + instance.protected_methods + (collection & [name.to_s, name.to_sym]).empty? + end + + # For Ruby <= 1.8.7, we have to match the method name that we are trying to call. + # In Ruby >= 1.9.1, we have to match the method run in this file. + def backtrace_match?(backtrace) #:nodoc: + method_name = /`#{Regexp.escape(name.split(':').last)}'/ + backtrace =~ method_name || backtrace =~ FILE_REGEXP + end + + def parse_argument_error(instance, e, caller) #:nodoc: + if e.message =~ /wrong number of arguments/ && backtrace_match?(e.backtrace.first.to_s) + if instance.is_a?(Thor::Group) + raise e, "'#{name}' was called incorrectly. Are you sure it has arity equals to 0?" + else + raise InvocationError, "'#{name}' was called incorrectly. Call as " << + "'#{formatted_usage(instance.class)}'" + end + else + raise e + end + end + + def parse_no_method_error(instance, e) #:nodoc: + if e.message =~ /^undefined method `#{name}' for #{Regexp.escape(instance.to_s)}$/ + raise UndefinedTaskError, "The #{instance.class.namespace} namespace " << + "doesn't have a '#{name}' task" + else + raise e + end + end + + end +end diff --git a/lib/bubble/vendor/thor/util.rb b/lib/bubble/vendor/thor/util.rb new file mode 100644 index 0000000000..c2aed89ccf --- /dev/null +++ b/lib/bubble/vendor/thor/util.rb @@ -0,0 +1,233 @@ +require 'rbconfig' + +class Thor + module Sandbox #:nodoc: + end + + # This module holds several utilities: + # + # 1) Methods to convert thor namespaces to constants and vice-versa. + # + # Thor::Utils.namespace_from_thor_class(Foo::Bar::Baz) #=> "foo:bar:baz" + # + # 2) Loading thor files and sandboxing: + # + # Thor::Utils.load_thorfile("~/.thor/foo") + # + module Util + + # Receives a namespace and search for it in the Thor::Base subclasses. + # + # ==== Parameters + # namespace<String>:: The namespace to search for. + # + def self.find_by_namespace(namespace) + namespace = "default#{namespace}" if namespace.empty? || namespace =~ /^:/ + + Thor::Base.subclasses.find do |klass| + klass.namespace == namespace + end + end + + # Receives a constant and converts it to a Thor namespace. Since Thor tasks + # can be added to a sandbox, this method is also responsable for removing + # the sandbox namespace. + # + # This method should not be used in general because it's used to deal with + # older versions of Thor. On current versions, if you need to get the + # namespace from a class, just call namespace on it. + # + # ==== Parameters + # constant<Object>:: The constant to be converted to the thor path. + # + # ==== Returns + # String:: If we receive Foo::Bar::Baz it returns "foo:bar:baz" + # + def self.namespace_from_thor_class(constant, remove_default=true) + constant = constant.to_s.gsub(/^Thor::Sandbox::/, "") + constant = snake_case(constant).squeeze(":") + constant.gsub!(/^default/, '') if remove_default + constant + end + + # Given the contents, evaluate it inside the sandbox and returns the + # namespaces defined in the sandbox. + # + # ==== Parameters + # contents<String> + # + # ==== Returns + # Array[Object] + # + def self.namespaces_in_content(contents, file=__FILE__) + old_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.clear + + load_thorfile(file, contents) + + new_constants = Thor::Base.subclasses.dup + Thor::Base.subclasses.replace(old_constants) + + new_constants.map!{ |c| c.namespace } + new_constants.compact! + new_constants + end + + # Returns the thor classes declared inside the given class. + # + def self.thor_classes_in(klass) + stringfied_constants = klass.constants.map { |c| c.to_s } + Thor::Base.subclasses.select do |subclass| + next unless subclass.name + stringfied_constants.include?(subclass.name.gsub("#{klass.name}::", '')) + end + end + + # Receives a string and convert it to snake case. SnakeCase returns snake_case. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def self.snake_case(str) + return str.downcase if str =~ /^[A-Z_]+$/ + str.gsub(/\B[A-Z]/, '_\&').squeeze('_') =~ /_*(.*)/ + return $+.downcase + end + + # Receives a string and convert it to camel case. camel_case returns CamelCase. + # + # ==== Parameters + # String + # + # ==== Returns + # String + # + def self.camel_case(str) + return str if str !~ /_/ && str =~ /[A-Z]+.*/ + str.split('_').map { |i| i.capitalize }.join + end + + # Receives a namespace and tries to retrieve a Thor or Thor::Group class + # from it. It first searches for a class using the all the given namespace, + # if it's not found, removes the highest entry and searches for the class + # again. If found, returns the highest entry as the class name. + # + # ==== Examples + # + # class Foo::Bar < Thor + # def baz + # end + # end + # + # class Baz::Foo < Thor::Group + # end + # + # Thor::Util.namespace_to_thor_class("foo:bar") #=> Foo::Bar, nil # will invoke default task + # Thor::Util.namespace_to_thor_class("baz:foo") #=> Baz::Foo, nil + # Thor::Util.namespace_to_thor_class("foo:bar:baz") #=> Foo::Bar, "baz" + # + # ==== Parameters + # namespace<String> + # + # ==== Errors + # Thor::Error:: raised if the namespace cannot be found. + # + # Thor::Error:: raised if the namespace evals to a class which does not + # inherit from Thor or Thor::Group. + # + def self.namespace_to_thor_class_and_task(namespace, raise_if_nil=true) + if namespace.include?(?:) + pieces = namespace.split(":") + task = pieces.pop + klass = Thor::Util.find_by_namespace(pieces.join(":")) + end + + unless klass + klass, task = Thor::Util.find_by_namespace(namespace), nil + end + + raise Error, "could not find Thor class or task '#{namespace}'" if raise_if_nil && klass.nil? + return klass, task + end + + # Receives a path and load the thor file in the path. The file is evaluated + # inside the sandbox to avoid namespacing conflicts. + # + def self.load_thorfile(path, content=nil) + content ||= File.binread(path) + + begin + Thor::Sandbox.class_eval(content, path) + rescue Exception => e + $stderr.puts "WARNING: unable to load thorfile #{path.inspect}: #{e.message}" + end + end + + def self.user_home + @@user_home ||= if ENV["HOME"] + ENV["HOME"] + elsif ENV["USERPROFILE"] + ENV["USERPROFILE"] + elsif ENV["HOMEDRIVE"] && ENV["HOMEPATH"] + File.join(ENV["HOMEDRIVE"], ENV["HOMEPATH"]) + elsif ENV["APPDATA"] + ENV["APPDATA"] + else + begin + File.expand_path("~") + rescue + if File::ALT_SEPARATOR + "C:/" + else + "/" + end + end + end + end + + # Returns the root where thor files are located, dependending on the OS. + # + def self.thor_root + File.join(user_home, ".thor").gsub(/\\/, '/') + end + + # Returns the files in the thor root. On Windows thor_root will be something + # like this: + # + # C:\Documents and Settings\james\.thor + # + # If we don't #gsub the \ character, Dir.glob will fail. + # + def self.thor_root_glob + files = Dir["#{thor_root}/*"] + + files.map! do |file| + File.directory?(file) ? File.join(file, "main.thor") : file + end + end + + # Where to look for Thor files. + # + def self.globs_for(path) + ["#{path}/Thorfile", "#{path}/*.thor", "#{path}/tasks/*.thor", "#{path}/lib/tasks/*.thor"] + end + + # Return the path to the ruby interpreter taking into account multiple + # installations and windows extensions. + # + def self.ruby_command + @ruby_command ||= begin + ruby = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) + ruby << Config::CONFIG['EXEEXT'] + + # escape string in case path to ruby executable contain spaces. + ruby.sub!(/.*\s.*/m, '"\&"') + ruby + end + end + + end +end diff --git a/lib/bubble/vendor/thor/version.rb b/lib/bubble/vendor/thor/version.rb new file mode 100644 index 0000000000..3622a82898 --- /dev/null +++ b/lib/bubble/vendor/thor/version.rb @@ -0,0 +1,3 @@ +class Thor + VERSION = "0.12.2".freeze +end diff --git a/spec/install/gems_spec.rb b/spec/install/gems_spec.rb new file mode 100644 index 0000000000..48fe5993f1 --- /dev/null +++ b/spec/install/gems_spec.rb @@ -0,0 +1,13 @@ +require File.expand_path('../../spec_helper', __FILE__) + +describe "bbl install" do + it "works" do + gemfile <<-G + gem "rack" + G + + bbl :install + run "require 'rack'; puts RACK" + out.should == "1.0.0" + end +end
\ No newline at end of file diff --git a/spec/runtime/load_spec.rb b/spec/runtime/load_spec.rb new file mode 100644 index 0000000000..9b60673b7c --- /dev/null +++ b/spec/runtime/load_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path('../../spec_helper', __FILE__) + +describe "Bubble.load" do + + before :each do + in_app_root + system_gems "rack-1.0.0" + end + + it "provides a list of the env dependencies" do + gemfile <<-G + gem "rack" + G + + env = Bubble.load + env.dependencies.should have_dep("rack", ">= 0") + end + + it "provides a list of the resolved gems" do + pending + gemfile <<-G + gem "rack" + G + + env = Bubble.load + env.gems.should have_gem("rack-1.0.0") + end + + it "raises an exception if the default gemfile is not found" do + lambda { + Bubble.load + }.should raise_error(Bubble::GemfileNotFound, /default/) + end + + it "raises an exception if a specified gemfile is not found" do + lambda { + Bubble.load("omg.rb") + }.should raise_error(Bubble::GemfileNotFound, /omg\.rb/) + end +end
\ No newline at end of file diff --git a/spec/runtime/setup_spec.rb b/spec/runtime/setup_spec.rb new file mode 100644 index 0000000000..792729df57 --- /dev/null +++ b/spec/runtime/setup_spec.rb @@ -0,0 +1,12 @@ +require File.expand_path('../../spec_helper', __FILE__) + +describe "Bubble.setup" do + + before :each do + in_app_root + end + + it "works" do + pending + end +end
\ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000000..319862303c --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,36 @@ +$:.unshift File.expand_path('..', __FILE__) +$:.unshift File.expand_path('../../lib', __FILE__) + +require 'rubygems' +require 'bubble' + +Dir["#{File.expand_path('../support', __FILE__)}/*.rb"].each do |file| + require file +end + +Spec::Rubygems.setup +FileUtils.rm_rf(Spec::Path.gem_repo1) + +Spec::Runner.configure do |config| + config.include Spec::Builders + config.include Spec::Helpers + config.include Spec::Matchers + config.include Spec::Path + config.include Spec::Rubygems + + original_wd = Dir.pwd + original_gem_home = ENV['GEM_HOME'] + + config.before :all do + build_repo1 + end + + config.before :each do + reset! + end + + config.after :each do + Dir.chdir(original_wd) + ENV['GEM_HOME'] = ENV['GEM_PATH'] = original_gem_home + end +end
\ No newline at end of file diff --git a/spec/support/builders.rb b/spec/support/builders.rb new file mode 100644 index 0000000000..030db494fc --- /dev/null +++ b/spec/support/builders.rb @@ -0,0 +1,257 @@ +module Spec + module Builders + + def build_repo1 + build_repo gem_repo1 do + build_gem "rake", "0.8.7" do |s| + s.executables = "rake" + end + build_gem "rack", %w(0.9.1 1.0.0) do |s| + s.executables = "rackup" + end + build_gem "rails", "2.3.2" do |s| + s.executables = "rails" + s.add_dependency "rake" + s.add_dependency "actionpack", "2.3.2" + s.add_dependency "activerecord", "2.3.2" + s.add_dependency "actionmailer", "2.3.2" + s.add_dependency "activeresource", "2.3.2" + end + build_gem "actionpack", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activerecord", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "actionmailer", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activeresource", "2.3.2" do |s| + s.add_dependency "activesupport", "2.3.2" + end + build_gem "activesupport", "2.3.2" + + build_gem "missing_dep" do |s| + s.add_dependency "not_here" + end + + build_gem "rspec", "1.2.7", :no_default => true do |s| + s.write "lib/spec.rb", "SPEC = '1.2.7'" + end + + build_gem "rack-test", :no_default => true do |s| + s.write "lib/rack/test.rb", "RACK_TEST = '1.0'" + end + + build_gem "platform_specific" do |s| + s.platform = "java" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 JAVA'" + end + + build_gem "platform_specific" do |s| + s.platform = "ruby" + s.write "lib/platform_specific.rb", "PLATFORM_SPECIFIC = '1.0.0 RUBY'" + end + + build_gem "only_java" do |s| + s.platform = "java" + end + + build_gem "very-simple" + + build_gem "very-simple-prerelease", "1.0.pre" + + build_gem "very_simple_binary" do |s| + s.require_paths << 'ext' + s.extensions << "ext/extconf.rb" + s.write "ext/extconf.rb", <<-RUBY + require "mkmf" + + exit 1 unless with_config("simple") + + extension_name = "very_simple_binary_c" + dir_config extension_name + create_makefile extension_name + RUBY + s.write "ext/very_simple_binary.c", <<-C + #include "ruby.h" + + void Init_very_simple_binary_c() { + rb_define_module("VerySimpleBinaryInC"); + } + C + end + end + end + + def build_repo2 + FileUtils.rm_rf gem_repo2 + FileUtils.cp_r gem_repo1, gem_repo2 + end + + def update_repo2 + update_repo gem_repo2 do + build_gem "rack", "1.2" do |s| + s.executables = "rackup" + end + end + end + + def build_repo(path, &blk) + return if File.directory?(path) + update_repo(path, &blk) + end + + def update_repo(path) + @_build_path = "#{path}/gems" + yield + @_build_path = nil + Dir.chdir(path) { gem_command :generate_index } + end + + def build_index(&block) + index = Gem::SourceIndex.new + IndexBuilder.run(index, &block) if block_given? + index + end + + def build_spec(name, version, platform = nil, &block) + spec = Gem::Specification.new + spec.instance_variable_set(:@name, name) + spec.instance_variable_set(:@version, Gem::Version.new(version)) + spec.platform = Gem::Platform.new(platform) if platform + DepBuilder.run(spec, &block) if block_given? + spec + end + + def build_dep(name, requirements = Gem::Requirement.default, type = :runtime) + Bundler::Dependency.new(name, :version => requirements) + end + + def build_lib(name, *args, &blk) + build_with(LibBuilder, name, args, &blk) + end + + def build_gem(name, *args, &blk) + build_with(GemBuilder, name, args, &blk) + end + + private + + def build_with(builder, name, args, &blk) + options = args.last.is_a?(Hash) ? args.pop : {} + versions = args.last || "1.0" + + options[:path] ||= @_build_path + + Array(versions).each do |version| + spec = builder.new(self, name, version) + yield spec if block_given? + spec._build(options) + end + end + + class IndexBuilder + include Builders + + def self.run(index, &block) + new(index).run(&block) + end + + def initialize(index) + @index = index + end + + def run(&block) + instance_eval(&block) + end + + def add_spec(*args, &block) + @index.add_spec(build_spec(*args, &block)) + end + end + + class DepBuilder + def self.run(spec, &block) + new(spec).run(&block) + end + + def initialize(spec) + @spec = spec + end + + def run(&block) + instance_eval(&block) + end + + def runtime(name, requirements) + @spec.add_runtime_dependency(name, requirements) + end + end + + class LibBuilder + def initialize(context, name, version) + @context = context + @name = name + @spec = Gem::Specification.new do |s| + s.name = name + s.version = version + s.summary = "This is just a fake gem for testing" + end + @files = {} + @default_files = { "lib/#{name}.rb" => "#{name.gsub('-', '').upcase} = '#{version}'" } + end + + def method_missing(*args, &blk) + @spec.send(*args, &blk) + end + + def write(file, source) + @files[file] = source + end + + def executables=(val) + Array(val).each do |file| + write "bin/#{file}", "require '#{@name}' ; puts #{@name.upcase}" + end + @spec.executables = Array(val) + end + + def _build(options) + path = options[:path] || _default_path + @files["#{name}.gemspec"] = @spec.to_ruby if options[:gemspec] + unless options[:no_default] + @files = @default_files.merge(@files) + end + @files.each do |file, source| + file = Pathname.new(path).join(file) + FileUtils.mkdir_p(file.dirname) + File.open(file, 'w') { |f| f.puts source } + end + @spec.files = @files.keys + path + end + + def _default_path + @context.tmp('libs', @spec.full_name) + end + end + + class GemBuilder < LibBuilder + + def _build(opts) + lib_path = super(:path => @context.tmp(".tmp/#{@spec.full_name}"), :no_default => opts[:no_default]) + Dir.chdir(lib_path) do + destination = opts[:path] || _default_path + FileUtils.mkdir_p(destination) + Gem::Builder.new(@spec).build + FileUtils.mv("#{@spec.full_name}.gem", opts[:path] || _default_path) + end + end + + def _default_path + @context.gem_repo1('gems') + end + end + end +end
\ No newline at end of file diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb new file mode 100644 index 0000000000..0945b0171a --- /dev/null +++ b/spec/support/helpers.rb @@ -0,0 +1,83 @@ +module Spec + module Helpers + def reset! + Dir["#{tmp}/{gems/*,*}"].each do |dir| + next if %(base remote1 gems).include?(File.basename(dir)) + FileUtils.rm_rf(dir) + end + FileUtils.mkdir_p(tmp) + end + + attr_reader :out + + def in_app_root(&blk) + FileUtils.mkdir_p(bundled_app) + Dir.chdir(bundled_app, &blk) + end + + def run_in_context(cmd) + env = bundled_path.join('environment.rb') + raise "Missing environment.rb" unless env.file? + @out = ruby "-r #{env}", cmd + end + + def run(cmd) + setup = "require 'rubygems' ; require 'bubble' ; Bubble.setup\n" + @out = ruby(setup + cmd) + end + + def ruby(opts, ruby = nil) + ruby, opts = opts, nil unless ruby + ruby.gsub!(/(?=")/, "\\") + ruby.gsub!('$', '\\$') + lib = File.join(File.dirname(__FILE__), '..', '..', 'lib') + %x{#{Gem.ruby} -I#{lib} #{opts} -e "#{ruby}"}.strip + end + + def gemfile(*args) + path = bundled_app("Gemfile") + path = args.shift if Pathname === args.first + str = args.shift || "" + FileUtils.mkdir_p(path.dirname.to_s) + File.open(path.to_s, 'w') do |f| + f.puts str + end + end + + def bubble(*args) + path = bundled_app("Gemfile") + path = args.shift if Pathname === args.first + str = args.shift || "" + FileUtils.mkdir_p(path.dirname) + Dir.chdir(path.dirname) do + gemfile(path, str) + Bubble.load(path) + end + end + + def install_gems(*gems) + Dir["#{gem_repo1}/**/*.gem"].each do |path| + if gems.include?(File.basename(path, ".gem")) + gem_command :install, "--no-rdoc --no-ri --ignore-dependencies #{path}" + end + end + end + + alias install_gem install_gems + + def system_gems(*gems) + FileUtils.mkdir_p(system_gem_path) + + Gem.clear_paths + + gem_home, gem_path = ENV['GEM_HOME'], ENV['GEM_PATH'] + ENV['GEM_HOME'], ENV['GEM_PATH'] = system_gem_path.to_s, system_gem_path.to_s + + install_gems(*gems) + if block_given? + yield + ENV['GEM_HOME'], ENV['GEM_PATH'] = gem_home, gem_path + end + end + end +end
\ No newline at end of file diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb new file mode 100644 index 0000000000..bf95a3969f --- /dev/null +++ b/spec/support/matchers.rb @@ -0,0 +1,18 @@ +module Spec + module Matchers + def have_dep(*args) + simple_matcher "have dependency" do |given, matcher| + dep = Bubble::Dependency.new(*args) + + # given.length == args.length / 2 + given.length == 1 && given.all? { |d| d == dep } + end + end + + def have_gem(*args) + simple_matcher "have gem" do |given, matcher| + given.length == args.length && given.all? { |g| args.include?(g.full_name) } + end + end + end +end
\ No newline at end of file diff --git a/spec/support/path.rb b/spec/support/path.rb new file mode 100644 index 0000000000..1184873c30 --- /dev/null +++ b/spec/support/path.rb @@ -0,0 +1,33 @@ +module Spec + module Path + def root + @root ||= Pathname.new(File.expand_path("../../..", __FILE__)) + end + + def tmp(*path) + root.join("tmp", *path) + end + + def bundled_app(*path) + tmp.join("bundled_app", *path) + end + + def base_system_gems + tmp.join("gems/base") + end + + def gem_repo1 + tmp("gems/remote1") + end + + def gem_repo2 + tmp("gems/remote2") + end + + def system_gem_path + tmp("gems/system") + end + + extend self + end +end
\ No newline at end of file diff --git a/spec/support/rubygems.rb b/spec/support/rubygems.rb new file mode 100644 index 0000000000..efefd2c356 --- /dev/null +++ b/spec/support/rubygems.rb @@ -0,0 +1,25 @@ +module Spec + module Rubygems + def self.setup + Gem.clear_paths + + ENV['GEM_HOME'] = ENV['GEM_PATH'] = Path.base_system_gems + + unless File.exist?("#{Path.base_system_gems}") + FileUtils.mkdir_p(Path.base_system_gems) + `gem install builder --no-rdoc --no-ri` + end + + Gem::DefaultUserInteraction.ui = Gem::SilentUI.new + end + + def gem_command(command, args = "", options = {}) + if command == :exec && !options[:no_quote] + args = args.gsub(/(?=")/, "\\") + args = %["#{args}"] + end + lib = File.join(File.dirname(__FILE__), '..', '..', 'lib') + %x{#{Gem.ruby} -I#{lib} -rubygems -S gem --backtrace #{command} #{args}}.strip + end + end +end
\ No newline at end of file |