#!/usr/local/bin/ruby require 'benchmark' require 'fileutils' QUIET = 0 NORMAL = 1 VERBOSE = 2 class External def self.externals_in_directory(parent) exts = [] prop = `svn propget svn:externals #{parent}` prop.split("\n").each do |prop_line| next if prop_line.strip.empty? exts << External.new(parent, prop_line) end exts end attr_reader :name, :path, :url, :revision def initialize(parent, property) match = /^(\S+)\s+(-r\s*\d*\s+)?(\S+)\s*$/.match(property) @name = match[1] @revision = match[2] ? match[2].gsub(/\D/,'').to_i : nil @url = match[3] @path = "#{parent}/#{@name}" end def ==(other) (path == other.path and url == other.url) end def to_s "External[name=#{name}, path=#{path}, url=#{url}, revision=#{revision}]" end def revision_actual info = `svn info #{@path}` info.grep(/^Revision:/).first.gsub(/Revision: /, '').to_i end def should_update? revision == nil || revision_actual != revision end end class Status class Line def initialize(text) text.strip! @modified = /^M/.match(text) ? true : false @external = /^X/.match(text) ? true : false @unversioned = /^\?/.match(text) ? true : false @modified_properties = /^.M/.match(text) ? true : false @path = text.gsub(/^...... /, '') end attr_reader :path def modified? @modified end def external? @external end def unversioned? @unversioned end def modified_properties? @modified_properties end end def initialize(text) @lines = text.split("\n").collect do |line| Status::Line.new(line) end end def modified @lines.select { |line| line.modified? }.collect {|line| line.path} end def externals @lines.select { |line| line.external? }.collect {|line| line.path} end def unversioned @lines.select { |line| line.unversioned? }.collect {|line| line.path} end end class Sub DEFAULT_BASE_URL = "svn+ssh://rubyforge.org/var/svn" def self.from_args(args) verbosity = NORMAL still_parsing_options = true command = :up options = {} while still_parsing_options case args[0] when 'up', 'co', 'help' options[:command] = args[0] when '-v', '--verbose' options[:verbosity] = VERBOSE when '-q', '--quiet' options[:verbosity] = QUIET when '-h', '--help' options[:command] = 'help' still_parsing_options = false when '-c', '--clean' options[:clean] = true when '--url' args.shift options[:url] = args.shift else still_parsing_options = false end args.shift if still_parsing_options end Sub.new(options, args) end attr_reader :verbosity, :clean, :command, :args, :url def initialize(options = {:verbosity => NORMAL}, args = []) options = defaults.merge(options) @verbosity = options[:verbosity] @clean = options[:clean] @command = options[:command] @url = options[:url] || ENV['SUB_BASE_URL'] || DEFAULT_BASE_URL @args = args @status = {} end def defaults { :verbosity => NORMAL, :command => :up, } end def execute self.send(@command) end # commands def up if @args.empty? @args = [`pwd`.chomp] end update_many(@args) end def co if @args.empty? raise "Please specify a project to check out" end project = args.shift dir_name = args.shift || project svn("co #{url}/#{project}/trunk #{dir_name}") end def help puts """ sub - Alex's wrapper for subversion Usage: sub co project_name [dir_name] checks out [base_url]/project_name/trunk into ./project_name (or dir_name if specified) sub up [dir]* fast update (default command, so 'sub dir...' or just 'sub' work too) sub help prints this message Options: --verbose, -v lots of output --quiet, -q no output at all except for errors --help, -h prints this message --clean, -c 'up' command removes all unversioned files and directories --url [base_url] sets base repository url for 'co' command (default is ENV['SUB_BASE_URL'] or #{DEFAULT_BASE_URL}) """ end # methods def update_many(roots) roots.each do |root| say "Updating #{root}" b = Benchmark.measure { update(root) } say "Updated %s in %.2f sec" % [root, b.real] end end def update(root) if @clean remove_unversioned(root) end externals_before = externals(root) svn("up --ignore-externals #{root}") externals = externals(root) # for some reason (array - array) doesn't work right here # so i had to write my own subtract method removed_externals = externals_before.subtract(externals) removed_externals.each do |ext| say "Removed external #{ext}" FileUtils.rm_rf(ext.path) end added_externals = externals.subtract(externals_before) existing_externals = externals.subtract(added_externals) # todo: extract Processes processes = [] def update_external(processes, external) pid = fork do say "Updating external #{external.path}" run("svn cleanup #{external.path}") if File.exists?(external.path) rev = external.revision.nil? ? '' : "-r#{external.revision}" svn("up #{rev} #{external.path} | grep -v 'At revision'") end processes << {:pid => pid, :external => external} end def checkout_external(processes, external) pid = fork do say "Checking out external #{external.path}" rev = external.revision.nil? ? '' : "-r#{external.revision}" svn("co #{rev} #{external.url} #{external.path}") end processes << {:pid => pid, :external => external} end added_externals.each do |external| checkout_external(processes, external) end already_up_to_date = [] existing_externals.each do |external| if external.should_update? update_external(processes, external) else already_up_to_date << external end end unless already_up_to_date.empty? say("External#{'s' if already_up_to_date.size > 1} " + already_up_to_date.collect {|external| external.name}.join(", ") + " already up to date") end processes.each do |process| Process.waitpid(process[:pid], 0) end end def remove_unversioned(root) status(root).unversioned.each do |path| if File.directory?(path) say "Removing unversioned directory #{path}" else say "Removing unversioned file #{path}" end FileUtils.rm_rf(path) end end def status(root) @status[root] ||= Status.new(run("svn st #{root}", true)) end def externals(root) exts = [] directories_containing_externals(root).collect do |parent| exts += External.externals_in_directory(parent) end exts end def directories_containing_externals(root) status(root).externals.collect do |path| if (path !~ /\//) "." else path.gsub(/\/[^\/]*$/, '') end end.uniq end def parse_externals(st) exts = [] st.split("\n").select do |line| line =~ /^X/ end.collect do |line| line.gsub(/^X */, '').gsub(/\/[^\/]*$/, '') end.uniq.collect do |parent| prop = `svn propget svn:externals #{parent}` prop.split("\n").each do |external| next if external.strip.empty? exts << External.new(parent, external) end end exts end def say(msg) puts msg if verbosity > QUIET end def svn(cmd) svncmd = "svn" svncmd += " --quiet" if (cmd =~ /^(up|co)/ && verbosity == QUIET) run("#{svncmd} #{cmd}") end def run(cmd, return_output = false) say("\t#{cmd}") if verbosity == VERBOSE if (return_output) `#{cmd}` else cmd += ">/dev/null"if verbosity == QUIET system(cmd) end end end class Array def subtract(other) self.select do |item| !other.has?(item) end end def has?(something) self.each do |item| return true if item == something end false end end