#!/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. |
||