#!/usr/bin/env ruby require 'P4' class MirrorPending def initialize(p4, source, target) @p4 = p4 @source_ws = source @target_ws = target @p4.connect unless @p4.connected? @source_ws_root = @p4.fetch_client(@source_ws)._root @target_ws_root = @p4.fetch_client(@target_ws)._root end def read_have have = Array.new @p4.at_exception_level(P4::RAISE_ERRORS) do @p4.run_have.each do |h| have << h['depotFile'] + "#" + h['haveRev'] end end return have end class ResolveState attr_accessor :type attr_accessor :action attr_accessor :baseFile attr_accessor :baseRev attr_accessor :fromFile attr_accessor :startFromRev attr_accessor :endFromRev def to_s "\tResolveState:\n" \ "\t\ttype = #{@type}\n" \ "\t\taction = #{@action}\n" \ "\t\tbaseFile = #{@baseFile}\n" \ "\t\tbaseRev = #{@baseRev}\n" \ "\t\tfromFile = #{@fromFile}\n" \ "\t\tstartFromRev = #{@startFromRev}\n" \ "\t\tendFromRev = #{@endFromRev}\n" end end class Attribute attr_accessor :permanent def initialize(key, value) @key = key @value = value @permanent = false end def apply(p4, file) if @permanent p4.run_attribute('-p', '-n', @key, '-v', @value, file) else p4.run_attribute('-n', @key, '-v', @value, file) end end end class IntegrationRecord attr_reader :record attr_reader :resolves attr_reader :fromFile attr_reader :startFromRev attr_reader :endFromRev attr_reader :baseRev def initialize(r) @record = "#{r.fromFile}##{r.startFromRev},#{r.endFromRev}" @resolves = Array.new << r @fromFile = r.fromFile @startFromRev = r.startFromRev @endFromRev = r.endFromRev @baseRev = r.baseRev end def addResolve(r) @resolves << r end def == (another) self.record == another.record end def hash @record.hash end end class OpenFile def initialize(fstat, source_ws_root, target_ws_root) @info = Hash.new @resolve = Array.new @editResolve = Array.new @branched = nil @moved = nil @attributes = Hash.new @source_ws_root = source_ws_root @target_ws_root = target_ws_root attrprop = Array.new # store all entries but the resolve arrays in @info fstat.each do |k,v| if k.start_with?("openattr-") key = k.sub("openattr-","") @attributes[key] = Attribute.new(key, v) end if k.start_with?("openattrProp-") key = k.sub("openattrProp-", "") attrprop << key # P4Ruby uses an unsorted hash, so I cannot rely on the order end if not v.kind_of?(Array) if k == "clientFile" @info["localFile"] = v.sub(@source_ws_root, @target_ws_root) end @info[k] = v end end attrprop.each do |key| @attributes[key].permanent = true end # if we have resolveAction, we have all other entries as well if fstat.has_key?("resolveAction") fstat["resolveAction"].length.times do |i| r = ResolveState.new r.action = fstat["resolveAction"][i] r.type = fstat["resolveType"][i] r.baseFile = fstat["resolveBaseFile"][i] r.baseRev = fstat["resolveBaseRev"][i] r.fromFile = fstat["resolveFromFile"][i] r.startFromRev = fstat["resolveStartFromRev"][i] r.endFromRev = fstat["resolveEndFromRev"][i] if @info["depotFile"] == r.fromFile @editResolve << r elsif r.action == "moved from" # @moved = r elsif r.action == "branch from" # there can be only one @branched = r else @resolve << r end end end if File.exists?(@info["clientFile"]) @content = File.open(@info["clientFile"],"rb") { |f| f.read } end end def to_s "OpenFile:\n\t#{@info.inspect}\n#{@resolve}\n#{@editResolve}" end @@commands = { "add" => :add_file, "edit" => :edit_file, "delete" => :delete_file, "branch" => :branch_file, "move/delete" => :move_delete_file, "move/add" => :move_add_file, "integrate" => :integrate_file } @@response = { "unresolved" => "s", "copy from" => "at", "ignored" => "ay", "merge from" => "am", "edit from" => "ae", "resolved" => "hint" } def apply_resolve(p4, file, resolveRecords) p4.run_resolve(file) do |mergedata| mergeResponse = "s" # skip by default # will be invoked for every resolve step # need to find the correct entry in the resolve records info = mergedata.info fromFile = info[0]["fromFile"] startFromRev = info[0]["startFromRev"] # startFromRev in resolve records is "off by one" compared to fstat output if startFromRev == "none" startFromRev = "1" else startFromRev = (startFromRev.to_i + 1).to_s end endFromRev = info[0]["endFromRev"] resolveType = info[0]["resolveType"] found = resolveRecords.find do |r| # puts "@@> #{mergedata}" # puts "@@> #{r.fromFile} == #{fromFile}" # puts "@@> #{r.startFromRev} == #{startFromRev}" # puts "@@> #{r.endFromRev} == #{endFromRev}" # puts "@@> #{r.type} == #{resolveType}" r.fromFile == fromFile and r.startFromRev == startFromRev and r.endFromRev == endFromRev and r.type == resolveType end if mergedata.action_resolve? if found if @@response.include?( found.action ) response = @@response[ found.action ] if response == "hint" mergeResponse = mergedata.merge_hint else mergeResponse = response end else puts ">>>>>>>>>>>> Unknown action #{found.action} <<<<<<<<<<" # raise exception? end else # Previously skipped resolve. Nothing to do here. end elsif mergedata.content_resolve? if found if @@response.include?( found.action ) mergeResponse = @@response[ found.action ] else puts ">>>>>>>>>>>> Unknown action #{found.action} <<<<<<<<<<" # raise exception? end else # Previously skipped resolve. Nothing to do here. end else puts ">>>>>>>>> Neither content nor action ???? <<<<<<<<<<<<<<<" end mergeResponse # this is what the block returns to the resolve process end end def apply_integ_resolve(p4, file, resolve) # sort them into unique integrates unique = Array.new resolve.each do |r| intrec = IntegrationRecord.new( r ) # use Integration Record as a probe as well as the real record if record = unique.find { |i| i == intrec } record.addResolve(r) # there can be more than one resolve for an integrate else unique << intrec end end # run the integrates unique.each do |integRecord| p4.run_integrate("-f", integRecord.record, file) # -f includes '-i'. We know the range, so this is safe apply_resolve(p4, file, integRecord.resolves) end end # ======================================= def add_file(p4) file = @info["localFile"] if @branched s = "#{@branched.fromFile}##{@branched.startFromRev},#{@branched.endFromRev}" p4.run_integrate( s, file ) p4.run_add(file) p4.run_reopen('-t', @info["type"], file) # ignored if no file type change File.open(file,"wb") do |f| f.write(@content) end else File.open(file,"wb") do |f| f.write(@content) end p4.run_add('-t', @info["type"], file) end # any outstanding resolve records are integrations dealt with here apply_integ_resolve(p4, file, @resolve) # because we do not have the actual record of which type is chosen # in each resolve, we simply reset the type here p4.run_reopen('-t', @info["type"], file) # ignored if no file type change # again, we do not have the record when which attribute is set # but resetting is not a problem, so we do that here @attributes.each do |key, attribute| attribute.apply(p4, file) end end # ======================================= def apply_edit_resolve(p4, file, type, resolve) # sort them into unique syncs, we are reusing the IntegrationRecord object for it unique = Array.new resolve.each do |r| intrec = IntegrationRecord.new( r ) # use Integration Record as a probe as well as the real record if record = unique.find { |i| i == intrec } record.addResolve(r) # there can be more than one resolve for an integrate else unique << intrec end end unique.each do |resyncRecords| p4.run_sync("#{file}##{resyncRecords.baseRev}") p4.run_edit(file) # ignore the "file already open" p4.run_reopen("-t", type, file) # separate in case file type changed already p4.run_sync("#{file}##{resyncRecords.endFromRev}") apply_resolve(p4, file, resyncRecords.resolves) end end def edit_file(p4) file = @info["localFile"] if @resolve.length > 0 # any integrations or downgrades to edits? apply_integ_resolve(p4, file, @resolve) end if @editResolve.length > 0 # any edits of previous revisions? apply_edit_resolve(p4, file, @info["type"], @editResolve) else p4.run_edit(@info["depotFile"]) p4.run_reopen('-t', @info["type"],@info["depotFile"]) # separate in case of file type change end # set the real content of the file here if File.writable?(file) File.open(file,"wb") do |f| f.write(@content) end else raise "File #{file} (#{@info['depotFile']}) Not Writable => odd" end # because we do not have the actual record of which type is chosen # in each resolve, we simply reset the type here p4.run_reopen('-t', @info["type"], file) # ignored if no file type change @attributes.each do |key, attribute| attribute.apply(p4, file) end end def delete_file(p4) p4.run_delete(@info["depotFile"]) end def branch_file(p4) file = @info["localFile"] if @branched s = "#{@branched.fromFile}##{@branched.startFromRev},#{@branched.endFromRev}" p4.run_integrate( s, file ) else puts ">>>>>>>> Branched file without branched record? <<<<<<<<<<<" end # any outstanding resolve records are integrations dealt with here apply_integ_resolve(p4, file, @resolve) # because we do not have the actual record of which type is chosen # in each resolve, we simply reset the type here p4.run_reopen('-t', @info["type"], file) # ignored if no file type change # again, we do not have the record when which attribute is set # but resetting is not a problem, so we do that here @attributes.each do |key, attribute| attribute.apply(p4, file) end end def move_delete_file(p4) # we will ignore move/delete, since move/add has all the information we need end def move_add_file(p4) file = @info["localFile"] moveSource = @info["movedFile"] p4.run_edit(moveSource) p4.run_move(moveSource, file) File.open(file,"wb") do |f| f.write(@content) end # any outstanding resolve records are integrations dealt with here # TODO # Need to be more careful here. # Need to distinguish between # moved and then integrated and # integrated with implicit move # apply_integ_resolve(p4, file, @resolve) p4.run_reopen('-t', @info["type"], file) # ignored if no file type change @attributes.each do |key, attribute| attribute.apply(p4, file) end end def integrate_file(p4) file = @info["localFile"] apply_integ_resolve(p4, file, @resolve) # because we do not have the actual record of which type is chosen # in each resolve, we simply reset the type here p4.run_reopen('-t', @info["type"], file) # ignored if no file type change # again, we do not have the record when which attribute is set # but resetting is not a problem, so we do that here @attributes.each do |key, attribute| attribute.apply(p4, file) end end def apply(p4) send(@@commands[@info["action"]], p4) end end def read_open result = [] @p4.run_fstat("-Oar", "-Ro", "//#{@source_ws}/...").each do |fstat| result << OpenFile.new(fstat, @source_ws_root, @target_ws_root) end result end def read_entries # one big fstat to get all the data we need # might be useful as an OutputHandler to deal with the large amount of data @p4.client = @source_ws have = read_have() openFiles = read_open return have, openFiles end def restore_entries(have, open) # for now debug output, soon to be removed #have.each do |h| # puts h #end #open.each do |o| # puts o #end @p4.client = @target_ws # This is just for paranoia. Making sure the target ws is truly empty # ... and I do not want any exceptions unless it is truly serious @p4.at_exception_level(P4::RAISE_ERRORS) do @p4.run_revert('//...') # no more open files @p4.run_sync('#0') # clear out workspace @p4.run_sync(have) end open.each do |o| o.apply(@p4) end end def mirror # read and analyze all entries from source # restore data to target have, open = read_entries() restore_entries(have, open) end end # Both source and target file must have the same mapping, or this will fail # A proper paranoid test would check this first, but alas, we do not have time if __FILE__ == $0 p4 = P4.new() if ARGV.length < 2 abort("Usage MirrorPending ") end m = MirrorPending.new(p4, ARGV[0], ARGV[1]) m.mirror end