#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c) 2011 Sven Erik Knop, Perforce Software Ltd # # 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. # # User contributed content on the Perforce Public Depot is not supported by Perforce, # although it may be supported by its author. This applies to all contributions # even those submitted by Perforce employees. # # PerforceTransfer.py # # This python script will provide the means to update a server # with the data of another server. This is useful for transferring # changes between independent servers when no remote depots are possible. # # # The script requires a config file, normally called transfer.cfg, # that provides the Perforce connection information for both servers. # The script also needs a directory in which # it can place the mapped files. This directory has to be the root # of both servers' workspaces (this will be verified). # # The config file has three sections: [general], [source] and [target]. # # [source] and [target] take the following parameters, respectively: # P4PORT # P4CLIENT # P4USER # COUNTER # P4PASSWD (optional) # # The general section currently has the following options # LOGGER (optional) logger config file # # The counter represents the last transferred change number and must # be initialized with a base change. # # usage: # # python PerforceTransfer.py [options] # # options: # -c configfile # --config configfile # specifies the configfile to use (default offline.cfg) # # -n # do not replicate, only show what would happen # # -i # --ignore replace integrations by adds and edits # # -v # --verbose # verbose mode # # (more too follow, undoubtedly) # # $Id: //guest/sven_erik_knop/P4Pythonlib/scripts/PerforceTransfer.py#17 $ from __future__ import print_function import sys from P4 import P4, P4Exception, Resolver, Map if sys.version_info[0] >= 3: from configparser import ConfigParser else: from ConfigParser import ConfigParser import argparse import os.path from datetime import datetime import logging CONFIG='transfer.cfg' GENERAL_SECTION = 'general' SOURCE_SECTION = 'source' TARGET_SECTION = 'target' LOGGER_NAME = "transfer" class ChangeRevision: def __init__( self, r, a, t, d ): self.rev = r self.action = a self.type = t self.depotFile = d self.localFile = None def setIntegrationInfo(self, integ): self.integration = integ def setLocalFile(self, localFile): self.localFile = localFile localFile = localFile.replace("%40","@") localFile = localFile.replace("%23","#") localFile = localFile.replace("%2A","*") localFile = localFile.replace("%25","%") self.fixedLocalFile = localFile def __repr__(self): return 'rev = {rev} action = {action} type = {type} depotFile = {depotfile}' .format( rev = self.rev, action = self.action, type = self.type, depotfile = self.depotFile, ) class P4Config: section = None P4PORT = None P4CLIENT = None P4USER = None P4PASSWD = None COUNTER = None counter = 0 def __init__(self, section, options): self.section = section self.myOptions = options self.logger = logging.getLogger(LOGGER_NAME) def __str__(self): return '[section = {} P4PORT = {} P4CLIENT = {} P4USER = {} P4PASSWD = {} COUNTER = {}]'.format( \ self.section, self.P4PORT, self.P4CLIENT, self.P4USER, self.P4PASSWD, self.COUNTER, ) def connect(self, progname): self.p4 = P4() self.p4.port = self.P4PORT self.p4.client = self.P4CLIENT self.p4.user = self.P4USER self.p4.prog = progname self.p4.exception_level = P4.RAISE_ERROR self.p4.connect() if not self.P4PASSWD == None: self.p4.password = self.P4PASSWD self.p4.run_login() clientspec = self.p4.fetch_client(self.p4.client) self.root = clientspec._root self.p4.cwd = self.root self.clientmap = Map(clientspec._view) ctr = Map('//"'+clientspec._client+'/..." "' + clientspec._root + '/..."') self.localmap = Map.join(self.clientmap, ctr) self.depotmap = self.localmap.reverse() def disconnect(self): self.p4.disconnect() def verifyCounter(self): change = self.p4.run_changes('-m1', '...') self.changeNumber = (int(change[0]['change']) if change else 0) return self.counter < self.changeNumber def missingChanges(self): changes = self.p4.run_changes('-l', '...@{rev},#head'.format(rev = self.counter + 1)) changes.reverse() if self.myOptions.maximum: changes = changes[:self.myOptions.maximum] return changes def resetWorkspace(self): self.p4.run_sync('...#none') def getChange(self, change): """Expects change number as a string""" self.p4.run_sync('-f', '...@{},{}'.format(change, change)) change = self.p4.run_describe(change)[0] files = [] for (n, rev) in enumerate(change['rev']): localFile = self.localmap.translate(change['depotFile'][n]) if len(localFile) > 0: chRev = ChangeRevision(rev, change['action'][n], change['type'][n], change['depotFile'][n]) files.append(chRev) chRev.setLocalFile(localFile) if chRev.action in ('branch', 'integrate', 'add', 'delete'): filelog = self.p4.run_filelog('-m1', '{}#{}'.format(chRev.depotFile, chRev.rev)) if filelog: depotFile = filelog[0] revision = depotFile.revisions[0] if len(revision.integrations) > 0: for integ in revision.integrations: if 'from' in integ.how or integ.how == "ignored": chRev.setIntegrationInfo(integ) integ.localFile = self.localmap.translate(integ.file) break else: self.logger.error("Failed to retrieve filelog for {}#{}".format(chRev.depotFile, chRev.rev)) if chRev.action == 'move/add': depotFile = self.p4.run_filelog('-m1', '{}#{}'.format(chRev.depotFile, chRev.rev))[0] revision = depotFile.revisions[0] integration = revision.integrations[0] chRev.setIntegrationInfo(integration) integration.localFile = self.localmap.translate(integration.file) return files def checkWarnings(self, where): if self.p4.warnings: self.logger.warning('warning in {} : {}'.format(where, str(self.p4.warnings))) def replicateChange(self, files, change, sourcePort ): """This is the heart of it all. Replicate all changes according to their description""" for f in files: self.logger.debug( f ) if not self.myOptions.preview: if f.action == 'edit': self.p4.run_sync('-k', f.localFile) self.p4.run_edit('-t', f.type, f.localFile) self.checkWarnings('edit') elif f.action == 'add': if 'integration' in f.__dict__: self.replicateBranch(f, True) # dirty branch else: self.p4.run_add('-ft', f.type, f.fixedLocalFile) self.checkWarnings('add') elif f.action == 'delete': if 'integration' in f.__dict__: self.replicateIntegration(f) self.checkWarnings('integrate (delete)') else: self.p4.run_delete('-v', f.localFile) self.checkWarnings('delete') elif f.action == 'purge': # special case. Type of file is +S, and source.sync removed the file # create a temporary file, it will be overwritten again later dummy = open(f.localFile, 'w') dummy.write('purged file') dummy.close() self.p4.run_sync('-k', f.localFile) self.p4.run_edit('-t', f.type, f.localFile) if self.p4.warnings: self.p4.run_add('-tf', f.type, f.fixedLocalFile) self.checkWarnings('purge -add') elif f.action == 'branch': self.replicateBranch(f, False) self.checkWarnings('branch') elif f.action == 'integrate': self.replicateIntegration(f) self.checkWarnings('integrate') elif f.action == 'move/add': self.move(f) newChangeId = None opened = self.p4.run_opened() if len(opened) > 0: description = change['desc'] \ + ''' Transferred from p4://%s@%s''' % (sourcePort, change['change']) result = self.p4.run_submit('-d', description) self.logger.debug(str(result)) # the submit information can be followed by resfreshFile lines # need to go backwards to find submittedChange a = -1 while 'submittedChange' not in result[a]: a -= 1 newChangeId = result[a]['submittedChange'] self.updateChange(change, newChangeId) self.reverifyRevisions(result) self.logger.info("source = {} : target = {}".format(change['change'],newChangeId)) return newChangeId def updateChange(self, change, newChangeId): # need to update the user and time stamp newChange = self.p4.fetch_change(newChangeId) newChange._user = change['user'] # date in change is in epoch time, we need it in canonical form newDate = datetime.utcfromtimestamp(int(change['time'])).strftime("%Y/%m/%d %H:%M:%S") newChange._date = newDate self.p4.save_change(newChange, '-f') def reverifyRevisions(self, result): revisionsToVerify = [ "{file}#{rev},{rev}".format(file=x['refreshFile'],rev=x['refreshRev']) for x in result if 'refreshFile' in x ] if revisionsToVerify: self.p4.run_verify('-qv', revisionsToVerify) def replicateBranch(self, file, dirty): if self.myOptions.ignore == False \ and file.integration.localFile: if file.integration.how == 'add from': # determine the filelog of the file in the target database # this is not so easy since filelog will return nothing for a deleted file # so we need to find the depotFile for the localFile first df = self.depotmap.translate(file.localFile) f = self.p4.run_filelog(df) if len(f) > 0 and len(f[0].revisions) >= 2: # in 2011.1 we can ignore into deleted files, so we need to make sure # we catch a real version i = 0 while f[0].revisions[i].action == 'delete': i += 1 rev = f[0].revisions[i] # this is the revision just before the delete self.p4.run_sync('-f', '%s#%d' % (rev.depotFile, rev.rev)) self.p4.run_add("-f", file.fixedLocalFile) else: # something fishy going on. Just add the file self.p4.run_add('-ft', file.type, file.fixedLocalFile) else: self.p4.run_integrate('-v', file.integration.localFile, file.localFile) if dirty: self.p4.run_edit(file.localFile) else: self.p4.run_add('-ft', file.type, file.fixedLocalFile) def replicateIntegration(self, file): if self.myOptions.ignore == False \ and file.integration.localFile: if file.integration.how == 'edit from': with open(file.localFile) as f: content = f.read() self.p4.run_sync('-f', file.localFile) # to avoid tamper checking self.p4.run_integrate(file.integration.localFile, file.localFile) class MyResolver(Resolver): def __init__(self, content): self.content = content def resolve(self, mergeData): with open(mergeData.result_path, 'w') as f: f.write(self.content) return 'ae' self.p4.run_resolve(resolver=MyResolver(content)) else: self.p4.run_sync('-f', file.localFile) # to avoid tamper checking self.p4.run_integrate(file.integration.localFile, file.localFile) if file.integration.how == 'copy from': self.p4.run_resolve('-at') elif file.integration.how == 'ignored': self.p4.run_resolve('-ay') elif file.integration.how in ('delete', 'delete from'): self.p4.run_resolve('-at') elif file.integration.how == 'merge from': # self.p4.run_edit(file.localFile) # to overcome tamper check self.p4.run_resolve('-am') else: self.logger.error ('Cannot deal with {}'.format( file.integration )) else: if file.integration.how in ('delete', 'delete from'): self.p4.run_delete('-v', file.localFile) else: self.p4.run_sync('-k', file.localFile) self.p4.run_edit(file.localFile) def move(self, file): source = file.integration.localFile self.p4.run_sync('-f', source) self.p4.run_edit(source) self.p4.run_move('-k', source, file.localFile) class P4Transfer: def __init__(self, *argv): parser = argparse.ArgumentParser( description="PerforceTransfer", epilog="Copyright (C) 2013 Sven Erik Knop, Perforce Software Ltd" ) parser.add_argument('-n', '--preview', action='store_true', help="Preview only, no transfer") parser.add_argument('-c', '--config', default=CONFIG, help="Default is " + CONFIG) parser.add_argument('-m', '--maximum', default=None, type=int, help="maximum number of changes to transfer") parser.add_argument('-p', '--preflight', action='store_true', help="Run a sanity check first to ensure target is empty") parser.add_argument('-v', '--verbose', nargs='?', const="INFO", default="WARNING", choices=('DEBUG', 'WARNING', 'INFO', 'ERROR', 'FATAL') , help="Various levels of debug output") parser.add_argument('-i', '--ignore', action='store_true') self.myOptions = parser.parse_args() self.logger = logging.getLogger(LOGGER_NAME) self.logger.setLevel(self.myOptions.verbose) def readConfig(self): self.parser = ConfigParser() self.myOptions.parser = self.parser # for later use try: self.parser.readfp(open(self.myOptions.config)) except: print( 'Could not read %s' % self.myOptions.config ) sys.exit(2) if self.parser.has_section(GENERAL_SECTION): if self.parser.has_option(GENERAL_SECTION, "LOGFILE"): logfile = self.parser.get(GENERAL_SECTION, "LOGFILE") fh = logging.FileHandler(logfile) fh.setLevel(self.myOptions.verbose) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') fh.setFormatter(formatter) self.logger.addHandler(fh) else: print( 'No general section in config file, bailing out') sys.exit(3) self.source = P4Config(SOURCE_SECTION, self.myOptions) self.target = P4Config(TARGET_SECTION, self.myOptions) self.readSection(self.source) self.readSection(self.target) def writeConfig(self): with open(self.myOptions.config, 'w') as f: self.parser.write(f) def readSection(self, p4config): if self.parser.has_section(p4config.section): self.readOptions(p4config) else: print( 'Config file needs section %s' % p4config.section ) sys.exit(3) def readOptions(self, p4config): self.readOption('P4CLIENT', p4config) self.readOption('P4USER', p4config) self.readOption('P4PORT', p4config) self.readOption('COUNTER', p4config, optional=True) self.readOption('P4PASSWD', p4config, optional=True) def readOption( self, option, p4config, optional=False ): if self.parser.has_option(p4config.section, option): p4config.__dict__[option] = self.parser.get(p4config.section, option) elif not optional: print( 'Required option %s not found in section %s' % (option, p4config.section) ) sys.exit(1) def setCounter(self, section, value): """Sets the counter to value. Value must be a string""" self.parser.set(section, 'COUNTER', value) # # This is the central method # It provides the replication process # Algorithm: # Read the config file # Connect to server1 and server # Determine if counter is there def replicate(self): """Central method that performs the replication between server1 and server2""" self.readConfig() self.source.connect('source replicate') self.target.connect('target replicate') if not self.source.root == self.target.root: print( 'server1 and server2 workspace root directories must be the same' ) sys.exit(5) self.source.counter = int(self.source.COUNTER) if not self.source.verifyCounter(): print("Nothing to do. Good bye") sys.exit(0) self.source.resetWorkspace() if self.myOptions.preflight: print("Running pre-flight check first ...") targetFiles = self.target.p4.run_fstat('-T clientFile', '...') sourceFiles = self.source.p4.run_fstat('-T clientFile', '...') for f in targetFiles: if f in sourceFiles: depotFile = self.target.p4.run_fstat(f['clientFile'])[0] print("Failed pre-flight check, file '{}' in source and target".format(depotFile['depotFile']), file=sys.stderr) sys.exit(1) print("Finished pre-flight check ...") try: for change in self.source.missingChanges(): self.logger.debug('Processing : {} "{}"'.format(change['change'], change['desc'])) files = self.source.getChange(change['change']) resultedChange = self.target.replicateChange(files, change, self.source.p4.port) if resultedChange: self.setCounter(self.source.section, change['change']) self.setCounter(self.target.section, resultedChange) self.writeConfig() except P4Exception as e: self.logger.error(e) self.source.disconnect() self.target.disconnect() if __name__ == '__main__': prog = P4Transfer(*sys.argv[1:]) prog.replicate()
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#17 | 8554 | Sven Erik Knop | Added debug output for failed filelog retrieval. | ||
#16 | 8463 | Sven Erik Knop |
Fixed further problem with files that have an illegal file name containing @,#,* or %. Now it is possible to re-edit the file again as well. Added test case to prove the point. |
||
#15 | 8461 | Sven Erik Knop |
Fixed adding files with illegal chars like '@'. Also added test case. |
||
#14 | 8432 | Sven Erik Knop | Added pre-flight checks (-p) to avoid overwriting existing files. | ||
#13 | 8430 | Sven Erik Knop |
Added maximum option for changes to limit the number of changes transferred in each run. Should be useful for testing. Mind that "p4 changes" starts at the latest changes, so if there are millions of changes to transfer it will still take a long time to load all of the changes into memory first. |
||
#12 | 8429 | Sven Erik Knop | Added logging | ||
#11 | 8428 | Sven Erik Knop |
Transferred changes are now adjusted: - the transfer user is replaced with the original user - the submit date is reset to the original date Any +k files are re-verified to assure that they have the correct checksum. Still missing: - Logging |
||
#10 | 8425 | Sven Erik Knop |
Make PerforceTransfer unidirectional from source to target. Adjusted test cases accordingly. Still missing: Update change user and timestamp to the source user and timestamp Reverify ktext files affected by the change update. Add proper logging |
||
#9 | 8232 | Sven Erik Knop | Better safe than sorry: quotes around the path of the localMap entries. | ||
#8 | 8231 | Sven Erik Knop |
Removed all traces of p4.run_where and replaced them with local map.translate. Hopefully this will improve the performance of PerforceTransfer. |
||
#7 | 8216 | Sven Erik Knop |
Added test cases for integration from outside transfer scope. Fixed bug for integrated deletes from the outside. |
||
#6 | 8215 | Sven Erik Knop |
Upgraded test to include merge w/ edit Fixed a bug in PerforceTransfer.py avoiding a tamper check error. |
||
#5 | 8212 | Sven Erik Knop |
Added integrate-delete test case Solved integrate-delete problem in PerforceTransfer |
||
#4 | 8211 | Sven Erik Knop |
Additional test cases for integrate Fixed a bug with "ignore", can now be replicated. |
||
#3 | 8210 | Sven Erik Knop |
Fixed a bug in PerforceTransfer where an add followed by an integ to another branch would break the add. Also added the beginning of a test framework to catch those kind of problems in the future. Currently the test framework only checks add, edit, delete and simple integrates. |
||
#2 | 8209 | Sven Erik Knop |
Change formatting to tabs Made Python3 compatible Fixed a small bug in integrate |
||
#1 | 7986 | Sven Erik Knop | Changed P4Transfer to PerforceTransfer to conform with naming convention. | ||
//guest/sven_erik_knop/P4Pythonlib/scripts/P4Transfer.py | |||||
#10 | 7973 | Sven Erik Knop |
Enable re-adding of files for 2010.2+ servers. The problem was that the server now adds integration records for re-added files, which made P4Transfer believe this was a dirty branch instead of an add. Now we check if the "how" is "add from", indicating a re-add. |
||
#9 | 7971 | Sven Erik Knop |
Updated P4Transfer to deal with merge w/ edit integrations. All types of integrations should now be supported. Also updated the documentation. |
||
#8 | 7966 | Sven Erik Knop |
Changed master and local to server1 and server2. Also added first draft of a documentation that should serve pretty much as the blog post I intend to write on this tool. |
||
#7 | 7965 | Sven Erik Knop | Updated the shebang to avoid hardcoding the Python version. | ||
#6 | 7964 | Sven Erik Knop | Changed type to kxtext by popular demand. | ||
#5 | 7963 | Sven Erik Knop | Fixed the tamper problem. | ||
#4 | 7962 | Sven Erik Knop |
Updated P4Transfer with the ability to deal with +k types and merged files from integration. The result of the latter is an 'edit from' to avoid a tamper check problem. This is a hack for now until I can find a better way around it, but the repercussions should be low. |
||
#3 | 7961 | Sven Erik Knop |
Enable preview (-n) again. Not sure how it got lost. |
||
#2 | 7960 | Sven Erik Knop | Updated Copyright date and changed to ktext. | ||
#1 | 7959 | Sven Erik Knop |
P4Transfer release 1.0. Documentation to follow. |