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