reversechange.rb #2

  • //
  • guest/
  • tony_smith/
  • perforce/
  • P4Rubylib/
  • scripts/
  • reversechange.rb
  • View
  • Commits
  • Open Download .zip Download (10 KB)
#!/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.