#!/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 <debuglevel> ] -c <changelist> # # = Author # # Tony Smith <tony@perforce.com> or <tony@smee.org> # 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 <level> ] -c <change>" ) 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
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#2 | 6437 | Tony Smith |
Update P4Ruby Library scripts to support Perforce P4Ruby 2007.3 rather than my old public depot P4Ruby. |
||
#1 | 4249 | Tony Smith |
Add a script to back out a change. Features: * Prepares a fresh pending changelist with the files needed to back out the change. * Attempts to resolve merges for backing out older edits using "p4 resolve -am" * Leaves conflict resolution (if any) to the user Shortcomings are numerous, but include: * Does not (yet) re-branch deleted branched files. * Does not handle filetype changes (yet). The odds of success are highest with new changes. For older changes things are much more complicated and you can expect to have some work to do. |