Reverting a perforce changelist - Aurora

Reverting a perforce changelist

| | Comments (11)

Indeed, when I design my killer language, the identifiers "foo" and "bar" will be reserved words, never used, and not even mentioned in the reference manual. Any program using one will simply dump core without comment. Multitudes will rejoice.
-- Tim Peters

Introduction

Sometimes you wind up with changes in your source control system that shouldn't be in there, or you hit the submit button in perforce when you really meant just update the description. Anyways, you find yourself with a changelist that you want to roll back. If you're fast enough and noone manages to check in stuff on top of your changes this can be quite easy. There are several nifty sync options to changelists, to files only in changelists and the previous revision in the currently selected changelist. Even so, there is a little bit of work involved when you do this, as you need to then need to open the files for open and then resolve them automagically to accept yours.

Tada! Python!

A long while ago, I think on one of the first projects I wrote with perforce I had this nifty script that could revert changelists for me. I've forgotten what ticked me off that particular time and inspired me to write it, but it turned out to be pretty easy. Of course that script is now long gone. I recently ran into the same problem and realized that I had neither script nor an easy way in perforce to pull this off. So I wrote a new incarnation of the script. This time bigger! Better! More bugs! Eh. Ok, maybe more bugs since it tries to be smarter. And we all know how that goes... the road to hell is paved with seemingly nifty and smart scripts. With the risk of fire and brimstone and revert mayhem, here is the script in all it's glory.

 
#!/usr/bin/env python
import os
import sys
import getopt
import marshal
import logging

P4_PORT_AND_USER = ' '

def p4( command ):
	"""
		Run a perforce command line instance and marshal the 
		result as a list of dictionaries.
	"""
	commandline = 'p4 %s -G %s' % (P4_PORT_AND_USER, command)
	logging.debug( '%s' % commandline )
	stream = os.popen( commandline, 'rb' )
	entries = []
	try:
		while 1:
			entry = marshal.load(stream)
			entries.append(entry)
	except EOFError:
		pass
	code = stream.close()
	if None != code:
		raise IOError( "Failed to execute %s: %d" % (commandline, int(code)) )
	return entries

def deleteFile(name):
	"""
		Deletes the file from the repository.
	"""
	p4( 'delete "%s"' % name )

def backRevision(name, revision, force):
	"""
		Given a file and a revision number, tries to go back 
		one revision in time.
	"""
	targetRevision = revision - 1
	headAction = p4( 'fstat "%s#%d"' % (name, targetRevision))[0]['headAction']

	if 'delete' == headAction:
		if laterRevisionExists(name, revision) and not force:
			logging.warn("%s has new edits since this change list was submitted. Skipping automated delete; You must delete manually." % (name))
		else:
			deleteFile(name)
	else:
		syncDoActionAndResolve(name, "edit", int(revision), force)

def laterRevisionExists(name, revision):
	headRevision = p4( 'fstat "%s"' % (name))[0]['headRev']
	return int(headRevision) > revision
	

def recoverFile(name, revision, force):
	"""
		Adds a deleted file back to the repository.
	"""
	syncDoActionAndResolve(name, "add", int(revision), force)

def syncDoActionAndResolve(name, action, revision, force):
	p4( 'sync "%s#%d"' % (name, revision -1) )
	p4( '%s "%s"' % (action, name) )
	p4( 'sync "%s"' % name )

	headRevision = int(p4( 'fstat "%s"' % (name))[0]['headRev'])
	logging.debug("revision = %d" % (revision))
	logging.debug("headRevision = %d" % (headRevision))

	if laterRevisionExists(name, revision) and not force:
		logging.warn("%s has new edits since this change list was submitted. Skipping automated resolve; You must resolve manually." % (name))
	else:
		p4( 'resolve -ay "%s"' % name )


def revertChangelist( changelistNumber, force ):
	"""
		Steps through the whole changelist file by file and looks at 
		the last action taken and then tries to go back one step.
	"""
	logging.debug( 'Trying to revert the changelist %d' % changelistNumber )
	description = p4( 'describe -s %d' % changelistNumber )[0]
	infos = []
	counter = 0
	try:
		while 1:
			name = description[ 'depotFile%d' % counter ]
			action = description[ 'action%d' % counter ]
			revision = int(description[ 'rev%d' % counter ])
			infos.append( (name, action, revision) )
			counter += 1
	except KeyError:
		pass
	
	for name, action, revision in infos:
		logging.debug( 'Processing %s#%d' % (name, revision) )
		if 'add' == action:
			deleteFile(name)
		if 'edit' == action:
			backRevision(name, revision, force)
		if 'delete' == action:
			recoverFile(name, revision, force)
		if 'branch' == action or 'integrate' == action:
			if 1 == revision:
				deleteFile(name)
			else:
				backRevision(name, revision, force)
	

def main(argv):
	"""
		Usage: p4revert.py [options] <changelist>

			Options:
				-v              : verbose
				-f              : force
				-c client       : perforce client
				-p port         : perforce port
				-u user         : perforce user
	"""
	try:
		options, arguments = getopt.getopt(argv, 'c:p:u:vf')
	except getopt.GetoptError:
		print 'Error parsing arguments'
		print main.__doc__
		return 1

	# Default tweakable values for the options.
	verbose = False
	force = False
	client = ''
	port = ''
	user = ''
	
	# Loop through all the options
	for o,a in options:
		if '-v' == o:
			verbose = True
		if '-c' == o:
			client = a
		if '-p' == o:
			port = a
		if '-u' == o:
			user = a
		if '-f' == o:
			force = True
	
	if len(arguments) != 1:
		print 'Must give one changelist number'
		print main.__doc__
		return 1
	
	try:
		changelistNumber = int(arguments[0])
	except ValueError:
		print 'Changelist number must be a number!'
		print main.__doc__
		return 1
	
	global P4_PORT_AND_USER
	if len(client):
		P4_PORT_AND_USER += ' -c %s ' % client
	if len(port):
		P4_PORT_AND_USER += ' -p %s ' % port
	if len(user):
		P4_PORT_AND_USER += ' -u %s ' % user
	
	# At this point we're all done with the options! Now to the real code.
	if verbose:
		logging.basicConfig( 
			level=logging.DEBUG, 
			format='%(asctime)s %(levelname)-7s: %(message)s' )
	else:
		logging.basicConfig( 
			level=logging.INFO, format='%(message)s' )

	revertChangelist( changelistNumber, force )
	logging.info( 'Revert of %d done.' % changelistNumber )
	results = p4 ( "resolve -n" )
	for result in results:
		code = result['code']
		if code == 'stat':
			logging.warning("%s must be resolved." % (result['fromFile']))
		elif code == 'error':
			logging.info("Change list reverted and files are ready for submit.")
	return 0
	
if __name__ == '__main__':
	# This is just the main stub trick that makes the script act like a regular
	# unix application. We return 1 for errors and 0 for success.
	sys.exit( main(sys.argv[1:]) )

		
Listing 1: p4revert.py

The script is really not intended to be used as is on the commandline, although there is nothing stopping you. The real power comes from the built in hooks perforce have to add context sensitive commands to p4win, our favourite application. If you go in and add the following to the Tools->Customize menu (after you click Add) you will then have a very handy tool in the context menu like this.

p4win tool setting
Fig 1: The tools configuration in p4win.
p4win context menu
Fig 2: The resulting context menu in the changelist view in p4win.

In closing.

Power to the people. Although with power there also comes responsibility. Computers just allows us to make our mistakes bigger, better and faster. In fact you can probably cause a lot of mayhem with this script if you don't think before you submit. So be careful. That said, it reduces this operation from maybe a couple of minutes for a large changelist to a couple of seconds depending on your perforce server's speed.

Update 9/11/07

I updated the script with changes kindly submitted by Matt Zimmer, with the permission of Intuit. Some refactoring and features like:

  • Changed default from overwrite to require manual resolve if later edits made to a file (you want to roll 28 back to 27 but there is a 29 in the depot).
  • Added force flag to force overwrite (27 would overwrite 29 in the above scenario).

Thanks Matt for those changes! Now we can undo changes in perforce with ease :).

Update 11/8/07

Small update to support filenames with spaces in them.

11 Comments

Artem Kulakov said:

Thanks, Jim.

For ages I was going to write a script to do this. Now I don't need to :)

Jim Tilander said:

Haha, glad to be of service. I hope there are no bad bugs in there, although I'm going to eat my own dogfood and use it myself next time I find myself needing to revert large things in a hurry. So we'll see.

Jim Tilander said:

Just trying out the new comment interface. Note that there has been an update to the page, with fixes to the script itself (p4revert.py). Thanks to Matt Zimmer for the fixes.

Chris Tohline said:

The link to p4revert.py is broken. No biggie, but the comment block and copyright isn't in the text block above.

Great script, thanks man.

Jim Tilander said:

Chris,

Thanks for the report. I've fixed the broken link. It was another effect of the big server move. The copyright notice was intentionally taken out of the listing on the webpage since it cluttered the page needlessly, I assumed that (if the link was working) people took the ready to go file instead of copy-past the code from the webpage (I've had problems with indenting etc when doing that in the past). Thanks for the report!

Graeme Kelly said:

Thanks for the script.

I have a problem, or maybe it's a feature request. I have integrated to a branch that I didn't want to integrate to, and would like to revert the change. For files deleted or added, this script works fine - but if the files have been edited/integrated then nothing happens.

Jim Tilander said:

Graeme,

That sounds suspiciously like a bug. IIRC when you integrated A.cpp into B.cpp and then run this script, it should just revert B.cpp one revision. Let me take a look and see what is happening...

Jim Tilander said:

Graeme,

Ok. I could have sworn that the action that got logged in the record was 'branch', but after installing a new version of perforce I saw that the action was now called 'integrate'... hm. Oh, well. It's fixed. Download the new version of the script and try it out! Sorry for the hassle.

Graeme Kelly said:

Yeah, it's working fine now. Thanks very much!

Mark Rubin said:

Hi, Jim,

This script looks fantastic, but when I try to use it, I get the following in the p4Win console:

TOOL: 20:50:31 C:\WINDOWS\system32\cmd.exe /c p4revert.py -c mrubin_integrate -p pf1.dt.corp.yahoo.com:2014 -u mrubin 85576

STATUS: 20:50:31 SyntaxError: invalid syntax (p4revert.py, line 126)

I just copied the source from your web page, so line 126 corresponds to
print 'Error parsing arguments'

I don't actually know Python, so it's a bit hard for me to figure out what's up, although I guess there's something wrong with parsing the arguments. But they look fine to me in the console output. Also, if I run from the command line as

p4revert.py -v -c mrubin_integrate -p pf1.dt.corp.yahoo.com:2014 -u mrubin 85576

I get the same error.

I must be doing something silly wrong, but I don't know what it is?

Thanks for any help, and thanks for building the tool!

-- Markg

Jim Tilander said:

Hi Mark,

Sorry about this, python is actually white space sensitive and what I suspect happened is that you have copied the actual text of the script here on the page and saved it down to a text file? That might wind up with all sorts of problems, for example, the line that you list 126, in the original script it should be 151 that has the "Error parsing Arguments" text.

Can you try to right click on the link beneath the script text that has the link to p4revert.py and select "Save As" in your browser? That should hopefully download the script in working order for you. What kind of browser do you use?

Let me know if it didn't work!

Cheers,
Jim

Leave a comment