~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/railties/lib/commands/plugin.rb

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

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