You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1034 lines
31 KiB
1034 lines
31 KiB
#!/usr/bin/env python |
|
|
|
import p4helpers |
|
import sys |
|
import os |
|
import shutil |
|
import pickle |
|
import zipfile |
|
import zlib |
|
import datetime |
|
import subprocess |
|
import getopt |
|
import glob |
|
import struct |
|
try: |
|
import wx |
|
except: |
|
pass |
|
|
|
# |
|
# Set program defaults. |
|
# |
|
g_ChangelistInfoFilename = '__CHANGELISTINFO__' |
|
|
|
RUNMODE_BACKUP = 0 |
|
RUNMODE_RESTORE = 1 |
|
RUNMODE_EMPTY = 2 |
|
RUNMODE_NONE = 3 |
|
|
|
g_RunMode = RUNMODE_NONE |
|
g_ZipFilename = '' |
|
g_bAutoRevert = False |
|
g_bBinsOnly = False |
|
g_Changelist = [] |
|
g_bShowUI = False |
|
g_bPickleAllChanges = False |
|
g_bVerbose = True |
|
g_IncludeGlobPatterns = [] |
|
g_ExcludeGlobPatterns = [] |
|
g_ChangelistComment = '' |
|
g_DefaultFilename = None |
|
|
|
g_bCreateDiff = False |
|
g_DiffDir = '' |
|
|
|
g_SettingsFilename = 'vcodepickle.cfg' |
|
g_Settings = {} |
|
|
|
g_UISaveDirKey = 'uisavedir' |
|
|
|
# These are used with -changetree if the user wants to restore the changelist to |
|
# a different tree than it originally came from. |
|
g_FromTree = None |
|
g_ToTree = None |
|
|
|
g_QueuedWarnings = [] |
|
|
|
|
|
# |
|
# Pickle data for various file versions supported by vcodepickle. |
|
# |
|
class CV2Data: |
|
changelistComment = None |
|
|
|
class CV3Data: |
|
relativeToDepotFileMap = None |
|
|
|
|
|
def ShowUsage(): |
|
print "vcodepickle.py [options] <--backup [options] or --restore or --diff or --empty> [--file filename] [--changelist <changelist> | --bins]" |
|
print "" |
|
print "-q/--quiet sshhhhhh." |
|
print "-b/--backup pickle local changes to file" |
|
print "-r/--restore apply pickled edits to client" |
|
print "--empty create a valid, but empty, pickle, used by buildbot" |
|
print "-d/--diff produce a diff of the changes in a vcodepickled zipfile" |
|
print "-f/--file filename pickle file - optional for backup, required for restore" |
|
print "-c/--changelist <#|all> in backup mode, pickle specified changelist(s)" |
|
print " in restore mode, reapply changes to the specified changelist" |
|
print " defaults to 'default'" |
|
print " --prompt if using --backup, this asks where to backup (and makes suggestions)" |
|
print " if using --restore, this brings up a file open dialog" |
|
print "" |
|
print "backup options:" |
|
print "" |
|
print " --bins pickle binaries from the auto-checkout changelist" |
|
print " --revert in backup mode, revert open files after pickling" |
|
print " -e/--exclude <globpat> a glob pattern of files to exclude from pickling" |
|
print " -i/--include <globpat> a glob pattern of files to include in the code pickle" |
|
print "" |
|
print "restore options:" |
|
print "" |
|
print " --changetree <src> <dst> allows you to restore to a Perforce tree different from the original one" |
|
print "" |
|
print "diff options:" |
|
print "" |
|
print " --diffdir <directory> specify where the diff files will go" |
|
print "" |
|
print 'ex: vcodepickle.py --backup --changelist all --exclude "bin/*"' |
|
print ' creates a zip file of all files not matching "bin/*" in all changelists' |
|
print "" |
|
print "ex: vcodepickle.py --restore --file vcodepickle_2009_01_20.zip -c 342421" |
|
print " restore from the specified file into changelist 342421" |
|
print "" |
|
print "ex: vcodepickle.py --diff --file VCP_2009-03-26__17h-01m-25s.zip" |
|
print " creates a diff of the pickled changes in the specified file" |
|
sys.exit( 1 ) |
|
|
|
|
|
def DefaultPickleDirName( username ): |
|
return '\\\\fileserver\\user\\' + username + '\\vcodepickles' |
|
|
|
|
|
def DefaultPickleDiffName( username ): |
|
return '\\\\fileserver\\user\\' + username + '\\vcodepicklediff' |
|
|
|
def CreateChangelist( comment ): |
|
po = subprocess.Popen( 'p4 change -i', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE ) |
|
po.stdin.write( 'Change: new\n\nDescription:\n\t' + comment.replace('\n','\n\t') + '\n\n' ) |
|
po.stdin.close() |
|
theOutput = po.stdout.readline() |
|
ret = po.wait() |
|
if ret == 0: |
|
return int( theOutput.split( ' ' )[1] ) |
|
else: |
|
print "Creating a changelist failed." |
|
print theOutput |
|
sys.exit( 1 ) |
|
|
|
def GetChangelistComment( nChangelist ): |
|
po = subprocess.Popen( 'p4 change -o ' + nChangelist, shell=True, stdout=subprocess.PIPE ) |
|
lines = po.stdout.readlines() |
|
ret = po.wait() |
|
if ret != 0: |
|
print "Couldn't get changelist description for change " + nChangelist |
|
sys.exit( 1 ) |
|
|
|
comment = '' |
|
bFound = False |
|
for line in lines: |
|
if bFound == True: |
|
if line[0] == '\t': |
|
comment += line[1:] |
|
else: |
|
break |
|
else: |
|
if line[:12] == 'Description:': |
|
bFound = True |
|
|
|
return comment |
|
|
|
|
|
def chomp( str ): |
|
while 1: |
|
str2 = str.lstrip( '\n' ).lstrip( '\r' ).rstrip( '\n' ).rstrip( '\r' ) |
|
if len( str2 ) == len( str ): |
|
return str |
|
else: |
|
str = str2 |
|
|
|
|
|
def AddQueuedWarning( s ): |
|
global g_QueuedWarnings |
|
g_QueuedWarnings.append( s ) |
|
|
|
def PrintQueuedWarnings(): |
|
global g_QueuedWarnings |
|
if len( g_QueuedWarnings ) > 0: |
|
print "\n\n===== WARNINGS =====\n" |
|
nWarning = 1 |
|
for warning in g_QueuedWarnings: |
|
print "%d:\n%s\n" % (nWarning, warning) |
|
nWarning += 1 |
|
|
|
|
|
def DoesFileExistInPerforce( filename ): |
|
x = p4helpers.ReadPerforceOutput( 'p4 -G fstat ' + filename ) |
|
return (x[1][0]['code'] != 'error') |
|
|
|
# |
|
# The settings file is just a bunch of key/value pairs on different lines separated by = |
|
# |
|
def LoadSettingsFile( filename ): |
|
ret = {} |
|
try: |
|
f = open( filename, 'rt' ) |
|
except IOError: |
|
return {} |
|
|
|
for line in f.readlines(): |
|
line = chomp( line ) |
|
parts = line.split( '=' ) |
|
if len( parts ) > 0: |
|
ret[parts[0]] = parts[1] |
|
f.close() |
|
return ret; |
|
|
|
def SaveSettingsFile( filename, thedict ): |
|
f = open( filename, 'wt' ) |
|
for (k,v) in thedict.items(): |
|
f.write( '%s=%s\n' % (k, v) ) |
|
f.close() |
|
|
|
|
|
def SetZipFilename( filename ): |
|
global g_ZipFilename |
|
g_ZipFilename = filename |
|
if g_ZipFilename[-4:].lower() != '.zip': |
|
g_ZipFilename += '.zip' |
|
|
|
|
|
def GetReadableChangelistComment( str ): |
|
alphabet = 'abcdefghijklmnopqrstuvwxyz' |
|
|
|
# Strip out all lists of repeating nonalphabetic characters |
|
tempstr = '' |
|
prevchar = '' |
|
for i in range(0,len(str)): |
|
testchar = str[i:i+1] |
|
if testchar != prevchar or alphabet.find(testchar) != -1: |
|
tempstr += testchar |
|
prevchar = testchar |
|
|
|
str = tempstr |
|
str = str[:100] |
|
|
|
outstr = '' |
|
validchars = alphabet + alphabet.upper() + '0123456789_' |
|
prevchar = '' |
|
for i in range(0,len(str)): |
|
testchar = str[i:i+1] |
|
if validchars.find( testchar ) == -1: |
|
if prevchar != '-': |
|
outstr += '-' |
|
else: |
|
outstr += testchar |
|
prevchar = outstr[-1:] |
|
|
|
return outstr |
|
|
|
|
|
def ShowUIForFilename(): |
|
global g_Settings |
|
global g_SettingsFilename |
|
global g_ZipFilename |
|
global g_DefaultFilename |
|
|
|
# |
|
# Get the directory to save to. |
|
# |
|
username = os.environ['username'] |
|
defaultdirname = DefaultPickleDirName(username) |
|
|
|
if not g_Settings.has_key( g_UISaveDirKey ): |
|
g_Settings[g_UISaveDirKey] = defaultdirname |
|
|
|
if g_Settings[g_UISaveDirKey] == 'default': |
|
theinput = raw_input( '\nDirectory\n=========\n(Leave blank for "%s")\n> ' % ( defaultdirname ) ) |
|
else: |
|
theinput = raw_input( '\nDirectory\n=========\n(Leave blank for "%s")\n(Type "default" for "%s"\n--> ' % ( g_Settings[g_UISaveDirKey], defaultdirname ) ) |
|
|
|
if theinput == '': |
|
theDirName = g_Settings[g_UISaveDirKey] |
|
else: |
|
g_Settings[g_UISaveDirKey] = theinput |
|
theDirName = theinput |
|
|
|
if theDirName.lower() == 'default': |
|
theDirName = defaultdirname |
|
|
|
# |
|
# Make sure it exists.. create it if necessary. |
|
# |
|
if os.access( theDirName, os.F_OK ) != True: |
|
theinput = raw_input( "Directory %s doesn't exist. Create? (Y/n) " % theDirName ) |
|
if theinput == '' or theinput.lower() == 'y': |
|
os.makedirs( theDirName ) |
|
else: |
|
print "Exiting." |
|
sys.exit( 1 ) |
|
|
|
# |
|
# Get the filename to save to. |
|
# |
|
theinput = raw_input( '\nFilename\n========\n(Leave blank for "%s")\n> ' % ( g_DefaultFilename ) ) |
|
|
|
if theinput == '': |
|
theZipFilename = g_DefaultFilename |
|
else: |
|
theZipFilename = theinput |
|
|
|
# |
|
# Construct the actual zip filename we'll use. |
|
# |
|
SetZipFilename( os.path.join( theDirName, theZipFilename ) ) |
|
|
|
|
|
# |
|
# Check if it already exists. |
|
# |
|
if os.access( g_ZipFilename, os.F_OK ) == True: |
|
theinput = raw_input( "%s already exists. Overwrite? (y/N)" % g_ZipFilename ) |
|
if theinput == '' or theinput.lower() == 'n': |
|
print "Exiting." |
|
sys.exit(1) |
|
|
|
# |
|
# Save the settings back out. |
|
# |
|
SaveSettingsFile( g_SettingsFilename, g_Settings ) |
|
|
|
|
|
|
|
class VCodePickle: |
|
theZipFile = None |
|
filesDeleted = {} |
|
filesAdded = {} |
|
filesEdited = {} |
|
changelistComment = '' |
|
relativeToDepotFileMap = {} # Maps src\engine\cl_main.cpp to //valvegames/main/src/engine/cl_main.cpp (obviously //valvegames/main/src changes based on the root) |
|
|
|
def Load( self, zipFilename ): |
|
self.theZipFile = zipfile.ZipFile( zipFilename, 'r', zipfile.ZIP_DEFLATED ) |
|
|
|
# Load the added and deleted lists. |
|
changelistinfo = self.theZipFile.read( g_ChangelistInfoFilename + '.TXT' ) |
|
(self.filesDeleted, self.filesAdded, self.filesEdited) = pickle.loads( changelistinfo ) |
|
|
|
self.changelistComment = 'vcodepickle: ' + os.path.basename(g_ZipFilename) |
|
|
|
# Load version2 data. |
|
try: |
|
ci2 = pickle.loads( self.theZipFile.read( g_ChangelistInfoFilename + 'v2.TXT' ) ) |
|
self.changelistComment = ci2.changelistComment |
|
except KeyError: |
|
pass |
|
|
|
# Load version3 data |
|
try: |
|
ci3 = pickle.loads( self.theZipFile.read( g_ChangelistInfoFilename + 'v3.TXT' ) ) |
|
self.relativeToDepotFileMap = ci3.relativeToDepotFileMap |
|
except KeyError: |
|
pass |
|
|
|
|
|
|
|
## |
|
# Given a 'zip' instance, copy data from the 'name' to the |
|
# 'out' stream. |
|
|
|
def explode( out, zip, name ): |
|
zinfo = zip.getinfo(name) |
|
|
|
stringFileHeader = "PK\003\004" # magic number for file header |
|
structFileHeader = "<4s2B4HlLL2H" # 12 items, file header record, 30 bytes |
|
_FH_FILENAME_LENGTH = 10 |
|
_FH_EXTRA_FIELD_LENGTH = 11 |
|
|
|
filepos = zip.fp.tell() |
|
zip.fp.seek(zinfo.header_offset, 0) |
|
|
|
# Skip the file header: |
|
fheader = zip.fp.read(30) |
|
if fheader[0:4] != stringFileHeader: |
|
raise zipfile.BadZipfile, "Bad magic number for file header" |
|
|
|
fheader = struct.unpack(structFileHeader, fheader) |
|
fname = zip.fp.read(fheader[_FH_FILENAME_LENGTH]) |
|
if fheader[_FH_EXTRA_FIELD_LENGTH]: |
|
zip.fp.read(fheader[_FH_EXTRA_FIELD_LENGTH]) |
|
|
|
if fname != zinfo.orig_filename: |
|
raise zipfile.BadZipfile, 'File name in directory "%s" and header "%s" differ.' % ( zinfo.orig_filename, fname) |
|
|
|
if zinfo.compress_type == zipfile.ZIP_STORED: |
|
decoder = None |
|
elif zinfo.compress_type == zipfile.ZIP_DEFLATED: |
|
if not zlib: |
|
raise RuntimeError, "De-compression requires the (missing) zlib module" |
|
decoder = zlib.decompressobj(-zlib.MAX_WBITS) |
|
else: |
|
raise zipfile.BadZipfile,"Unsupported compression method %d for file %s" % (zinfo.compress_type, name) |
|
|
|
size = zinfo.compress_size |
|
|
|
while 1: |
|
data = zip.fp.read(min(size, 8192)) |
|
if not data: |
|
break |
|
size -= len(data) |
|
if decoder: |
|
data = decoder.decompress(data) |
|
out.write(data) |
|
|
|
if decoder: |
|
out.write(decoder.decompress('Z')) |
|
out.write(decoder.flush()) |
|
|
|
zip.fp.seek(filepos, 0) |
|
|
|
|
|
|
|
def WriteFileFromZip( theZipFile, srcFilename, sDestFilename ): |
|
# thebytes = theZipFile.read( srcFilename ) |
|
|
|
try: |
|
f = open( sDestFilename, 'wb') |
|
except: |
|
os.makedirs( os.path.dirname( sDestFilename ) ) |
|
f = open( sDestFilename, 'wb' ) |
|
|
|
explode( f, theZipFile, srcFilename ); |
|
# f.write( thebytes ) |
|
f.close() |
|
|
|
|
|
def PerforceToZipFilename( sP4Filename ): |
|
if sP4Filename[0] != '/' or sP4Filename[1] != '/': |
|
print "PerforceToZipFilename: invalid filename (%s)" % (sP4Filename) |
|
|
|
return sP4Filename[2:] |
|
|
|
|
|
def CheckFilesOpenedForEdit( thePickle, theDict ): |
|
ret = [] |
|
for k in theDict.keys(): |
|
sDepotFilename = thePickle.relativeToDepotFileMap[k] |
|
if p4helpers.GetClientFileAction( sDepotFilename ) != 'none': |
|
ret.append( sDepotFilename ) |
|
|
|
return ret |
|
|
|
|
|
|
|
def HandleNewTree( thePickle ): |
|
global g_FromTree, g_ToTree |
|
|
|
if g_FromTree == None: |
|
return |
|
|
|
print "-changetree is translating these files:\n" |
|
|
|
for k in thePickle.relativeToDepotFileMap.keys(): |
|
sFromFilename = thePickle.relativeToDepotFileMap[k] |
|
|
|
if sFromFilename[:len(g_FromTree)].lower() == g_FromTree.lower(): |
|
sToFilename = g_ToTree + sFromFilename[len(g_FromTree):] |
|
|
|
print sFromFilename + "\n\t-> " + sToFilename + "\n" |
|
|
|
thePickle.relativeToDepotFileMap[k] = sToFilename |
|
else: |
|
print "Used -changetree [%s] [%s], but file %s does not come from the source tree." % (g_FromTree, g_ToTree, sFilename) |
|
sys.exit( 1 ) |
|
|
|
# Translate the revision number to a changelist number for edited files. |
|
newEdited = {} |
|
for k in thePickle.filesEdited.keys(): |
|
thePickle.filesEdited[k] = "head" |
|
|
|
sDepotFilename = thePickle.relativeToDepotFileMap[k] |
|
if DoesFileExistInPerforce( sDepotFilename ): |
|
newEdited[k] = thePickle.filesEdited[k] |
|
else: |
|
AddQueuedWarning( "%s was edited in the old tree, but doesn't exist in the new tree. This was treated as an add instead." % sDepotFilename ) |
|
thePickle.filesAdded[k] = thePickle.filesEdited[k] |
|
|
|
thePickle.filesEdited = newEdited |
|
|
|
print "\n" |
|
|
|
|
|
def PromptForOpenZipFilename( sClientRoot, promptDir ): |
|
if not sys.modules.has_key( "wx" ): |
|
print >>sys.stderr, "Couldn't load wx, no UI available." |
|
sys.exit( 1 ) |
|
|
|
temp_app = wx.PySimpleApp() |
|
dialog = wx.FileDialog ( None, style = wx.OPEN ) |
|
dialog.SetMessage( "Select code pickle" ); |
|
dialog.SetDirectory(promptDir) |
|
dialog.SetWildcard( "VCodePickle Zips (*.zip)|*.zip|All Files (*.*)|*.*" ); |
|
|
|
if dialog.ShowModal() == wx.ID_OK: |
|
print 'Selected: %s\n' % (dialog.GetPath()) |
|
return dialog.GetPath() |
|
else: |
|
print 'Cancelled.' |
|
return None |
|
|
|
|
|
def DoRestore( sClientRoot, sZipFilename ): |
|
global g_bBinsOnly |
|
thePickle = VCodePickle() |
|
thePickle.Load( sZipFilename ) |
|
|
|
# First make sure they don't have any files open for edit that we want to mess with. |
|
print "Checking the code pickle...\n" |
|
|
|
openedForEdit = CheckFilesOpenedForEdit( thePickle, thePickle.filesAdded ) + CheckFilesOpenedForEdit( thePickle, thePickle.filesEdited ) |
|
if len( openedForEdit ) > 0 and g_bBinsOnly==False: |
|
print "You have one or more files open locally that are in the vcodepickle you are trying to restore:\n" |
|
for f in openedForEdit: |
|
print "\t" + f |
|
print "\nPlease revert the specified file(s) and retry the vcodepickle command you were doing." |
|
return |
|
|
|
changelistNum = None |
|
if ( g_bBinsOnly == False ): |
|
changelistNum = CreateChangelist( thePickle.changelistComment ) |
|
else: |
|
changelistNum = 'default' |
|
|
|
HandleNewTree( thePickle ) |
|
|
|
if g_bVerbose: print "Adding %d files..." % len( thePickle.filesAdded ) |
|
for k in thePickle.filesAdded.keys(): |
|
sDepotFilename = thePickle.relativeToDepotFileMap[k] |
|
|
|
if DoesFileExistInPerforce( sDepotFilename ): |
|
sLocalClientFilename = p4helpers.GetClientFileInfo( sDepotFilename )[0] |
|
AddQueuedWarning( "%s was added in the code pickle, but already exists in Perforce. We'll treat it as an edit." % sLocalClientFilename ); |
|
|
|
p4helpers.CheckPerforceReturn( 'p4 sync %s' % (sDepotFilename) ) |
|
p4helpers.CheckPerforceReturn( 'p4 edit -c %s %s' % (changelistNum, sDepotFilename) ) |
|
WriteFileFromZip( thePickle.theZipFile, k.replace('\\','/'), sLocalClientFilename ) |
|
else: |
|
# Note: We do the "p4 add" first because we don't yet know the local filename for this depot filename, |
|
# and there's no way to get it if the file doesn't already exist. |
|
# |
|
# Perforce will let you do a "p4 add" on a file that doesn't exist in your local tree yet, then you can |
|
# do a "p4 fstat" (p4helpers.GetClientFileInfo) on it to find its local filename. |
|
sCmd = 'p4 add -c %s %s' % (changelistNum, sDepotFilename) |
|
p4helpers.CheckPerforceReturn( sCmd ) |
|
|
|
# Get the local filename and copy from the zip file to that. |
|
sLocalClientFilename = p4helpers.GetClientFileInfo( sDepotFilename ) |
|
if sLocalClientFilename == None: |
|
AddQueuedWarning( "Unable to add %s. It probably isn't in your client spec." % sDepotFilename ) |
|
else: |
|
sLocalClientFilename = sLocalClientFilename[0] |
|
WriteFileFromZip( thePickle.theZipFile, k.replace('\\','/'), sLocalClientFilename ) |
|
|
|
if g_bVerbose: print "\t" + k |
|
|
|
if g_bVerbose: print "Deleting %d files..." % len( thePickle.filesDeleted ) |
|
for k in thePickle.filesDeleted.keys(): |
|
sDepotFilename = thePickle.relativeToDepotFileMap[k] |
|
p4helpers.CheckPerforceReturn( 'p4 delete -c %s %s ' % (changelistNum, sDepotFilename) ) |
|
if g_bVerbose: print "\t" + k |
|
|
|
if g_bVerbose: print "Editing %d files..." % len( thePickle.filesEdited ) |
|
for k in thePickle.filesEdited.keys(): |
|
revisionNumber = thePickle.filesEdited[k] |
|
sDepotFilename = thePickle.relativeToDepotFileMap[k] |
|
sLocalClientFilename = p4helpers.GetClientFileInfo( sDepotFilename )[0] |
|
|
|
# the zipfiles like forward slashes |
|
internalfilename = k.replace('\\','/') |
|
|
|
# Sync to the revision they had it at, edit, copy the file over, and sync to the latest so Perforce forces a resolve. |
|
p4helpers.CheckPerforceReturn( 'p4 sync %s#%s' % (sDepotFilename, revisionNumber) ) |
|
p4helpers.CheckPerforceReturn( 'p4 edit -c %s %s' % (changelistNum, sDepotFilename) ) |
|
WriteFileFromZip( thePickle.theZipFile, internalfilename, sLocalClientFilename ) |
|
p4helpers.CheckPerforceReturn( 'p4 sync %s' % sDepotFilename ) |
|
if g_bVerbose: print "\t%s#%s" % (k, revisionNumber) |
|
|
|
thePickle.theZipFile.close() |
|
|
|
PrintQueuedWarnings() |
|
|
|
if g_FromTree != None: |
|
print "\n" |
|
print " WARNING!!!!! -changetree edited the HEAD revision of the files in new tree." |
|
print " So if they have been edited since you checked them out for your" |
|
print " code pickle, then you must MANUALLY merge those changes into the new version." |
|
|
|
|
|
def SetupBinChangelists(): |
|
global g_Changelist |
|
clientInfo = p4helpers.GetClientInfo() |
|
pendingChanges = p4helpers.GetPendingChanges( clientInfo[ 'Client' ] ) |
|
for changeList in pendingChanges: |
|
if ( changeList[ 'desc' ].find( 'Visual Studio Auto Checkout' ) <> -1 ): |
|
g_Changelist.append( changeList[ 'change' ] ) |
|
|
|
def AutoFillChangelistArray(): |
|
global g_bBinsOnly |
|
global g_RunMode |
|
global g_bPickleAllChanges |
|
global g_Changelist |
|
|
|
if ( g_bBinsOnly and g_RunMode == RUNMODE_BACKUP ): |
|
SetupBinChangelists() |
|
|
|
elif g_bPickleAllChanges: |
|
clientInfo = p4helpers.GetClientInfo() |
|
pendingChanges = p4helpers.GetPendingChanges( clientInfo[ 'Client' ] ) |
|
for changeList in pendingChanges: |
|
g_Changelist.append( changeList['change'] ) |
|
|
|
g_Changelist.append( 'default' ) |
|
|
|
elif len( g_Changelist ) == 0 and g_RunMode == RUNMODE_BACKUP: |
|
g_Changelist.append( 'default' ) |
|
|
|
|
|
def CheckFileExcludedByPatterns( sFilename ): |
|
global g_IncludeGlobPatterns |
|
global g_ExcludeGlobPatterns |
|
|
|
bInclude = ( len( g_IncludeGlobPatterns ) == 0 ) # include by default |
|
for includePattern in g_IncludeGlobPatterns: |
|
if glob.fnmatch.fnmatch( sFilename, includePattern ): |
|
bInclude = True |
|
break |
|
if not bInclude: |
|
if g_bVerbose: print "skipping %s, doesn't match any include pattern" % sFilename |
|
return True |
|
|
|
for excludePattern in g_ExcludeGlobPatterns: |
|
if glob.fnmatch.fnmatch( sFilename, excludePattern ): |
|
if g_bVerbose: print "skipping %s matches exclude pattern %s" % ( sFilename, excludePattern ) |
|
return True |
|
|
|
return False |
|
|
|
|
|
|
|
def DoBackup( sClientRoot, sZipFilename, bAutoRevert ): |
|
global g_Changelist |
|
global g_ChangelistInfoFilename |
|
global g_ChangelistComment |
|
|
|
filesDeleted = {} |
|
filesAdded = {} |
|
filesEdited = {} |
|
|
|
# |
|
# Filter out all the files that aren't of the changelist we want. |
|
# |
|
openedFiles = p4helpers.ReadPerforceOutput( 'p4 -G opened' )[1] |
|
changelistfiles = [x for x in openedFiles if ( x['change'] in g_Changelist ) ] |
|
|
|
# Ok, there's something to do. Open the zip file. |
|
theZipFile = zipfile.ZipFile( sZipFilename, 'w', zipfile.ZIP_DEFLATED ) |
|
|
|
# |
|
# |
|
# |
|
relativeToDepotFileMap = {} |
|
|
|
for openedFile in changelistfiles: |
|
# Get the local client filename. |
|
depotFile = openedFile['depotFile'] # (//valvegames/main/src/etc....) |
|
x = p4helpers.GetClientFileInfo( depotFile ) |
|
|
|
localFilename = x[0] |
|
sFriendlyName = localFilename[ len(sClientRoot)+1: ] # ONLY for printing messages |
|
|
|
# Get the filename in the zipfile. We're just going to use the depot filename and make it palatable to the zipfile. |
|
# You could easily restore the depot filename from this filename, but for now we're just looking it up in relativeToDepotFileMap on the receiving end. |
|
sFilenameInZip = PerforceToZipFilename( depotFile ) |
|
|
|
# Check include and exclude patterns and skip this file if necessary. |
|
if CheckFileExcludedByPatterns( sFilenameInZip ): |
|
continue |
|
|
|
relativeToDepotFileMap[sFilenameInZip] = depotFile |
|
|
|
# Don't do anything if it's a delete. |
|
action = x[2] |
|
|
|
bCopy = True |
|
if action == 'delete': |
|
filesDeleted[sFilenameInZip] = 1 |
|
bCopy = False |
|
print "%s (DELETE)" % (sFriendlyName) |
|
else: |
|
if os.access( localFilename, os.F_OK ) == True: |
|
theZipFile.write( localFilename, sFilenameInZip ) |
|
else: |
|
print "\n**\n** WARNING: File '%s' is in the changelist but not on disk. It will not be in the zipfile.\n**\n" % sFriendlyName |
|
|
|
if action == 'add': |
|
filesAdded[sFilenameInZip] = 1 |
|
print "%s (ADD)" % (sFriendlyName) |
|
else: |
|
filesEdited[sFilenameInZip] = openedFile['rev'] |
|
print "%s#%s" % (sFriendlyName, openedFile['rev']) |
|
|
|
# Revert the file.. |
|
if bAutoRevert == True: |
|
p4helpers.CheckPerforceReturn( 'p4 revert ' + localFilename ) |
|
|
|
|
|
# Save the metadata about add/delete/edit and file revisions. |
|
data = pickle.dumps( [filesDeleted, filesAdded, filesEdited] ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + '.TXT', data ) |
|
|
|
# Save v2 data. |
|
ci2 = CV2Data() |
|
ci2.changelistComment = g_ChangelistComment |
|
data = pickle.dumps( ci2 ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + 'v2.TXT', data ) |
|
|
|
# Save v3 data. |
|
ci3 = CV3Data() |
|
ci3.relativeToDepotFileMap = relativeToDepotFileMap |
|
data = pickle.dumps( ci3 ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + 'v3.TXT', data ) |
|
|
|
|
|
# |
|
# Write a human-readable changelist file. |
|
# |
|
theZipFile.writestr( 'ReadableChangelist.txt', g_ChangelistComment ) |
|
|
|
theZipFile.close() |
|
|
|
|
|
def DoEmptyPickle( sZipFilename ): |
|
global g_ChangelistComment |
|
|
|
filesDeleted = {} |
|
filesAdded = {} |
|
filesEdited = {} |
|
|
|
relativeToDepotFileMap = {} |
|
|
|
# Ok, there's something to do. Open the zip file. |
|
theZipFile = zipfile.ZipFile( sZipFilename, 'w', zipfile.ZIP_DEFLATED ) |
|
|
|
# Save the metadata about add/delete/edit and file revisions. |
|
data = pickle.dumps( [filesDeleted, filesAdded, filesEdited] ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + '.TXT', data ) |
|
|
|
# Save v2 data. |
|
ci2 = CV2Data() |
|
ci2.changelistComment = g_ChangelistComment |
|
data = pickle.dumps( ci2 ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + 'v2.TXT', data ) |
|
|
|
# Save v3 data. |
|
ci3 = CV3Data() |
|
ci3.relativeToDepotFileMap = relativeToDepotFileMap |
|
data = pickle.dumps( ci3 ) |
|
theZipFile.writestr( g_ChangelistInfoFilename + 'v3.TXT', data ) |
|
|
|
|
|
# |
|
# Write a human-readable changelist file. |
|
# |
|
theZipFile.writestr( 'ReadableChangelist.txt', g_ChangelistComment ) |
|
|
|
theZipFile.close() |
|
|
|
|
|
|
|
def DoDiff( sZipFilename, sDiffDir ): |
|
# Load the zipfile. |
|
thePickle = VCodePickle() |
|
thePickle.Load( sZipFilename ) |
|
|
|
sTempFilesDir = os.path.join( sDiffDir, "tempfiles" ) |
|
|
|
# Get rid of the old directory. |
|
if os.access( sDiffDir, os.F_OK ) == True: |
|
theinput = raw_input( "Warning: This will clear out %s. Proceed (y/N)? " % sDiffDir ) |
|
if theinput == '' or theinput.lower() == 'n': |
|
print "Exiting." |
|
sys.exit( 0 ) |
|
|
|
# Delete all the files. |
|
# This method is lame but if we use unlink() on a network path, you won't be able to delete them |
|
# next time around or even from that command line. You can delete them from an explorer window, but |
|
# not from the command prompt anymore. Strange. |
|
os.system( 'rd /s /q "%s"' % sDiffDir ) |
|
|
|
# Create the directories. |
|
os.makedirs( sTempFilesDir ) |
|
|
|
# Dump out the changelist description. |
|
f = open( os.path.join( sDiffDir, "changelist.txt" ), 'wt' ) |
|
f.write( thePickle.changelistComment ) |
|
f.close() |
|
|
|
# Dump out all the files. |
|
for sFilename in thePickle.filesEdited.keys(): |
|
# The zipfile utils like forward slashes. |
|
sInternalFilename = sFilename.replace('\\','/') |
|
|
|
# Write the file to tempfiles\basefilename |
|
sEditedFilename = os.path.join( sTempFilesDir, os.path.basename( sInternalFilename ) ) |
|
WriteFileFromZip( thePickle.theZipFile, sInternalFilename, sEditedFilename ) |
|
|
|
# Have Perforce write out the prior version. |
|
nFileRevision = int( thePickle.filesEdited[sFilename] ) |
|
sPriorFilename = os.path.join( sTempFilesDir, os.path.basename( sInternalFilename ) ) + '.prev' |
|
p4helpers.ReadPerforceOutput( 'p4 -G print -o %s %s#%d' % (sPriorFilename, thePickle.relativeToDepotFileMap[sFilename], nFileRevision) ) |
|
|
|
# Write out a batch file with the diff command. |
|
sBatchFilename = os.path.join( sDiffDir, sInternalFilename.replace( '/', '-' ) ) + '.bat' |
|
fp = open( sBatchFilename, 'wt' ) |
|
sPercent = '%' |
|
fp.write( """ |
|
@echo off |
|
setlocal |
|
""" ) |
|
fp.write( "set sPriorFilename=%s\n" % sPriorFilename ) |
|
fp.write( "set sEditedFilename=%s\n" % sEditedFilename ) |
|
|
|
fp.write( """ |
|
rem // ------------------------------------------------------------------------------------------------------------------------ // |
|
rem // First, check if a P4DIFF environment variable exists. |
|
rem // ------------------------------------------------------------------------------------------------------------------------ // |
|
set diffProg=%P4DIFF% |
|
if "%diffprog%" == "" ( |
|
goto CheckRegistry |
|
) |
|
|
|
:DoDiff |
|
"%diffProg%" %sPriorFilename% %sEditedFilename% |
|
goto End |
|
|
|
rem // ------------------------------------------------------------------------------------------------------------------------ // |
|
rem // No P4DIFF? Ok, check for what P4Win has setup as its diff program. |
|
rem // The nasty for command there basically runs the reg command, does away with stderr output, pipes the output |
|
rem // to FINDSTR to strip out everything but the second line which contains the result we want. Then it takes the relevant |
|
rem // part of the end of that line, which is what we care about. |
|
rem // ------------------------------------------------------------------------------------------------------------------------ // |
|
:CheckRegistry |
|
for /F "tokens=2* delims=REG_SZ" %%a in ('reg query HKEY_CURRENT_USER\Software\Perforce\P4win\Options /v DiffApp 2^>nul ^| FINDSTR DiffApp') do set initialValue=%%a |
|
if "%initialValue%" == "" ( |
|
set diffProg=p4diff.exe |
|
) else ( |
|
for /F "tokens=*" %%s in ("%initialValue%") do set diffProg=%%s |
|
) |
|
goto DoDiff |
|
|
|
:End |
|
|
|
""" ) |
|
|
|
# Now the batch file references the diff program. Just add the arguments we care about. |
|
fp.close() |
|
|
|
print "Wrote %s" % sBatchFilename |
|
|
|
thePickle.theZipFile.close() |
|
|
|
# Open up that directory. |
|
os.system( 'start %s' % sDiffDir ) |
|
|
|
|
|
|
|
|
|
def main(): |
|
global g_bVerbose |
|
global g_RunMode |
|
global g_ZipFilename |
|
global g_bAutoRevert |
|
global g_bBinsOnly |
|
global g_bPickleAllChanges |
|
global g_Changelist |
|
global g_IncludeGlobPatterns |
|
global g_ExcludeGlobPatterns |
|
global g_ChangelistInfoFilename |
|
global g_ChangelistComment |
|
global g_DiffDir |
|
global g_bShowUI |
|
global g_DefaultFilename |
|
global g_bCreateDiff |
|
|
|
|
|
# |
|
# Parse out the command line. |
|
# |
|
try: |
|
opts, args = getopt.getopt( sys.argv[1:], "bdrf:c:ri:e:?q", |
|
[ "prompt", "diff", "diffdir=", "backup", "restore", "file=", "bins", "changelist=", "revert", "include=", "exclude=", "quiet", "help", "empty" ] ) |
|
except getopt.GetoptError, e: |
|
print "" |
|
print "Argument error: ", e |
|
print "" |
|
ShowUsage() |
|
sys.exit(1) |
|
|
|
bPrompt = False |
|
for o, a in opts: |
|
if o in ( "-?", "--help" ): |
|
ShowUsage() |
|
sys.exit(1) |
|
if o in ( "-q", "--quiet" ): |
|
g_bVerbose = False |
|
if o in ( "--prompt" ): |
|
bPrompt = True |
|
if o in ( "-b", "--backup" ): |
|
assert( g_RunMode == RUNMODE_NONE ) |
|
g_RunMode = RUNMODE_BACKUP |
|
if o in ( "-r", "--restore" ): |
|
assert( g_RunMode == RUNMODE_NONE ) |
|
g_RunMode = RUNMODE_RESTORE |
|
if o in ( "-e", "--empty" ): |
|
assert( g_RunMode == RUNMODE_NONE ) |
|
g_RunMode = RUNMODE_EMPTY |
|
if o in ( "-d", "--diff" ): |
|
g_bCreateDiff = True |
|
if o.lower() == "--diffdir" : |
|
g_DiffDir = a |
|
if o in ( "-f", "--file" ): |
|
g_ZipFilename = a |
|
if g_ZipFilename[-4:].lower() != '.zip': |
|
g_ZipFilename += '.zip' |
|
if o in ( "-c", "--changelist" ): |
|
if a in ( 'all', '*' ): |
|
g_bPickleAllChanges = True |
|
else: |
|
g_Changelist.append( a ) |
|
if o.lower() == "--bins": |
|
g_bBinsOnly = True |
|
if o in ( "--revert" ): |
|
g_bAutoRevert = True |
|
if o in ( "-i", "--include" ): |
|
g_IncludeGlobPatterns.append( a ) |
|
if o in ( "-e", "--exclude" ): |
|
g_ExcludeGlobPatterns.append( a ) |
|
|
|
if g_RunMode == RUNMODE_NONE and not g_bCreateDiff: |
|
ShowUsage() |
|
sys.exit( 1 ) |
|
|
|
if ( g_RunMode == RUNMODE_RESTORE or g_RunMode == RUNMODE_EMPTY ) and len( g_ZipFilename ) == 0 and not bPrompt: |
|
print >>sys.stderr, "\n*** file parameter is required in restore/empty mode ***\n\n\n" |
|
ShowUsage() |
|
sys.exit( 1 ) |
|
|
|
if len(g_Changelist) and g_bBinsOnly and g_RunMode == RUNMODE_BACKUP: |
|
print >>sys.stderr, "\n*** -c and --bins are mutally exclusive ***\n\n\n" |
|
ShowUsage() |
|
sys.exit( 1 ) |
|
|
|
if ( len( g_IncludeGlobPatterns ) or len( g_ExcludeGlobPatterns ) ) and g_RunMode != RUNMODE_BACKUP: |
|
print >>sys.stderr, "\n*** -i/-e apply only in backup mode ***\n\n\n" |
|
ShowUsage() |
|
sys.exit(1) |
|
|
|
g_Settings = LoadSettingsFile( g_SettingsFilename ) |
|
|
|
# Auto-fill g_Changelist if necessary. |
|
AutoFillChangelistArray() |
|
|
|
|
|
if g_RunMode == RUNMODE_BACKUP: |
|
# |
|
# Get the string for the changelist. |
|
# |
|
sDate = datetime.datetime.now().strftime("%Y-%m-%d__%Hh-%Mm-%Ss") |
|
if g_Changelist[0] == 'default': |
|
g_ChangelistComment = 'vcodepickle: ' + os.path.basename(g_ZipFilename) |
|
g_DefaultFilename = 'VCP_' + sDate + '.zip' |
|
else: |
|
g_ChangelistComment = GetChangelistComment( g_Changelist[0] ) |
|
partOfChangelistComment = GetReadableChangelistComment( g_ChangelistComment ) |
|
g_DefaultFilename = 'VCP_' + partOfChangelistComment + '_' + sDate + '.zip' |
|
|
|
# |
|
# Generate a default zip filename if they didn't specify one. |
|
# |
|
# Either we'll use a mixture of the current date and their changelist text |
|
# and if it was launched from a Perforce tool (used the -ui) parameter, |
|
# we'll let them specify where to save it (and default to \\fileserver\user\username\[the date and the changelist comment]) |
|
# |
|
if bPrompt == True: |
|
ShowUIForFilename() |
|
else: |
|
# |
|
# Non-ui mode. Just use what they entered on the command line or use the default if they didn't enter anything. |
|
# |
|
if len( g_ZipFilename ) == 0: |
|
SetZipFilename( g_DefaultFilename ) |
|
|
|
|
|
g_ClientInfo = p4helpers.GetClientInfo() |
|
|
|
# Get the root directory without a trailing slash. |
|
g_ClientRoot = g_ClientInfo['Root'] |
|
if g_ClientRoot[-1:] == '\\' or g_ClientRoot[-1:] == '/': |
|
g_ClientRoot = g_ClientRoot[:-1] |
|
|
|
if g_bVerbose: |
|
print "\n---- vcodepickle ----" |
|
print "zipfile : " + g_ZipFilename |
|
print "p4 client root : " + g_ClientRoot |
|
if g_bAutoRevert == True: |
|
print "-revert specified" |
|
print "-----------------------" |
|
print "" |
|
|
|
|
|
|
|
if g_RunMode == RUNMODE_BACKUP: |
|
DoBackup( g_ClientRoot, g_ZipFilename, g_bAutoRevert ) |
|
if g_RunMode == RUNMODE_EMPTY: |
|
DoEmptyPickle( g_ZipFilename ) |
|
|
|
elif g_RunMode == RUNMODE_RESTORE: |
|
if bPrompt: |
|
username = os.environ['username'] |
|
g_ZipFilename = PromptForOpenZipFilename( g_ClientRoot, DefaultPickleDirName(username) ) |
|
if g_ZipFilename != None: |
|
DoRestore( g_ClientRoot, g_ZipFilename ) |
|
else: |
|
DoRestore( g_ClientRoot, g_ZipFilename ) |
|
|
|
if g_bCreateDiff: |
|
print "\n\nPreparing diff...\n" |
|
username = os.environ['username'] |
|
if g_DiffDir == '': |
|
g_DiffDir = DefaultPickleDiffName(username) |
|
|
|
if bPrompt and ( g_RunMode != RUNMODE_BACKUP ): # If RUNMODE_BACKUP, then g_ZipFilename is already set to a valid thing. |
|
g_ZipFilename = PromptForOpenZipFilename( g_ClientRoot, DefaultPickleDirName(username) ) |
|
if g_ZipFilename != None: |
|
DoDiff( g_ZipFilename, g_DiffDir ) |
|
else: |
|
DoDiff( g_ZipFilename, g_DiffDir ) |
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
main() |
|
|
|
|
|
|