#!/usr/bin/ruby #-- #------------------------------------------------------------------------------- #++ # = Introduction # # integrename.rb is a script to perform Perforce integrations between # related branches and follow renames in both the source and target # branches as far as possible. # # For this purpose, a rename must be a branch and delete that occurred # IN THE SAME CHANGELIST. Anything else is handled according to the # regular Perforce defined process. # # = Usage # # integrename.rb -b # # = Version # # $Id: //guest/tony_smith/perforce/P4Rubylib/scripts/integrename.rb#1 $ # # = Limitations # # Currently, the branchview used for the integrations is not updated. This # means that the script must recompute the rename paths every time it's # used. A future version will output new mappings to be appended to the # branch view to avoid this recomputation. # # A future, future version will optionally add these mappings to the # branch view automatically # # = Author # # Tony Smith # # = Copyright # # (c) Perforce Software Inc, 2004. All rights reserved. # #-- #------------------------------------------------------------------------------- #++ require "getoptlong" require "P4" #-- # Regular expressions used for parsing the output of "p4 integrate" #++ FIND_RE =Regexp.new('^(.*)#\d+ - ') BRANCH_RE=Regexp.new('(.*)#(\d+) - branch.* from ([^#]+)#(\d+),?#?(\d+)?') DELTGT_RE=Regexp.new('(.*) - can\'t branch from ([^#]+)#(\d+),?#?(\d+)? without -d') OPENDEL_RE=Regexp.new('(.*) - can\'t integrate \(already opened for delete\)' ) # # A class to represent a path and a revRange. Very simple # class Revision def initialize( path, srev=nil, erev=nil ) @path = path @srev = srev @erev = erev if( srev && !erev ) @erev = srev end end attr_accessor :path, :srev, :erev end # # A class for storing the details of an attempted integration so that we can # re-use either the source or target parts in a corrective integration # class Integration def initialize( from, to ) @from = from @to = to end attr_reader :from, :to def fromstr sprintf( "%s#%d,%d", from.path, from.srev, from.erev ) end def tostr to.path end end # # An exception class to facilitate reverting of the deletes for files that # have been renamed. class OpenedForDelete < RuntimeError end # # The class that does all the work. # class Integrator def initialize( p4 ) @p4 = p4 @branches = Array.new # A list of files being branched @rm_targets = Array.new # A list of integs into deleted files. end attr_reader :p4, :branches, :rm_targets # # Main public method. # def integrate( branch, source ) branchspec_integ( branch, source ) handle_branched_files( branch ) handle_deleted_targets( branch ) end # # All the following methods are protected # protected # # Perform integration using the branchspec. # def branchspec_integ( branch, source ) p4.run_integ( "-I", "-b", branch, "-s", source ).each do |out| parse_integ_output( out ) end end # # Perform integration using a filespec that we've constructed directly. # Only used from within this class. We still need the branchspec because # we may need it to deduce target paths # def filespec_integ( branch, source, dest ) retried = false begin p4.run_integ( "-I", source, dest ).each do |out| parse_integ_output( out ) end rescue OpenedForDelete if( ! retried ) p4.run_revert( dest ) retried = true retry else raise end end handle_branched_files( branch ) handle_deleted_targets( branch ) end # # Parse the output from "p4 integrate" and queue the problems. # def parse_integ_output( line ) if ( md = BRANCH_RE.match( line ) ) to = Revision.new( md[1], md[2].to_i ) from = Revision.new( md[3], md[4].to_i, md[5] ? md[5].to_i : nil ) branches.push( Integration.new( from, to ) ) elsif( md = DELTGT_RE.match( line ) ) to = Revision.new( md[1] ) from = Revision.new( md[2], md[3] ) rm_targets.push( Integration.new( from, to ) ) elsif( md = OPENDEL_RE.match( line ) ) raise( OpenedForDelete, line ) end end # # Check the files being branched. Each file being branched is potentially # a rename in the source of the integration. We have to run a filelog # on the source file to find out where it originated from. Then we have to # run a fstat on the original file to find out if the subsequent revision # was a delete and if it occurred in the same change as the branch. If so, # then this is a rename and we have to redo the integration with the # correct target. # def handle_branched_files( branch ) branches.each do |integ| fl = p4.run_filelog( integ.from.path ).shift if( src = find_previous_name( integ, fl ) ) ti = Integrator.new( p4 ) tgt = find_target( branch, src ) p4.run_revert( integ.tostr ) ti.filespec_integ( branch, integ.fromstr, tgt ) end end end # # Check the deleted targets. Each deleted target is potentially a file # that has been renamed in the target branch. For these, we have to # find out its new name and patch up the target of the integration to # match # def handle_deleted_targets( branch ) rm_targets.each do |i| df = p4.run_filelog( "-m2", i.to.path ).shift tgt = find_new_name( df ) if( tgt ) ti = Integrator.new( p4 ) ti.filespec_integ( branch, i.fromstr, tgt ) else # The deleted target was not a rename. Warn the user warn( "Deleted target #{i.to.path} needs -d or -Dt flags" ) end end end # # Given the record of the integration attempt and the filelog on the # source file, figure out whether the source file was renamed and if so # return the previous name for the file # def find_previous_name( integ, filelog ) filelog.each_revision do |rev| rev.each_integration do |i| if i.how == "branch from" # The file was branched! Need to find where it was # branched from and if the source file was subsequently # deleted in the same change as this branch. df = p4.run_filelog( "-m2", i.file + "##{i.erev+1}" ).shift r = df.revisions.shift if r.action == "delete" && r.change == rev.change # We have a rename puts( "SRC: #{integ.from.path} used to be called #{i.file}" ) return i.file end end end end nil end # # Follows the trail - look in the history of the current file. If it's # deleted at the head rev then look at the previous rev to see if it was # branched anywhere. If so, then if the branch and delete occured in the # same change, we have a rename. # def find_new_name( df ) if( df.revisions[0].action == "delete" ) df.revisions[1].each_integration do |integ| if integ.how == "branch into" fs = p4.run_fstat(integ.file + "#" + integ.erev.to_s).shift if fs[ "headChange" ].to_i == df.revisions[0].change puts( "TGT: #{df.depot_file} now called #{integ.file}" ) return integ.file end end end end return nil end # # Finds the target file given a source file and a branch view. This # saves us having to reimplement Perforce views in this script. # def find_target( branch, src ) p4.run_integ( "-n", "-b", branch, "-f", "-d", "-i", "-s", src ).each do |out| if( md = FIND_RE.match( out ) ) return md[1] end end end # # Warn the user # def warn( msg ) $stderr.puts( msg ) end end def croak_usage puts( "\n" ) puts( "\tUsage: integrename.rb -b \n" ) puts( "\n" ) exit( 0 ) end #-- #------------------------------------------------------------------------------- # Start of main script #------------------------------------------------------------------------------- #++ branchspec = nil source = nil opts = GetoptLong.new( [ "-b", GetoptLong::REQUIRED_ARGUMENT ] ) opts.each do |opt,arg| if opt == "-b" branchspec = arg end end croak_usage if( ARGV.length != 1 ) croak_usage unless branchspec source = ARGV.shift begin p4 = P4.new p4.parse_forms p4.exception_level = 1 p4.debug = 0 p4.connect i = Integrator.new( p4 ) i.integrate( branchspec, source ) rescue P4Exception p4.errors.each { |e| $stderr.puts( e ) } raise end