1 # Rails Plugin Manager.
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/
14 # $ ./script/plugin install continuous_builder asset_timestamping
16 # Finding Repositories:
18 # $ ./script/plugin discover
20 # Adding Repositories:
22 # $ ./script/plugin source http://svn.protocool.com/rails/plugins/
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)
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
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.
42 # * Or, if `vendor/plugins` is not under subversion control, the
43 # plugin is pulled via `svn checkout` or `svn export` but looks
46 # Specifying revisions:
48 # * Subversion revision is a single integer.
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')
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)
68 class RailsEnvironment
75 def self.find(dir
=nil)
78 return new(dir
) if File
.exist
?(File
.join(dir
, 'config', 'environment.rb'))
79 dir
= File
.dirname(dir
)
87 def self.default
=(rails_env
)
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
)
96 plugin
= Plugins
[name_uri_or_plugin
]
99 plugin
= name_uri_or_plugin
104 puts
"Plugin not found: #{name_uri_or_plugin}"
109 require 'active_support/core_ext/kernel'
110 silence_stderr
{`svn --version` rescue nil}
111 !$
?.nil? && $
?.success
?
115 use_svn
? && File
.directory
?("#{root}/vendor/plugins/.svn")
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")
125 def best_install_method
126 return :http unless use_svn
?
128 when use_externals
? then :externals
129 when use_checkout
? then :checkout
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)
143 def externals
=(items
)
144 unless items
.is_a
? String
145 items
= items
.map
{|name
,uri
| "#{name.ljust(29)} #{uri.chomp('/')}"}.join("\n")
147 Tempfile
.open("svn-set-prop") do |file
|
150 system("svn propset -q svn:externals -F \"#{file.path}\" \"#{root}/vendor/plugins\"")
157 attr_reader
:name, :uri
159 def initialize(uri
, name
=nil)
165 name
=~
/\// ? new(name
) : Repositories
.instance
.find_plugin(name
)
169 "#{@name.ljust(30)}#{@uri}"
173 @uri =~
/svn(?:\+ssh)?:\/\
/*/
177 @uri =~
/^git:\/\
// || @uri =~
/\.git$/
181 File
.directory
?("#{rails_env.root}/vendor/plugins/#{name}") \
182 or rails_env
.externals
.detect
{ |name
, repo
| self.uri
== repo
}
185 def install(method
=nil, options
= {})
186 method
||= rails_env
.best_install_method
?
188 method
= :export if svn_url
?
189 method
= :git if git_url
?
192 uninstall
if installed
? and options
[:force]
195 send("install_using_#{method}", options
)
198 puts
"already installed: #{name} (#{uri}). pass --force to reinstall"
203 path
= "#{rails_env.root}/vendor/plugins/#{name}"
204 if File
.directory
?(path
)
205 puts
"Removing 'vendor/plugins/#{name}'" if $verbose
209 puts
"Plugin doesn't exist: #{path}"
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
218 tmp
= "#{rails_env.root}/_tmp_about.yml"
220 cmd
= "svn export #{@uri} \"#{rails_env.root}/#{tmp}\""
224 open(svn_url
? ? tmp
: File
.join(@uri, 'about.yml')) do |stream
|
226 end rescue "No about.yml found in #{uri}"
228 FileUtils
.rm_rf tmp
if svn_url
?
234 install_hook_file
= "#{rails_env.root}/vendor/plugins/#{name}/install.rb"
235 load install_hook_file
if File
.exist
? install_hook_file
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
243 def install_using_export(options
= {})
244 svn_command
:export, options
247 def install_using_checkout(options
= {})
248 svn_command
:checkout, options
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
)
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]
269 def install_using_git(options
= {})
270 root
= rails_env
.root
271 install_path
= mkdir_p
"#{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
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
282 puts
"removing: .git .gitignore" if $verbose
283 rm_rf
%w(.git
.gitignore
)
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
301 @name = File
.basename(url
)
302 if @name == 'trunk' || @name.empty
?
303 @name = File
.basename(File
.dirname(url
))
305 @name.gsub
!(/\.git$/, '') if @name =~
/\.git$/
309 @rails_env || RailsEnvironment
.default
316 def initialize(cache_file
= File
.join(find_home
, ".rails-plugin-sources"))
317 @cache_file = File
.expand_path(cache_file
)
322 @repositories.each(&block
)
326 unless find
{|repo
| repo
.uri
== uri
}
327 @repositories.push(Repository
.new(uri
)).last
332 @repositories.reject
!{|repo
| repo
.uri
== uri
}
336 @repositories.detect
{|repo
| repo
.uri
== uri
}
343 def find_plugin(name
)
344 @repositories.each
do |repo
|
345 repo
.each
do |plugin
|
346 return plugin
if plugin
.name
== name
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
) }
361 File
.open(@cache_file, 'w') do |f
|
371 http://dev.rubyonrails.com/svn/rails/plugins/
376 ['HOME', 'USERPROFILE'].each
do |homekey
|
377 return ENV[homekey
] if ENV[homekey
]
379 if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
380 return "#{ENV['HOMEDRIVE']}:#{ENV['HOMEPATH']}"
383 File
.expand_path("~")
384 rescue StandardError
=> ex
385 if File
::ALT_SEPARATOR
394 @instance ||= Repositories
.new
397 def self.each(&block
)
398 self.instance
.each(&block
)
404 attr_reader
:uri, :plugins
407 @uri = uri
.chomp('/') << "/"
414 puts
"Discovering plugins in #{@uri}"
418 @plugins = index
.reject
{ |line
| line
!~
/\/$/ }
419 @plugins.map
! { |name
| Plugin
.new(File
.join(@uri, name
), name
) }
431 @index ||= RecursiveHTTPFetcher
.new(@uri).ls
436 # load default environment and parse arguments
441 attr_reader
:environment, :script_name, :sources
443 @environment = RailsEnvironment
.default
444 @rails_root = RailsEnvironment
.default
.root
445 @script_name = File
.basename($0)
449 def environment
=(value
)
451 RailsEnvironment
.default
= value
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."
461 o
.separator
"GENERAL OPTIONS"
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
}
469 o
.on("-v", "--verbose", "Turn on verbose output.") { |verbose
| $verbose = verbose
}
470 o
.on("-h", "--help", "Show this help message.") { puts o
; exit
}
473 o
.separator
"COMMANDS"
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."
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"
511 def parse
!(args
=ARGV)
512 general
, sub
= split_args(args
)
513 options
.parse
!(general
)
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)
520 puts
"Unknown command: #{command}"
528 left
<< args
.shift
while args
[0] and args
[0] =~
/^-/
529 left
<< args
.shift
if args
[0]
533 def self.parse
!(args
=ARGV)
534 Plugin
.new
.parse
!(args
)
540 def initialize(base_command
)
541 @base_command = base_command
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."
553 o
.separator
"Options:"
555 o
.on( "-s", "--source=URL1,URL2", Array
,
556 "Use the specified plugin repositories.") {|sources
| @sources = sources
}
558 "List locally installed plugins.") {|local
| @local, @remote = local
, false}
560 "List remotely available plugins. This is the default behavior",
561 "unless --local is provided.") {|remote
| @remote = remote
}
567 unless @sources.empty
?
568 @sources.map
!{ |uri
| Repository
.new(uri
) }
570 @sources = Repositories
.instance
.all
573 @sources.map
{|r
| r
.plugins
}.flatten
.each
do |plugin
|
574 if @local or !plugin
.installed
?
579 cd
"#{@base_command.environment.root}/vendor/plugins"
580 Dir
["*"].select
{|p
| File
.directory
?(p
)}.each
do |name
|
589 def initialize(base_command
)
590 @base_command = base_command
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."
599 o
.separator
"Options:"
601 o
.on( "-c", "--check",
602 "Report status of repository.") { |sources
| @sources = sources
}
608 Repositories
.each
do |repo
|
616 def initialize(base_command
)
617 @base_command = base_command
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."
632 if Repositories
.instance
.add(uri
)
633 puts
"added: #{uri.ljust(50)}" if $verbose
636 puts
"failed: #{uri.ljust(50)}"
639 Repositories
.instance
.save
640 puts
"Added #{count} repositories."
646 def initialize(base_command
)
647 @base_command = base_command
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."
656 o
.on_tail("-h", "--help", "Show this help message.") { puts o
; exit
}
664 if Repositories
.instance
.remove(uri
)
666 puts
"removed: #{uri.ljust(50)}"
668 puts
"failed: #{uri.ljust(50)}"
671 Repositories
.instance
.save
672 puts
"Removed #{count} repositories."
678 def initialize(base_command
)
679 @base_command = base_command
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."
690 o
.separator
"Options:"
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
}
701 args
= ['http://wiki.rubyonrails.org/rails/pages/Plugins'] if args
.empty
?
703 scrape(uri
) do |repo_uri
|
707 $stdout.print
"Add #{repo_uri}? [Y/n] "
708 throw :next_uri if $stdin.gets
!~
/^y?$/i
717 Repositories
.instance
.add(repo_uri
)
718 puts
"discovered: #{repo_uri}" if $verbose or !@prompt
722 Repositories
.instance
.save
727 puts
"Scraping #{uri}" if $verbose
729 content
= open(uri
).each
do |line
|
731 if line
=~
/<a[^>]*href=['"]([^'"]*)['"]/ || line
=~
/(svn:\/\
/[^<|\n]*)/
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
)
740 puts
"Problems scraping '#{uri}': #{$!.to_s}"
745 def extract_repository_uri(uri
)
746 uri
.match(/(svn|https?):.*\/plugins\
//i
)[0]
751 def initialize(base_command
)
752 @base_command = base_command
754 @options = { :quiet => false, :revision => nil, :force => false }
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."
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 }
782 o
.separator
"You can specify plugin names as given in 'plugin list' output or absolute URLs to "
783 o
.separator
"a plugin repository."
787 def determine_install_method
788 best
= @base_command.environment
.best_install_method
789 @method = :http if best
== :http and @method == :export
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."
807 environment
= @base_command.environment
808 install_method
= determine_install_method
809 puts
"Plugins will be installed using #{install_method}" if $verbose
811 ::Plugin.find(name
).install(install_method
, @options)
813 rescue StandardError
=> e
814 puts
"Plugin not found: #{args.inspect}"
815 puts e
.inspect
if $verbose
821 def initialize(base_command
)
822 @base_command = base_command
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."
838 root
= @base_command.environment
.root
840 args
= Dir
["vendor/plugins/*"].map
do |f
|
841 File
.directory
?("#{f}/.svn") ? File
.basename(f
) : nil
842 end.compact
if args
.empty
?
845 if File
.directory
?(name
)
846 puts
"Updating plugin: #{name}"
847 system("svn #{$verbose ? '' : '-q'} up \"#{name}\" #{@revision ? "-r
#{@revision}" : ''}")
849 puts
"Plugin doesn't exist: #{name}"
856 def initialize(base_command
)
857 @base_command = base_command
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."
870 root
= @base_command.environment
.root
872 ::Plugin.new(name
).uninstall
878 def initialize(base_command
)
879 @base_command = base_command
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."
893 puts
::Plugin.find(name
).info
900 class RecursiveHTTPFetcher
902 def initialize(urls_to_fetch
, level
= 1, cwd
= ".")
905 @urls_to_fetch = RUBY_VERSION >= '1.9' ? urls_to_fetch
.lines
: urls_to_fetch
.to_a
910 @urls_to_fetch.collect
do |url
|
911 if url
=~
/^svn(\+ssh)?:\/\
/.*/
912 `svn ls #{url}`.split("\n").map
{|entry
| "/#{entry}"} rescue nil
914 open(url
) do |stream
|
915 links("", stream
.read
)
922 @cwd = File
.join(@cwd, dir
)
923 FileUtils
.mkdir_p(@cwd)
927 @cwd = File
.dirname(@cwd)
930 def links(base_url
, contents
)
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
)
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
)
950 def fetch(links
= @urls_to_fetch)
952 (l
=~
/\/$/ || links
== @urls_to_fetch) ? fetch_dir(l
) : download(l
)
958 push_d(File
.basename(url
)) if @level > 0
959 open(url
) do |stream
|
960 contents
= stream
.read
961 fetch(links(url
, contents
))
968 Commands
::Plugin.parse
!