integrename.rb #2

  • //
  • guest/
  • tony_smith/
  • perforce/
  • P4Rubylib/
  • scripts/
  • integrename.rb
  • View
  • Commits
  • Open Download .zip Download (8 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.
    #
    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 <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
#6 7564 Tony Smith Bug fix: when a rename is pure the script would still delete the
target file instead of leaving it alone. We really only need to handle
integrates where the content of the file changes - pure name changes
in the source are not an issue for us since the whole point of the
script is not to propagate them.

This change ensures that in the case of a file branched due to a
rename, the branch and the associated delete are both reverted.

Also took out the -I flag to integ, which is archaic now.
#5 6437 Tony Smith Update P4Ruby Library scripts to support Perforce P4Ruby 2007.3
rather than my old public depot P4Ruby.
#4 5837 Tony Smith Add support for branches downgraded to add in integrename.rb
(thanks to Martin Gamwell Davids). Also added some code to output
the detected mappings so they can be added to the branch view. The
script doesn't add them automatically since it's complicated to work
out which side of the view the source/dest paths should be and the
user may not want the entries added anyway. At least this way, they
can see what they are.
#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.