#!/usr/bin/ruby #-- #------------------------------------------------------------------------------- #++ # # = Introduction # # A script to reverse a specified change. This script re-adds files that # have been deleted; deletes files that were added; and reverses the edits # made within a single specified changelist. # # = What this script tries to do # # This script runs the necessary Perforce commands to reverse the effects of # a change. Files that were added in the bad change will be deleted; files # deleted in the bad change will be re-added; and the edits in the bad # change will be backed out. # # The script places all files it opens in a new pending changelist created # explicitly for this purpose. # # If the change being backed out is old, the script will attempt to remerge # the subsequent changes with the revision prior to the bad change. It may # or may not succeed. If it doesn't, you will have some work to do. Note # that the older the change being backed out is, the higher the probability # of failure. # # = What this script does NOT try to do # # 1. The script does not submit anything to the depot. # # 2. The script does not attempt to resolve merge conflicts # # 3. The script does not currently rebranch branched files that we deleted in # the bad change. This may work in a future version. # # 4. The script does not try to handle filetype changes. # # 5. The script doesn't handle symlinks well (and is unlikely to) # # = Usage # # reversechange.rb [ -v ] -c # # = Author # # Tony Smith or # Copyright (c) 2004 Perforce Software Inc. # # = 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. # # = Version # # $Id: //guest/tony_smith/perforce/P4Rubylib/scripts/reversechange.rb#2 $ # #-- #------------------------------------------------------------------------------- #++ require "getoptlong" require "P4" # # A class to handle the reversal of a change. This is in a class to allow # others to derive from it and add/replace bits of it to suit. # class Reverser def initialize( p4, verbose=0 ) @p4 = p4 @conflicts = 0 @verbose = verbose @changelist = 0 end attr_reader :p4 @@CONF_MSG = "\n" + "Warning: Some conflicts remain. To complete this process you should:\n" + "\n" + " 1. Use 'p4 resolve' or your GUI merge tool to complete the merge\n" + " 2. Use 'p4 diff' to review the changes\n" + " 3. Use 'p4 submit -c %d' to submit your changelist\n" + "\n" @@OK_MSG = "\n" + "Success: All merges completed without conflicts. To complete this\n" + "process you should:\n" + "\n" + " 1. Use 'p4 diff' to review the changes\n" + " 2. Use 'p4 submit -c %d' to submit your changelist\n" + "\n" # # Main interface - back out the specified change # def reverse( change ) # Reset counter @conflicts = 0 p4.exception_level = 1 # Ignore file(s) up-to-date warning p4.run_sync p4.exception_level = 2 # Now we want all warnings spec = p4.run_describe( change ).shift @changelist = mkchangelist( change ) files = spec[ "depotFile" ] revs = spec[ "rev" ] actions = spec[ "action" ] types = spec[ "type" ] i = 0 while( files[ i ] ) df = files[ i ] ac = actions[ i ] ty = types[ i ] re = revs[ i ] if( ac == "add" || ac == "branch" ) reverse_add( df, re, ty ) elsif( ac == "delete" ) reverse_delete( df, re, ty ) elsif( ac == "edit" || ac == "integrate" ) reverse_edit( df, re, ty ) end i += 1 end if( @conflicts > 0 ) conflicts_warning() else success_message() end end ######### protected ######### # # Create a new pending changelist # def mkchangelist( oldchange ) spec = p4.fetch_change spec[ "Files" ] = Array.new spec[ "Jobs" ] = Array.new spec[ "Description" ] = "Change to back out change ##{oldchange}" out = p4.save_change( spec ).shift if( out =~ /Change (\d+) created./ ) return $1 end raise( RuntimeError,"Failed to create a pending changelist!\n\t#{out}" ) end # # Reverse the effects of an add/branch - i.e. delete the file # def reverse_add( df, re, ty ) message( 1, "\n[Reverse Add] #{df}##{re}..." ) message( 2, "\tOpening head revision for delete..." ) p4.run_delete( "-c", @changelist, df ) end # # Reverse the effects of a delete - i.e. add the file. Could be cleverer # about deletes that result from integrations. # def reverse_delete( df, re, ty ) message( 1, "\n[Reverse Delete] #{df}##{re}..." ) prev = re.to_i - 1 message( 2, "\tSyncing back to revision #{prev}..." ) p4.run_sync( df + "##{prev}" ) message( 2, "\tRe-adding file using revision ##{prev}..." ) p4.run_add( "-c", @changelist, "-t", ty, df ) end # # Reverse the effects of an edit operation. Since this change may # not be the most recent change to a file, we need to do a little # tap-dancing to get the reversal right. In a nutshell we: # # 1. Sync to the revision prior to the edit # 2. Open the file for edit at that revision # 3. Sync to the edited revision to schedule a resolve # 4. Use "p4 resolve -ay" to ignore the bad change # 5. Sync to the head revision # 6. Use "p4 resolve -am" to merge in subsequent changes if any # def reverse_edit( df, re, ty ) message( 1, "\n[Reverse Edit] #{df}##{re}..." ) prev = re.to_i - 1 fs = p4.run_fstat( df ).shift if( fs[ "headAction" ] == "delete" ) message( 1, "\tNOT reversing edit - deleted at head revision." ) return end message( 2, "\tSyncing back to revision ##{prev}..." ) p4.run_sync( df + "##{prev}" ) message( 2, "\tOpening revision ##{prev} for edit..." ) p4.run_edit( "-c", @changelist, df ) message( 2, "\tSyncing to revision ##{re}..." ) p4.run_sync( df + "##{re}" ) message( 2, "\tIgnoring revision ##{re} in merge..." ) p4.run_resolve( "-ay", df ) begin message( 2, "\tSyncing to head revision..." ) p4.run_sync( df ) message( 2,"\tAttempting to merge changes after revision ##{re}...") attempt_merge( df ) rescue P4Exception if( p4.warnings[ 0 ] !~ /file.s. up-to-date/ ) raise end end end # # Attempt an automatic merge and keep a count of the number of files # with conflicts # def attempt_merge( df ) out = p4.run_resolve( "-am", df )[-1] # Really only interested in the last line. if( out =~ /resolve skipped/ ) message( 2, "\tConflicts remain..." ) @conflicts += 1 else message( 2, "\tMerge succeeded..." ) end end # # Warn the user there are still pending resolves that need to be done # manually # def conflicts_warning $stderr.puts( @@CONF_MSG % @changelist ) end # # Write out the success message for the user on stdout. # def success_message puts( @@OK_MSG % @changelist ) end # # Method to report output to the user - may be overridden by subclasses # def message( level, string ) puts( string ) if @verbose >= level end end # # Display usage information and exit # def croakusage puts( "Usage: reversechange.rb [-v ] -c " ) exit( 0 ) end #-- #------------------------------------------------------------------------------- # START OF MAIN SCRIPT #------------------------------------------------------------------------------- #++ changelist = nil verbose = 0 opts = GetoptLong.new( [ "-c", GetoptLong::REQUIRED_ARGUMENT ], [ "-v", GetoptLong::REQUIRED_ARGUMENT ] ) opts.each do |opt,arg| if( opt == "-c" ) changelist = arg elsif( opt == "-v" ) verbose = arg.to_i end end # Providing a changelist is mandatory if( ! changelist ) croakusage end begin p4 = P4.new p4.connect p4.debug = 1 if( verbose >= 3 ) reverser = Reverser.new( p4, verbose ) reverser.reverse( changelist ) rescue P4Exception if( p4.errors.length > 0 ) $stderr.puts( "Perforce error(s) during script execution:" ) p4.errors.each { |e| $stderr.puts( e ) } elsif( p4.warnings.length > 0 ) $stderr.puts( "Perforce warning(s) during script execution:" ) p4.warnings.each { |w| $stderr.puts( w ) } end exit( 1 ) end