Upgraded Rails and RSpec
[monkeycharger.git] / vendor / rails / railties / lib / commands / plugin.rb
blobecef450ff4823b5477cf06d115ca945dc18b7816
1 # Rails Plugin Manager.
2
3 # Listing available plugins:
5 #   $ ./script/plugin list
6 #   continuous_builder            http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder
7 #   asset_timestamping            http://svn.aviditybytes.com/rails/plugins/asset_timestamping
8 #   enumerations_mixin            http://svn.protocool.com/rails/plugins/enumerations_mixin/trunk
9 #   calculations                  http://techno-weenie.net/svn/projects/calculations/
10 #   ...
12 # Installing plugins:
14 #   $ ./script/plugin install continuous_builder asset_timestamping
16 # Finding Repositories:
18 #   $ ./script/plugin discover
19
20 # Adding Repositories:
22 #   $ ./script/plugin source http://svn.protocool.com/rails/plugins/
24 # How it works:
25
26 #   * Maintains a list of subversion repositories that are assumed to have
27 #     a plugin directory structure. Manage them with the (source, unsource,
28 #     and sources commands)
29 #     
30 #   * The discover command scrapes the following page for things that
31 #     look like subversion repositories with plugins:
32 #     http://wiki.rubyonrails.org/rails/pages/Plugins
33
34 #   * Unless you specify that you want to use svn, script/plugin uses plain old
35 #     HTTP for downloads.  The following bullets are true if you specify
36 #     that you want to use svn.
38 #   * If `vendor/plugins` is under subversion control, the script will
39 #     modify the svn:externals property and perform an update. You can
40 #     use normal subversion commands to keep the plugins up to date.
41
42 #   * Or, if `vendor/plugins` is not under subversion control, the
43 #     plugin is pulled via `svn checkout` or `svn export` but looks
44 #     exactly the same.
45
46 # This is Free Software, copyright 2005 by Ryan Tomayko (rtomayko@gmail.com) 
47 # and is licensed MIT: (http://www.opensource.org/licenses/mit-license.php)
49 $verbose = false
52 require 'open-uri'
53 require 'fileutils'
54 require 'tempfile'
56 include FileUtils
58 class RailsEnvironment
59   attr_reader :root
61   def initialize(dir)
62     @root = dir
63   end
65   def self.find(dir=nil)
66     dir ||= pwd
67     while dir.length > 1
68       return new(dir) if File.exist?(File.join(dir, 'config', 'environment.rb'))
69       dir = File.dirname(dir)
70     end
71   end
72   
73   def self.default
74     @default ||= find
75   end
76   
77   def self.default=(rails_env)
78     @default = rails_env
79   end
80   
81   def install(name_uri_or_plugin)
82     if name_uri_or_plugin.is_a? String
83       if name_uri_or_plugin =~ /:\/\// 
84         plugin = Plugin.new(name_uri_or_plugin)
85       else
86         plugin = Plugins[name_uri_or_plugin]
87       end
88     else
89       plugin = name_uri_or_plugin
90     end
91     unless plugin.nil?
92       plugin.install
93     else
94       puts "Plugin not found: #{name_uri_or_plugin}"
95     end
96   end
98   def use_svn?
99     require 'active_support/core_ext/kernel'
100     silence_stderr {`svn --version` rescue nil}
101     !$?.nil? && $?.success?
102   end
104   def use_externals?
105     use_svn? && File.directory?("#{root}/vendor/plugins/.svn")
106   end
108   def use_checkout?
109     # this is a bit of a guess. we assume that if the rails environment
110     # is under subversion then they probably want the plugin checked out
111     # instead of exported. This can be overridden on the command line
112     File.directory?("#{root}/.svn")
113   end
115   def best_install_method
116     return :http unless use_svn?
117     case
118       when use_externals? then :externals
119       when use_checkout? then :checkout
120       else :export
121     end
122   end
124   def externals
125     return [] unless use_externals?
126     ext = `svn propget svn:externals "#{root}/vendor/plugins"`
127     ext.reject{ |line| line.strip == '' }.map do |line| 
128       line.strip.split(/\s+/, 2) 
129     end
130   end
132   def externals=(items)
133     unless items.is_a? String
134       items = items.map{|name,uri| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n")
135     end
136     Tempfile.open("svn-set-prop") do |file|
137       file.write(items)
138       file.flush
139       system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"")
140     end
141   end
142   
145 class Plugin
146   attr_reader :name, :uri
147   
148   def initialize(uri, name=nil)
149     @uri = uri
150     guess_name(uri)
151   end
152   
153   def self.find(name)
154     name =~ /\// ? new(name) : Repositories.instance.find_plugin(name)
155   end
156   
157   def to_s
158     "#{@name.ljust(30)}#{@uri}"
159   end
160   
161   def svn_url?
162     @uri =~ /svn(?:\+ssh)?:\/\/*/
163   end
164   
165   def installed?
166     File.directory?("#{rails_env.root}/vendor/plugins/#{name}") \
167       or rails_env.externals.detect{ |name, repo| self.uri == repo }
168   end
169   
170   def install(method=nil, options = {})
171     method ||= rails_env.best_install_method?
172     method   = :export if method == :http and svn_url?
174     uninstall if installed? and options[:force]
176     unless installed?
177       send("install_using_#{method}", options)
178       run_install_hook
179     else
180       puts "already installed: #{name} (#{uri}).  pass --force to reinstall"
181     end
182   end
184   def uninstall
185     path = "#{rails_env.root}/vendor/plugins/#{name}"
186     if File.directory?(path)
187       puts "Removing 'vendor/plugins/#{name}'" if $verbose
188       run_uninstall_hook
189       rm_r path
190     else
191       puts "Plugin doesn't exist: #{path}"
192     end
193     # clean up svn:externals
194     externals = rails_env.externals
195     externals.reject!{|n,u| name == n or name == u}
196     rails_env.externals = externals
197   end
199   def info
200     tmp = "#{rails_env.root}/_tmp_about.yml"
201     if svn_url?
202       cmd = "svn export #{@uri} \"#{rails_env.root}/#{tmp}\""
203       puts cmd if $verbose
204       system(cmd)
205     end
206     open(svn_url? ? tmp : File.join(@uri, 'about.yml')) do |stream|
207       stream.read
208     end rescue "No about.yml found in #{uri}"
209   ensure
210     FileUtils.rm_rf tmp if svn_url?
211   end
213   private 
215     def run_install_hook
216       install_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/install.rb"
217       load install_hook_file if File.exists? install_hook_file
218     end
220     def run_uninstall_hook
221       uninstall_hook_file = "#{rails_env.root}/vendor/plugins/#{name}/uninstall.rb"
222       load uninstall_hook_file if File.exists? uninstall_hook_file
223     end
225     def install_using_export(options = {})
226       svn_command :export, options
227     end
228     
229     def install_using_checkout(options = {})
230       svn_command :checkout, options
231     end
232     
233     def install_using_externals(options = {})
234       externals = rails_env.externals
235       externals.push([@name, uri])
236       rails_env.externals = externals
237       install_using_checkout(options)
238     end
240     def install_using_http(options = {})
241       root = rails_env.root
242       mkdir_p "#{root}/vendor/plugins/#{@name}"
243       Dir.chdir "#{root}/vendor/plugins/#{@name}" do
244         puts "fetching from '#{uri}'" if $verbose
245         fetcher = RecursiveHTTPFetcher.new(uri, -1)
246         fetcher.quiet = true if options[:quiet]
247         fetcher.fetch
248       end
249     end
251     def svn_command(cmd, options = {})
252       root = rails_env.root
253       mkdir_p "#{root}/vendor/plugins"
254       base_cmd = "svn #{cmd} #{uri} \"#{root}/vendor/plugins/#{name}\""
255       base_cmd += ' -q' if options[:quiet] and not $verbose
256       base_cmd += " -r #{options[:revision]}" if options[:revision]
257       puts base_cmd if $verbose
258       system(base_cmd)
259     end
261     def guess_name(url)
262       @name = File.basename(url)
263       if @name == 'trunk' || @name.empty?
264         @name = File.basename(File.dirname(url))
265       end
266     end
267     
268     def rails_env
269       @rails_env || RailsEnvironment.default
270     end
273 class Repositories
274   include Enumerable
275   
276   def initialize(cache_file = File.join(find_home, ".rails-plugin-sources"))
277     @cache_file = File.expand_path(cache_file)
278     load!
279   end
280   
281   def each(&block)
282     @repositories.each(&block)
283   end
284   
285   def add(uri)
286     unless find{|repo| repo.uri == uri }
287       @repositories.push(Repository.new(uri)).last
288     end
289   end
290   
291   def remove(uri)
292     @repositories.reject!{|repo| repo.uri == uri}
293   end
294   
295   def exist?(uri)
296     @repositories.detect{|repo| repo.uri == uri }
297   end
298   
299   def all
300     @repositories
301   end
302   
303   def find_plugin(name)
304     @repositories.each do |repo|
305       repo.each do |plugin|
306         return plugin if plugin.name == name
307       end
308     end
309     return nil
310   end
311   
312   def load!
313     contents = File.exist?(@cache_file) ? File.read(@cache_file) : defaults
314     contents = defaults if contents.empty?
315     @repositories = contents.split(/\n/).reject do |line|
316       line =~ /^\s*#/ or line =~ /^\s*$/
317     end.map { |source| Repository.new(source.strip) }
318   end
319   
320   def save
321     File.open(@cache_file, 'w') do |f|
322       each do |repo|
323         f.write(repo.uri)
324         f.write("\n")
325       end
326     end
327   end
328   
329   def defaults
330     <<-DEFAULTS
331     http://dev.rubyonrails.com/svn/rails/plugins/
332     DEFAULTS
333   end
335   def find_home
336     ['HOME', 'USERPROFILE'].each do |homekey|
337       return ENV[homekey] if ENV[homekey]
338     end
339     if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
340       return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
341     end
342     begin
343       File.expand_path("~")
344     rescue StandardError => ex
345       if File::ALT_SEPARATOR
346         "C:/"
347       else
348         "/"
349       end
350     end
351   end
353   def self.instance
354     @instance ||= Repositories.new
355   end
356   
357   def self.each(&block)
358     self.instance.each(&block)
359   end
362 class Repository
363   include Enumerable
364   attr_reader :uri, :plugins
365   
366   def initialize(uri)
367     @uri = uri.chomp('/') << "/"
368     @plugins = nil
369   end
370   
371   def plugins
372     unless @plugins
373       if $verbose
374         puts "Discovering plugins in #{@uri}" 
375         puts index
376       end
378       @plugins = index.reject{ |line| line !~ /\/$/ }
379       @plugins.map! { |name| Plugin.new(File.join(@uri, name), name) }
380     end
382     @plugins
383   end
384   
385   def each(&block)
386     plugins.each(&block)
387   end
388   
389   private
390     def index
391       @index ||= RecursiveHTTPFetcher.new(@uri).ls
392     end
396 # load default environment and parse arguments
397 require 'optparse'
398 module Commands
400   class Plugin
401     attr_reader :environment, :script_name, :sources
402     def initialize
403       @environment = RailsEnvironment.default
404       @rails_root = RailsEnvironment.default.root
405       @script_name = File.basename($0) 
406       @sources = []
407     end
408     
409     def environment=(value)
410       @environment = value
411       RailsEnvironment.default = value
412     end
413     
414     def options
415       OptionParser.new do |o|
416         o.set_summary_indent('  ')
417         o.banner =    "Usage: #{@script_name} [OPTIONS] command"
418         o.define_head "Rails plugin manager."
419         
420         o.separator ""        
421         o.separator "GENERAL OPTIONS"
422         
423         o.on("-r", "--root=DIR", String,
424              "Set an explicit rails app directory.",
425              "Default: #{@rails_root}") { |@rails_root| self.environment = RailsEnvironment.new(@rails_root) }
426         o.on("-s", "--source=URL1,URL2", Array,
427              "Use the specified plugin repositories instead of the defaults.") { |@sources|}
428         
429         o.on("-v", "--verbose", "Turn on verbose output.") { |$verbose| }
430         o.on("-h", "--help", "Show this help message.") { puts o; exit }
431         
432         o.separator ""
433         o.separator "COMMANDS"
434         
435         o.separator "  discover   Discover plugin repositories."
436         o.separator "  list       List available plugins."
437         o.separator "  install    Install plugin(s) from known repositories or URLs."
438         o.separator "  update     Update installed plugins."
439         o.separator "  remove     Uninstall plugins."
440         o.separator "  source     Add a plugin source repository."
441         o.separator "  unsource   Remove a plugin repository."
442         o.separator "  sources    List currently configured plugin repositories."
443         
444         o.separator ""
445         o.separator "EXAMPLES"
446         o.separator "  Install a plugin:"
447         o.separator "    #{@script_name} install continuous_builder\n"
448         o.separator "  Install a plugin from a subversion URL:"
449         o.separator "    #{@script_name} install http://dev.rubyonrails.com/svn/rails/plugins/continuous_builder\n"
450         o.separator "  Install a plugin and add a svn:externals entry to vendor/plugins"
451         o.separator "    #{@script_name} install -x continuous_builder\n"
452         o.separator "  List all available plugins:"
453         o.separator "    #{@script_name} list\n"
454         o.separator "  List plugins in the specified repository:"
455         o.separator "    #{@script_name} list --source=http://dev.rubyonrails.com/svn/rails/plugins/\n"
456         o.separator "  Discover and prompt to add new repositories:"
457         o.separator "    #{@script_name} discover\n"
458         o.separator "  Discover new repositories but just list them, don't add anything:"
459         o.separator "    #{@script_name} discover -l\n"
460         o.separator "  Add a new repository to the source list:"
461         o.separator "    #{@script_name} source http://dev.rubyonrails.com/svn/rails/plugins/\n"
462         o.separator "  Remove a repository from the source list:"
463         o.separator "    #{@script_name} unsource http://dev.rubyonrails.com/svn/rails/plugins/\n"
464         o.separator "  Show currently configured repositories:"
465         o.separator "    #{@script_name} sources\n"        
466       end
467     end
468     
469     def parse!(args=ARGV)
470       general, sub = split_args(args)
471       options.parse!(general)
472       
473       command = general.shift
474       if command =~ /^(list|discover|install|source|unsource|sources|remove|update|info)$/
475         command = Commands.const_get(command.capitalize).new(self)
476         command.parse!(sub)
477       else
478         puts "Unknown command: #{command}"
479         puts options
480         exit 1
481       end
482     end
483     
484     def split_args(args)
485       left = []
486       left << args.shift while args[0] and args[0] =~ /^-/
487       left << args.shift if args[0]
488       return [left, args]
489     end
490     
491     def self.parse!(args=ARGV)
492       Plugin.new.parse!(args)
493     end
494   end
495   
496   
497   class List
498     def initialize(base_command)
499       @base_command = base_command
500       @sources = []
501       @local = false
502       @remote = true
503     end
504     
505     def options
506       OptionParser.new do |o|
507         o.set_summary_indent('  ')
508         o.banner =    "Usage: #{@base_command.script_name} list [OPTIONS] [PATTERN]"
509         o.define_head "List available plugins."
510         o.separator   ""        
511         o.separator   "Options:"
512         o.separator   ""
513         o.on(         "-s", "--source=URL1,URL2", Array,
514                       "Use the specified plugin repositories.") {|@sources|}
515         o.on(         "--local", 
516                       "List locally installed plugins.") {|@local| @remote = false}
517         o.on(         "--remote",
518                       "List remotely available plugins. This is the default behavior",
519                       "unless --local is provided.") {|@remote|}
520       end
521     end
522     
523     def parse!(args)
524       options.order!(args)
525       unless @sources.empty?
526         @sources.map!{ |uri| Repository.new(uri) }
527       else
528         @sources = Repositories.instance.all
529       end
530       if @remote
531         @sources.map{|r| r.plugins}.flatten.each do |plugin| 
532           if @local or !plugin.installed?
533             puts plugin.to_s
534           end
535         end
536       else
537         cd "#{@base_command.environment.root}/vendor/plugins"
538         Dir["*"].select{|p| File.directory?(p)}.each do |name| 
539           puts name
540         end
541       end
542     end
543   end
544   
545   
546   class Sources
547     def initialize(base_command)
548       @base_command = base_command
549     end
550     
551     def options
552       OptionParser.new do |o|
553         o.set_summary_indent('  ')
554         o.banner =    "Usage: #{@base_command.script_name} sources [OPTIONS] [PATTERN]"
555         o.define_head "List configured plugin repositories."
556         o.separator   ""        
557         o.separator   "Options:"
558         o.separator   ""
559         o.on(         "-c", "--check", 
560                       "Report status of repository.") { |@sources|}
561       end
562     end
563     
564     def parse!(args)
565       options.parse!(args)
566       Repositories.each do |repo|
567         puts repo.uri
568       end
569     end
570   end
571   
572   
573   class Source
574     def initialize(base_command)
575       @base_command = base_command
576     end
577     
578     def options
579       OptionParser.new do |o|
580         o.set_summary_indent('  ')
581         o.banner =    "Usage: #{@base_command.script_name} source REPOSITORY [REPOSITORY [REPOSITORY]...]"
582         o.define_head "Add new repositories to the default search list."
583       end
584     end
585     
586     def parse!(args)
587       options.parse!(args)
588       count = 0
589       args.each do |uri|
590         if Repositories.instance.add(uri)
591           puts "added: #{uri.ljust(50)}" if $verbose
592           count += 1
593         else
594           puts "failed: #{uri.ljust(50)}"
595         end
596       end
597       Repositories.instance.save
598       puts "Added #{count} repositories."
599     end
600   end
601   
602   
603   class Unsource
604     def initialize(base_command)
605       @base_command = base_command
606     end
607     
608     def options
609       OptionParser.new do |o|
610         o.set_summary_indent('  ')
611         o.banner =    "Usage: #{@base_command.script_name} source URI [URI [URI]...]"
612         o.define_head "Remove repositories from the default search list."
613         o.separator ""
614         o.on_tail("-h", "--help", "Show this help message.") { puts o; exit }
615       end
616     end
617     
618     def parse!(args)
619       options.parse!(args)
620       count = 0
621       args.each do |uri|
622         if Repositories.instance.remove(uri)
623           count += 1
624           puts "removed: #{uri.ljust(50)}"
625         else
626           puts "failed: #{uri.ljust(50)}"
627         end
628       end
629       Repositories.instance.save
630       puts "Removed #{count} repositories."
631     end
632   end
634   
635   class Discover
636     def initialize(base_command)
637       @base_command = base_command
638       @list = false
639       @prompt = true
640     end
641     
642     def options
643       OptionParser.new do |o|
644         o.set_summary_indent('  ')
645         o.banner =    "Usage: #{@base_command.script_name} discover URI [URI [URI]...]"
646         o.define_head "Discover repositories referenced on a page."
647         o.separator   ""        
648         o.separator   "Options:"
649         o.separator   ""
650         o.on(         "-l", "--list", 
651                       "List but don't prompt or add discovered repositories.") { |@list| @prompt = !@list }
652         o.on(         "-n", "--no-prompt", 
653                       "Add all new repositories without prompting.") { |v| @prompt = !v }
654       end
655     end
657     def parse!(args)
658       options.parse!(args)
659       args = ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args.empty?
660       args.each do |uri|
661         scrape(uri) do |repo_uri|
662           catch(:next_uri) do
663             if @prompt
664               begin
665                 $stdout.print "Add #{repo_uri}? [Y/n] "
666                 throw :next_uri if $stdin.gets !~ /^y?$/i
667               rescue Interrupt
668                 $stdout.puts
669                 exit 1
670               end
671             elsif @list
672               puts repo_uri
673               throw :next_uri
674             end
675             Repositories.instance.add(repo_uri)
676             puts "discovered: #{repo_uri}" if $verbose or !@prompt
677           end
678         end
679       end
680       Repositories.instance.save
681     end
682     
683     def scrape(uri)
684       require 'open-uri'
685       puts "Scraping #{uri}" if $verbose
686       dupes = []
687       content = open(uri).each do |line|
688         begin
689           if line =~ /<a[^>]*href=['"]([^'"]*)['"]/ || line =~ /(svn:\/\/[^<|\n]*)/
690             uri = $1
691             if uri =~ /^\w+:\/\// && uri =~ /\/plugins\// && uri !~ /\/browser\// && uri !~ /^http:\/\/wiki\.rubyonrails/ && uri !~ /http:\/\/instiki/
692               uri = extract_repository_uri(uri)
693               yield uri unless dupes.include?(uri) || Repositories.instance.exist?(uri)
694               dupes << uri
695             end
696           end
697         rescue
698           puts "Problems scraping '#{uri}': #{$!.to_s}"
699         end
700       end
701     end
702     
703     def extract_repository_uri(uri)
704       uri.match(/(svn|https?):.*\/plugins\//i)[0]
705     end 
706   end
707   
708   class Install
709     def initialize(base_command)
710       @base_command = base_command
711       @method = :http
712       @options = { :quiet => false, :revision => nil, :force => false }
713     end
714     
715     def options
716       OptionParser.new do |o|
717         o.set_summary_indent('  ')
718         o.banner =    "Usage: #{@base_command.script_name} install PLUGIN [PLUGIN [PLUGIN] ...]"
719         o.define_head "Install one or more plugins."
720         o.separator   ""
721         o.separator   "Options:"
722         o.on(         "-x", "--externals", 
723                       "Use svn:externals to grab the plugin.", 
724                       "Enables plugin updates and plugin versioning.") { |v| @method = :externals }
725         o.on(         "-o", "--checkout",
726                       "Use svn checkout to grab the plugin.",
727                       "Enables updating but does not add a svn:externals entry.") { |v| @method = :checkout }
728         o.on(         "-q", "--quiet",
729                       "Suppresses the output from installation.",
730                       "Ignored if -v is passed (./script/plugin -v install ...)") { |v| @options[:quiet] = true }
731         o.on(         "-r REVISION", "--revision REVISION",
732                       "Checks out the given revision from subversion.",
733                       "Ignored if subversion is not used.") { |v| @options[:revision] = v }
734         o.on(         "-f", "--force",
735                       "Reinstalls a plugin if it's already installed.") { |v| @options[:force] = true }
736         o.separator   ""
737         o.separator   "You can specify plugin names as given in 'plugin list' output or absolute URLs to "
738         o.separator   "a plugin repository."
739       end
740     end
741     
742     def determine_install_method
743       best = @base_command.environment.best_install_method
744       @method = :http if best == :http and @method == :export
745       case
746       when (best == :http and @method != :http)
747         msg = "Cannot install using subversion because `svn' cannot be found in your PATH"
748       when (best == :export and (@method != :export and @method != :http))
749         msg = "Cannot install using #{@method} because this project is not under subversion."
750       when (best != :externals and @method == :externals)
751         msg = "Cannot install using externals because vendor/plugins is not under subversion."
752       end
753       if msg
754         puts msg
755         exit 1
756       end
757       @method
758     end
759     
760     def parse!(args)
761       options.parse!(args)
762       environment = @base_command.environment
763       install_method = determine_install_method
764       puts "Plugins will be installed using #{install_method}" if $verbose
765       args.each do |name|
766         ::Plugin.find(name).install(install_method, @options)
767       end
768     rescue StandardError => e
769       puts "Plugin not found: #{args.inspect}"
770       puts e.inspect if $verbose
771       exit 1
772     end
773   end
775   class Update
776     def initialize(base_command)
777       @base_command = base_command
778     end
779    
780     def options
781       OptionParser.new do |o|
782         o.set_summary_indent('  ')
783         o.banner =    "Usage: #{@base_command.script_name} update [name [name]...]"
784         o.on(         "-r REVISION", "--revision REVISION",
785                       "Checks out the given revision from subversion.",
786                       "Ignored if subversion is not used.") { |v| @revision = v }
787         o.define_head "Update plugins."
788       end
789     end
790    
791     def parse!(args)
792       options.parse!(args)
793       root = @base_command.environment.root
794       cd root
795       args = Dir["vendor/plugins/*"].map do |f|
796         File.directory?("#{f}/.svn") ? File.basename(f) : nil
797       end.compact if args.empty?
798       cd "vendor/plugins"
799       args.each do |name|
800         if File.directory?(name)
801           puts "Updating plugin: #{name}"
802           system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r #{@revision}" : ''}")
803         else
804           puts "Plugin doesn't exist: #{name}"
805         end
806       end
807     end
808   end
810   class Remove
811     def initialize(base_command)
812       @base_command = base_command
813     end
814     
815     def options
816       OptionParser.new do |o|
817         o.set_summary_indent('  ')
818         o.banner =    "Usage: #{@base_command.script_name} remove name [name]..."
819         o.define_head "Remove plugins."
820       end
821     end
822     
823     def parse!(args)
824       options.parse!(args)
825       root = @base_command.environment.root
826       args.each do |name|
827         ::Plugin.new(name).uninstall
828       end
829     end
830   end
832   class Info
833     def initialize(base_command)
834       @base_command = base_command
835     end
837     def options
838       OptionParser.new do |o|
839         o.set_summary_indent('  ')
840         o.banner =    "Usage: #{@base_command.script_name} info name [name]..."
841         o.define_head "Shows plugin info at {url}/about.yml."
842       end
843     end
845     def parse!(args)
846       options.parse!(args)
847       args.each do |name|
848         puts ::Plugin.find(name).info
849         puts
850       end
851     end
852   end
855 class RecursiveHTTPFetcher
856   attr_accessor :quiet
857   def initialize(urls_to_fetch, level = 1, cwd = ".")
858     @level = level
859     @cwd = cwd
860     @urls_to_fetch = urls_to_fetch.to_a
861     @quiet = false
862   end
864   def ls
865     @urls_to_fetch.collect do |url|
866       if url =~ /^svn:\/\/.*/
867         `svn ls #{url}`.split("\n").map {|entry| "/#{entry}"} rescue nil
868       else
869         open(url) do |stream|
870           links("", stream.read)
871         end rescue nil
872       end
873     end.flatten
874   end
876   def push_d(dir)
877     @cwd = File.join(@cwd, dir)
878     FileUtils.mkdir_p(@cwd)
879   end
881   def pop_d
882     @cwd = File.dirname(@cwd)
883   end
885   def links(base_url, contents)
886     links = []
887     contents.scan(/href\s*=\s*\"*[^\">]*/i) do |link|
888       link = link.sub(/href="/i, "")
889       next if link =~ /svnindex.xsl$/
890       next if link =~ /^(\w*:|)\/\// || link =~ /^\./
891       links << File.join(base_url, link)
892     end
893     links
894   end
895   
896   def download(link)
897     puts "+ #{File.join(@cwd, File.basename(link))}" unless @quiet
898     open(link) do |stream|
899       File.open(File.join(@cwd, File.basename(link)), "wb") do |file|
900         file.write(stream.read)
901       end
902     end
903   end
904   
905   def fetch(links = @urls_to_fetch)
906     links.each do |l|
907       (l =~ /\/$/ || links == @urls_to_fetch) ? fetch_dir(l) : download(l)
908     end
909   end
910   
911   def fetch_dir(url)
912     @level += 1
913     push_d(File.basename(url)) if @level > 0
914     open(url) do |stream|
915       contents =  stream.read
916       fetch(links(url, contents))
917     end
918     pop_d if @level > 0
919     @level -= 1
920   end
923 Commands::Plugin.parse!