integrename.rb #1

  • //
  • guest/
  • robert_cowham/
  • perforce/
  • utils/
  • integrename.rb
  • View
  • Commits
  • Open Download .zip Download (9 KB)
#!/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 <branchspec> <source>
#
# = 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 <tony@perforce.com>
#
# = 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. There's
    # also an extra little trick here which is that if the initial integrate
    # fails because both the source and target have been deleted (a double
    # rename), we'll retry with an older revision of the source.
    #
    def find_target( branch, src )
	s = src
	retry_count = 0
	el = p4.exception_level?
	p4.exception_level = 2
	begin
	    
	    p4.run_integ("-n", "-b", branch, "-f", "-d", "-i", "-s", s).each do
		|out|
		if( md = FIND_RE.match( out ) )
		    p4.exception_level = el
		    return md[1]
		end
	    end
	rescue P4Exception
	    if( p4.errors.length > 0 || retry_count > 0 )
		p4.exception_level = el
		raise
	    end

	    # Warnings probably mean both source and target were deleted so
	    # let's fstat the source and try with the previous revision.
	    fs = p4.run_fstat( s ).shift
	    s = s + "#" + ( fs[ "headRev" ].to_i - 1 ).to_s
	    retry_count += 1
	    retry
	ensure
	    p4.exception_level = el
	end
	raise( RuntimeError, "Failed to identify target for #{src}" )
    end

    #
    # Warn the user
    #
    def warn( msg )
	$stderr.puts( msg )
    end
end



def croak_usage
    puts( "\n" )
    puts( "\tUsage: integrename.rb -b <branchspec> <src>\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
# Change User Description Committed
#1 4580 Robert Cowham My version...
//guest/tony_smith/perforce/P4Rubylib/scripts/integrename.rb
#3 4434 Tony Smith Bug fix.
integrename.rb can now handle the case where both the
source and target have been renamed.
#2 4248 Tony Smith Change filetype - no functional change
#1 4243 Tony Smith Add my script to follow renames when integrating using a branchview.

Lots of limitations, and I'm sure it doesn't catch everything but it
does try to handle renames in both source and target.