#!/usr/bin/env ruby begin require 'lib/bnr_tools' rescue end require 'svn_xml' require 'fileutils' require 'open4' module SvnCommands DIGG_DEFAULT_SVN_URL="svn+ssh://svn.digg.internal/repository" DIGG_DEFAULT_WC_PATH="#{ENV['HOME']}/diggRepository" class SvnException < RuntimeError def initialize(status_code, output) @status_code = status_code @output = output end def to_s "Status code:#{@status_code} / output:#{@output} #{super}" end end class << self def svnVersion @@svnVersion ||= VersionNumber.new(`svn --version --quiet`) end def extra_merge_flags (Integer(svnVersion.major) >= 1 && Integer(svnVersion.minor) >= 5) ? ["--accept postpone"] : [] end def new_cmd(command_string, expected_result_class=nil, max_retries=3) puts "Executing: '#{command_string}'" retry_count = 0 begin out = "" err = "" status = Open4::popen4(command_string) do |pid, stdin, stdout, stderr| stdin.close out = stdout.read err = stderr.read end unless status.exitstatus == 0 raise SvnException.new(status.exitstatus, "stdout: #{out}\n\nstderr: #{err}") end if (expected_result_class) parsed = REXML::Document.new(out) expected_result_class.new(parsed.elements[expected_result_class.expected_node_name]) else nil end rescue Exception => e if e.kind_of?(Interrupt) exit(1) elsif retry_count < max_retries # puts "invocation failed with #{e.class} #{e} ... trying again" retry_count += 1 retry else raise e end end end def copy(fromPath, toPath, extra_args=nil, working_copy_path=nil, commit_msg=nil, repository_base_url=DIGG_DEFAULT_SVN_URL) path_base = working_copy_path || repository_base_url full_from_path = "#{path_base.chomp("/")}/#{fromPath}" full_to_path = "#{path_base.chomp("/")}/#{toPath}" if (working_copy_path) SvnCommands.new_cmd("svn cp #{Array(extra_args).join(" ")} \"#{full_from_path}\" \"#{full_to_path}\"",nil,1) else if (commit_msg) SvnCommands.new_cmd("svn cp #{Array(extra_args).join(" ")} \"#{full_from_path}\" \"#{full_to_path}\" -m\"#{commit_msg}\"",nil,1) else raise SvnException.new(-1, "Without a commit message, I can't svn cp a remote URL. Sorry.") end end end def merge(fromPath, toPath, start_rev, end_rev = nil, extra_args=nil, working_copy_path=nil, repository_base_url=DIGG_DEFAULT_SVN_URL) extra_args = (Array(extra_args) << SvnCommands.extra_merge_flags).compact full_from_path = "#{repository_base_url.chomp("/")}/#{fromPath}" full_to_path = "#{working_copy_path.chomp("/")}/#{toPath}" rev_spec = begin if end_rev.nil?() || Integer(end_rev) == 0 "-c #{Integer(start_rev)}" else "-r#{Integer(start_rev)}:#{Integer(end_rev)}" end end SvnCommands.new_cmd("svn merge #{rev_spec} #{extra_args.join(" ")} \"#{full_from_path}\" \"#{full_to_path}\"") end def sync(fromPath, toPath, working_copy_path, extra_args=nil, repository_base_url=DIGG_DEFAULT_SVN_URL) extra_args = (Array(extra_args) << SvnCommands.extra_merge_flags).compact repository_from_path = "#{repository_base_url.chomp("/")}/#{fromPath}" repository_to_path = "#{repository_base_url.chomp("/")}/#{toPath}" wc_to_path = "#{working_copy_path.chomp("/")}/#{toPath}" SvnCommands.new_cmd("svn merge #{extra_args.join(" ")} \"#{repository_to_path}\" \"#{repository_from_path}\" \"#{wc_to_path}\"") end def mkdir(path, commit_msg=nil, working_copy_path=nil, repository_base_url=DIGG_DEFAULT_SVN_URL) path_base = working_copy_path || repository_base_url full_path = "#{path_base.chomp("/")}/#{path}" extra_args = (Array(extra_args) << SvnCommands.extra_merge_flags).compact if (working_copy_path) SvnCommands.new_cmd("svn mkdir #{full_path}") else if (commit_msg) SvnCommands.new_cmd("svn mkdir #{full_path} -m'#{commit_msg}'") else raise SvnException.new(-1, "No commit message specified; cannot mkdir directly in the repository") end end end end class PathOp attr_accessor :fullpath, :entries def initialize(relativePath, repository_base_url=DIGG_DEFAULT_SVN_URL) @fullpath=relativePath.nil? ? repository_base_url : "#{repository_base_url}/#{relativePath}" end end class Ls < PathOp attr_accessor :lists @@all_lists ||= {} def initialize(relativePath=nil, extra_args=nil, force_update=false, repository_base_url=DIGG_DEFAULT_SVN_URL) super(relativePath, repository_base_url) cmd = "svn ls --xml #{Array(extra_args).join(" ")} #{fullpath}" @@all_lists[fullpath] = if (force_update || @@all_lists[fullpath].nil?) SvnCommands.new_cmd(cmd, SvnXml::Lists) else @@all_lists[fullpath] end @lists = @@all_lists[fullpath] @entries = @lists.entries end end class Log < PathOp attr_reader :entries @@all_entries ||= {} def initialize(relativePath=nil, extra_args=nil, force_update=false, repository_base_url=DIGG_DEFAULT_SVN_URL) super(relativePath, repository_base_url) @@all_entries[fullpath] = if (force_update || @@all_entries[fullpath].nil?) SvnCommands.new_cmd("svn log -vv --xml #{Array(extra_args).join(" ")} #{fullpath}", SvnXml::Log).entries else @@all_entries[fullpath] end @entries = @@all_entries[fullpath] end def messages @messages ||= self.entries.collect { |entry| entry.msg } end def revisions @revisions ||= self.entries.collect { |entry| entry.revision } end def oldest_revision self.revisions.sort.first end def newest_revision self.revisions.sort.last end end class Update < PathOp @@shallow_paths ||= {} @@recursive_paths ||= {} def updatedShallow?(path) path_parent = File.dirname(path) if [".","/",""].include?(path_parent) @@shallow_paths[path] || false else @@shallow_paths[path] || updatedRecursive?(path_parent) end end def updatedRecursive?(path) path_parent = File.dirname(path) if [".","/",""].include? path_parent @@recursive_paths[path] || false else @@recursive_paths[path] || updatedRecursive?(path_parent) end end def alreadyUpdated?(shallowUpdate) if shallowUpdate updatedShallow?(fullpath) else updatedRecursive?(fullpath) end end def pretty_hash(h) if h.kind_of?(Hash) h.inject(nil) { |memo, kv| key = kv[0] val = kv[1] (memo ? "#{memo}; " : "") + "#{key}=#{pretty_hash(val)}" } else h.to_s end end def updateCache(shallow) if shallow @@shallow_paths[fullpath] ||= 0 @@shallow_paths[fullpath] += 1 #puts "Updated shallow cache; current contents #{pretty_hash(@@shallow_paths)}" else @@recursive_paths[fullpath] ||= 0 @@recursive_paths[fullpath] += 1 #puts "Updated recursive cache; current contents #{pretty_hash(@@recursive_paths)}" end end def ensureWorkingCopyExistsAt(aPath) out = `svn info #{aPath}` unless $? == 0 FileUtils.mkdir_p(aPath) SvnCommands.new_cmd("svn co -N svn+ssh://svn.digg.internal/repository/ #{aPath.chomp("/")}") end end def initialize(path, extra_args=nil, force_update=false, shallow=false, working_copy_path=DIGG_DEFAULT_WC_PATH) super(path, working_copy_path) cache_hit = alreadyUpdated?(shallow) #puts "Update requested on #{path} - cache hit = #{cache_hit}" if force_update || cache_hit == false || cache_hit.nil? argPile = Array(extra_args) if (shallow) argPile << "-N" end argPile << fullpath if (path == nil || path.size < 2) ensureWorkingCopyExistsAt(working_copy_path) else wc_fullpath = "#{working_copy_path}/#{path}" unless (File.exist? wc_fullpath) # make sure our parent dir is in good shape ... #puts "Hm, #{fullpath} doesn't exist. Calling checkOut (shallow) on #{File.dirname(relative_path)} ..." SvnCommands::Update.new(File.dirname(path), nil, force_update, true, working_copy_path) end end SvnCommands.new_cmd("svn up #{argPile.join(" ")}") end updateCache(shallow) end end end