#!/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 # # = License # # Copyright (c) 1997-2008, Perforce Software, Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # = Author # # Tony Smith <tony@perforce.com> # # = Copyright # # (c) Perforce Software Inc, 2004-2008. 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. @mappings = Array.new # A list of computed mappings. 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 ) if( !@mappings.empty? ) puts <<-EOS The following renames were detected. You may wish to add them to the branch view for future integrations. EOS @mappings.each do |m| puts( m ) end puts( "" ) end end # # All the following methods are protected # protected # # Perform integration using the branchspec. # def branchspec_integ( branch, source ) p4.run_integ( "-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( 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 ) # branched file p4.run_revert( tgt ) # deleted file @mappings.push( "#{integ.from.path} #{tgt}" ) 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 ) @mappings.push( "#{i.from.path} #{tgt}" ) 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" || i.how == "add 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" || integ.how == "add 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 p4.at_exception_level( P4::RAISE_ALL ) do begin p4.run_integ("-n", "-b", branch, "-f", "-d", "-i", "-s", s).each do |out| if( md = FIND_RE.match( out ) ) return md[1] end end rescue P4Exception if( p4.errors.length > 0 || retry_count > 0 ) 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 end 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.exception_level = P4::RAISE_ERRORS p4.debug = 0 p4.api_level = 57 # Fix output level to what we expect 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. |