# This script replicates changelists between 2 Perforce servers # See p4replicate.html for notes and restrictions. # Author: Robert Cowham # Copyright: 2005, Robert Cowham # License: see license.txt require "P4" require "getoptlong" require "net/smtp" iniFile = '' @@g_trace = 0 def trace(*args) if @@g_trace > 0 then $stdout.puts args end end class IniFile attr_reader :file; attr_accessor :sections; def initialize(file = '') if !file.empty? and !test(?f, file) then raise "'#{file}' does not exist or is not a plain file.\n" end @file = file || ''; @config = Hash.new; unless @file.empty? then _parseIniFile; end end def to_s() result = ''; @config.keys.sort.each do |key| result.concat("#{key}="); if @config[key].is_a? Array then result.concat(@config[key].join("\n\t") + "\n\n") else result.concat(@config[key] + "\n\n"); end end return result; end def [](k) return @config[k]; end # --- Private methods --- private; # .ini parser def _parseIniFile currentKey = nil; open(@file, 'r') do |fp| line = ''; until fp.eof? if line.empty? then line = fp.gets; else tmp = fp.gets; line.concat(tmp[2, tmp.size - 2]); end # if line.empty? line.chomp!; line, currentKey = _parseLine(line, currentKey); if line != nil then next; end line = ''; end # until fp.eof? end # open end # _parseIniFile # Line parser def _parseLine(line, currentKey) case line when /^\s*\#|^\s*$/ ; # Ignore comment lines when /^([^=]+?)=/ value = $'; key = $1; @config[key] = value currentKey = '' when /^([^:]+?):/ # Views have terminating ":" value = $'; key = $1; currentKey = key @config[key] = Array.new when /^\s+(\S+.*)$/ # continuation of view view = $1 if !/view/.match(currentKey) then raise "view line defined out of order.\n"; end @config[currentKey] << view when '' currentKey = nil; when ';' ; end # case line return nil, currentKey; end end # class IniFile def sendMail(server, from, to, subject, text) raise "mail server not specified" if server.length == 0 raise "sender address not specified" if from.length == 0 raise "receiver address not specified" if to.length == 0 smtp = Net::SMTP.new(server) smtp.start() smtp.ready(from, to) do |wa| wa.write("From: #{from}\r\n") wa.write("Reply-To: #{from}\r\n") wa.write("To: #{to.join(',')}\r\n") wa.write("Subject: #{subject}\r\n") wa.write("\r\n") wa.write("#{text}\r\n") wa.write("\r\n") end # smtp.ready(...) end # def sendMail() class ConfigException < Exception attr_reader :param def initialize(param) @param = param end end class Counter attr_reader :value def initialize(p4, name) @p4 = p4 @name = name @value = p4.run_counter(name)[0].to_i end def incr @value = @value + 1 @p4.run_counter(@name, @value) end end class Change def initialize(p4src, p4dest, change_number) @p4src = p4src @p4dest = p4dest @change_number = change_number @ignored_files = Array.new @src_change = @p4src.run("describe", "-s", change_number)[0] @depot_files = @src_change["depotFile"] if !@depot_files.nil? then trace "Change depot_files:\n", @depot_files actions = @src_change["action"] i = 0 while i < @depot_files.size open_file(@depot_files[i], actions[i]) i += 1 end end end def submit if !@depot_files.nil? then change = @p4dest.fetch_change extra_desc = "" if @ignored_files.size > 0 then extra_desc = "\nIgnored files from source changelist:\n" + @ignored_files.join("\n") end change["Description"] = "Replicated change #{@src_change['change']} " + "from server: '#{@p4src.port?}' \n" + "User: " + @src_change["user"] + " Time: " + Time.at(@src_change["time"].to_i).strftime("%Y/%m/%d %H:%M:%S") + "\n" + "Original Description:\n\n" + @src_change["desc"] + extra_desc result = @p4dest.save_submit(change) trace "Submit result:\n" + result.join("\n") # Also edit Source change - requires admin privileges # change_no = '' # result.reverse.each do |line| # if /^Change (\d+) submitted.$/.match(line) then # change_no = $1 # break # elsif /^Change \d+ renamed change (\d+) and submitted.$/.match(line) then # change_no = $1 # break # end # end # if change_no == '' then # raise "Couldn't find change no in submitted change:\n" + result.join("\n") # end # src_update = @p4src.fetch_change(@src_change["change"]) # src_update["Description"] = "Replicated by change #{change_no} " + # "on server: '#{@p4dest.port?}'\n" + src_update["Description"] # result = @p4src.save_change(src_update, "-f") trace "Src not changed update result:\n" + result.join("\n") end end private def open_file(depot_file_name, action) trace "open_file fstat on: '#{depot_file_name}'" fstat = @p4src.run("fstat", "-l", depot_file_name)[0] trace "fstat result:\n" + fstat.to_a.join(" ") # handle files in changelist but not in replication view if !fstat.has_key?("clientFile") || fstat["clientFile"] == "" then @ignored_files << depot_file_name return end local_file_name = fstat["clientFile"] file_type = fstat["headType"] trace "Action '#{action}' on '#{local_file_name}'\n" case action when 'add', 'branch', 'import' result = @p4dest.run("add", "-t" +file_type, local_file_name) trace "Action result: \n" + result.join("\n") if result.join(" ") =~ /can\'t add existing file/ then raise "Error adding existing file '#{local_file_name}'" end when 'edit', 'integrate' result = @p4dest.run("edit", "-t" +file_type, local_file_name) trace "Action result: \n" + result.join("\n") when 'delete' result = @p4dest.run("delete", local_file_name) trace "Action result: \n" + result.join("\n") else raise "Unknown action #{action}" end end end class View attr_reader :src attr_reader :depot_path attr_reader :dest attr_reader :view def initialize(view_line, client_name, is_src) # Parse for standard view lines - dealing with quotes if /^\s*(\S*)\s+(\S*)\s*$/.match(view_line) then parse_details($1, $2) elsif /^\s*\"([^\"]*)\"\s+(\S*)\s*$/.match(view_line) then parse_details($1, $2) elsif /^\s*\"([^\"]*)\"\s+\"([^\"]*)\"\s*$/.match(view_line) then parse_details($1, $2) elsif /^\s*(\S*)\s+\"([^\"]*)\"\s*$/.match(view_line) then parse_details($1, $2) else raise "Problem with view: #{view_line}" end # construct view if is_src then lhs = @src else # dest lhs = @dest end @view = "\"" + lhs + "\" \"//" + client_name + "/" + @depot_path + "\"" end private def parse_details(src, dest) @src = src @dest = dest if /^\/\/(.*)$/.match(@src) then @depot_path = $1 else raise "Invalid src part of view: #{@src}" end end end class Client attr_reader :name attr_reader :root def initialize(p4, root, view, is_src) @name = p4.client? @root = root @client = p4.fetch_client(@name) trace "Client:\n" + @client.to_a.join("\n") @client["Root"] = @root # turn on clobber for src if is_src then @client["Options"].sub!("noclobber", "clobber") end # Now copy the views into the client - overwriting what's there @client["View"] = Array.new view.each do |view_line| v = View.new(view_line, @name, is_src) @client["View"] << v.view end p4.save_client(@client) end end def replicate(iniFile) begin p4src = P4.new p4src.parse_forms p4dest = P4.new p4dest.parse_forms # Check on required parameters required_params = ["src_p4port", "src_p4client", "src_p4clientroot", "src_p4user", "src_counter", "view", "dest_p4port", "dest_p4client", "dest_p4user"] required_params.each do |p| if iniFile[p].nil? then raise ConfigException.new(p) end end p4src.port = iniFile["src_p4port"] p4src.client = iniFile["src_p4client"] p4src.user = iniFile["src_p4user"] p4src.password = iniFile["src_p4password"] if !iniFile["src_p4password"].nil? p4src.exception_level = 1 p4src.connect p4dest.port = iniFile["dest_p4port"] p4dest.client = iniFile["dest_p4client"] p4dest.user = iniFile["dest_p4user"] p4dest.password = iniFile["dest_p4password"] if !iniFile["dest_p4password"].nil? p4dest.exception_level = 1 p4dest.connect counter = Counter.new(p4src, iniFile["src_counter"]) trace "P4Counter #{counter.value}\n" dest_client = Client.new(p4dest, iniFile["src_p4clientroot"], iniFile["view"], false) src_client = Client.new(p4src, iniFile["src_p4clientroot"], iniFile["view"], true) trace "Src_client: #{src_client.name}\n" trace "Src_client Root dir: #{src_client.root}\n" # Check for partial runs and get user to reset status opened = p4dest.run("opened") if opened.size > 0 then raise "Error - files are currently open in destination client:\n" + opened.join("\n") + "\nPlease revert files\n" end # For all changes in the client greater than counter value, replicate those changes begin result = p4src.run("changes", "-m1", "-ssubmitted", "//" + src_client.name + "/...") if result.nil? || result[0].nil? then max_client_change = 0 else max_client_change = result[0]["change"].to_i end rescue max_client_change = 0 end trace "Max client #{src_client.name} change: #{max_client_change}\n" changelist_count = 0 # Note that the following 2 lines is for safety and may not be necessary. # If running over a slow connection to server then use a p4proxy result = p4src.run("sync", "#none") result = p4dest.run("flush") while counter.value <= max_client_change # Check if the next change is relevant to source client result = p4src.run("changes", "-m1", "-ssubmitted", "//" + src_client.name + "/...@" + counter.value.to_s)[0] trace "Counter:" + counter.value.to_s if !result.nil? then trace "Change relevant:" + result["change"] if result["change"] == counter.value.to_s then print "Replicating change: #{result['change']}\r" trace "Syncing to @#{counter.value.to_s}\n" result = p4src.run("sync", "@" + counter.value.to_s) trace "Result of sync:\n" + result.join("\n") # Only replicate a change if the sync did something... if result.size > 0 then change = Change.new(p4src, p4dest, counter.value) change.submit changelist_count += 1 end trace "Synced: #{result.size} files\n" end end counter.incr end p4src.disconnect p4dest.disconnect return max_client_change, changelist_count rescue ConfigException => ce $stderr.print "Config parameter #{ce.param} missing!" rescue P4Exception # If any errors occur, we'll jump in here. Just log them # and raise the exception up to the higher level p4src.errors.each { |e| $stderr.puts( e ) } p4dest.errors.each { |e| $stderr.puts( e ) } raise rescue p4src.errors.each { |e| $stderr.puts( e ) } p4dest.errors.each { |e| $stderr.puts( e ) } raise end end if __FILE__ == $0 # process the parsed options opts = GetoptLong.new( [ "--verbose", "-v", GetoptLong::NO_ARGUMENT ] ) opts.each do |opt, arg| if opt == '--verbose' then @@g_trace = 1 end end if ARGV[0] config_file = ARGV[0] else config_file = "p4replicate.ini" end begin iniFile = IniFile.new(config_file) last_change_number, changes_replicated = replicate(iniFile) if changes_replicated > 0 then msg = "Number of changes replicated: #{changes_replicated}\n" + "Last change processed: #{last_change_number}" print msg titl = "p4replicate: " + config_file + "; changes: #{changes_replicated}; last: #{last_change_number};" if !iniFile["mail_success_to"].nil? then sendMail(iniFile["mail_server"], iniFile["mail_from"], iniFile["mail_success_to"].split(","), titl, msg) end end rescue => err msg = "Error in script: #{err}\n\n" + "-----------------------\n" + "Stacktrace for support:\n" + $!.backtrace.join("\n") $stderr.puts msg if !iniFile["mail_fail_to"].nil? then sendMail(iniFile["mail_server"], iniFile["mail_from"], iniFile["mail_fail_to"].split(","), "p4replicate FAILURE with ini file: " + config_file, msg) end end end