#!/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. # # This script transfers changes in one direction - from a source server to a target server. # There is a variation called PerforceExchange.py which transfers changes # in both direction. # # 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 # LOGFILE (optional) file to which to log # MAIL_FORM_URL (option) a URL which can be used to send emails via a simple POST # # 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/robert_cowham/perforce/utils/python/scripts/PerforceTransfer.py#10 $ from __future__ import print_function import sys from P4 import P4, P4Exception, Resolver, Map, OutputHandler 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 import time import logutils CONFIG = 'transfer.cfg' GENERAL_SECTION = 'general' SOURCE_SECTION = 'source' TARGET_SECTION = 'target' LOGGER_NAME = "transfer" def sizeof_fmt(num): for x in ['bytes','KB','MB','GB','TB']: if num < 1024.0: return "%3.1f %s" % (num, x) num /= 1024.0 class ChangeRevision: "Represents a change" 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 ReportProgress(object): "Report overall progress" def __init__(self, p4, changes, logger): self.logger = logger self.filesToSync = 0 self.changesToSync = len(changes) self.sizeToSync = 0 self.filesSynced = 0 self.changesSynced = 0 self.sizeSynced = 0 self.previousSizeSynced = 0 self.sync_progress_size_interval = None # Set to integer value to get reports for chg in changes: sizes = p4.run_sizes('-s', '@%s,%s' % (chg['change'], chg['change'])) self.sizeToSync += int(sizes[0]['fileSize']) self.filesToSync += int(sizes[0]['fileCount']) self.logger.info("Syncing %d changes, files %d, size %s" % (self.changesToSync, self.filesToSync, sizeof_fmt(self.sizeToSync))) def SetSyncProgressSizeInterval(self, interval): "Set appropriate" if interval: self.sync_progress_size_interval = int(interval) def ReportChangeSync(self): self.changesSynced += 1 def ReportFileSync(self, fileSize): self.filesSynced += 1 self.sizeSynced += fileSize if not self.sync_progress_size_interval: return if self.sizeSynced > self.previousSizeSynced + self.sync_progress_size_interval: self.previousSizeSynced = self.sizeSynced self.logger.info("Synced %d/%d changes, files %d/%d (%2.1f %%), size %s/%s (%2.1f %%)" % ( \ self.changesSynced, self.changesToSync, self.filesSynced, self.filesToSync, 100 * (self.filesSynced / self.filesToSync), sizeof_fmt(self.sizeSynced), sizeof_fmt(self.sizeToSync), 100 * (self.sizeSynced / self.sizeToSync))) class P4Config: "Reads and parses config file" section = None P4PORT = None P4CLIENT = None P4USER = None P4PASSWD = None COUNTER = None counter = 0 def __init__(self, section, options): self.section = section self.options = 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.options.maximum: changes = changes[:self.options.maximum] return changes def resetWorkspace(self): self.p4.run_sync('...#none') def getChange(self, change): """Expects change number as a string""" class SyncOutput(OutputHandler): "Log sync progress" def __init__(self, progress): OutputHandler.__init__(self) self.progress = progress def outputStat(self, stat): if 'fileSize' in stat: self.progress.ReportFileSync(int(stat['fileSize'])) return OutputHandler.HANDLED def outputInfo(self, info): return OutputHandler.HANDLED def outputMessage(self, msg): return OutputHandler.HANDLED self.progress.ReportChangeSync() callback = SyncOutput(self.progress) self.p4.run_sync('...@{},{}'.format(change, change), handler=callback) 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.options.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): # An integration where source has been obliterated will not have integrations has_integration = False try: i = file.integration has_integration = True except: pass if has_integration and not self.options.ignore 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 not self.options.ignore 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): "Local resolver to accept edits on merge" 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: "Main transfer class" 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('-r', '--repeat', action='store_true', help="Repeat transfer in a loop") parser.add_argument('-s', '--stoponerror', action='store_true', help="Repeat transfer in a loop") 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.options = parser.parse_args() self.options.sync_progress_size_interval = None self.logger = logging.getLogger(LOGGER_NAME) self.logger.setLevel(self.options.verbose) def readConfig(self): self.parser = ConfigParser() self.options.parser = self.parser # for later use try: self.parser.readfp(open(self.options.config)) except: print('Could not read %s' % self.options.config) sys.exit(2) self.options.mail_form_url = None self.options.sync_progress_size_interval = None 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.options.verbose) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S') fh.setFormatter(formatter) self.logger.addHandler(fh) self.options.mail_form_url = self.parser.get(GENERAL_SECTION, "mail_form_url", fallback=None) self.options.sync_progress_size_interval = self.parser.get(GENERAL_SECTION, "sync_progress_size_interval", fallback=None) else: print('No general section in config file, bailing out') sys.exit(3) self.source = P4Config(SOURCE_SECTION, self.options) self.target = P4Config(TARGET_SECTION, self.options) self.readSection(self.source) self.readSection(self.target) def writeConfig(self): with open(self.options.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) def replicate_changes(self, logger): "Perform a replication loop" self.source.connect('source replicate') self.target.connect('target replicate') changes = self.source.missingChanges() logger.output("Transferring %d changes" % len(changes)) self.source.progress = ReportProgress(self.source.p4, changes, logger) self.source.progress.SetSyncProgressSizeInterval(self.options.sync_progress_size_interval) for change in changes: msg = 'Processing : {} "{}"'.format(change['change'], change['desc'].strip()) self.logger.info(msg) logger.output(msg) 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() self.source.disconnect() self.target.disconnect() # # 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.options.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 ...") logger = logutils.Logger(self.options.mail_form_url) self.logger = logger self.source.logger = logger self.target.logger = logger timer = logutils.TimeRecorder(logger) timer.add("Starting transfer") logger.output(self.source.options) logger.output(self.target.options) self.source.disconnect() self.target.disconnect() finished = False while not finished: try: self.replicate_changes(logger) timer.report() logger.output("Changes transferred") if not self.options.repeat: finished = True else: time.sleep(1 * 60 * 60) except Exception as e: self.logger.error(e) logger.report_exception("") if self.options.stoponerror: logger.notify("Error", "Exception encountered and --stoponerror specified") return else: time.sleep(10 * 60) logger.notify("Changes transferred", "Completed successfully") if __name__ == '__main__': prog = P4Transfer(*sys.argv[1:]) prog.replicate()
# | Change | User | Description | Committed | |
---|---|---|---|---|---|
#35 | 9643 | Robert Cowham | Renamed to p4transfer - in the above directory | ||
#34 | 9640 | Robert Cowham | Whitespace - convert tabs to spaces! | ||
#33 | 9639 | Robert Cowham | Merged in Sven's changes to do with keyword handling. | ||
#32 | 9638 | Robert Cowham | Remove files from transfer client workspace after each successful transfer - keeps things tidy. | ||
#31 | 9637 | Robert Cowham | Improved formatting of email | ||
#30 | 9636 | Robert Cowham | Added new option summary_report_interval - to send regular emails when the transfer is set on repeat. | ||
#29 | 9631 | Robert Cowham |
Added error_report_interval in configuration file. Intended to set interval after which to report errors (usually less than report_interval). Useful when being run with --repeat option. |
||
#28 | 9025 | Robert Cowham | Refactor into method | ||
#27 | 9024 | Robert Cowham | Don't print stack trace on some exceptions. | ||
#26 | 8978 | Robert Cowham | Added --sample_config option and improved documentation of config file. | ||
#25 | 8977 | Robert Cowham | Reduce logging on zero changes to transfer | ||
#24 | 8970 | Robert Cowham |
Fix formatting output Log/notify instance_name instead of script path to distinguish between instances Read config file every time round loop to allow updates by user |
||
#23 | 8961 | Robert Cowham | Make sure counter_name is specified. | ||
#22 | 8960 | Robert Cowham |
Refactored to created P4Source and P4Target classes. Store change counter in a counter variable on the target rather than in config file. Tests alll pass |
||
#21 | 8958 | Robert Cowham | Note issues with Python3 | ||
#20 | 8957 | Robert Cowham |
Convert options to int Log sleeps! |
||
#19 | 8939 | Robert Cowham | Added configuration options - particularly for email notification via smtp | ||
#18 | 8934 | Robert Cowham | Log exception using new method | ||
#17 | 8925 | Robert Cowham | Migrating to standard logging utilities | ||
#16 | 8901 | Robert Cowham | Log pretty much all p4 commands! | ||
#15 | 8895 | Robert Cowham | Make argument parsing 2/3 compatible and add tests for it. | ||
#14 | 8894 | Robert Cowham | Python 2.7 fix for parse_args | ||
#13 | 8884 | Robert Cowham | Fix Python 2.7 problems | ||
#12 | 8883 | Robert Cowham | Removed the vairous python 3 unicode stuff. | ||
#11 | 8882 | Robert Cowham |
Make transfer class callable from test harness. Attempt to work with unicode in Python 3 - failing so will revert but stored for posterity! |
||
#10 | 8871 | Robert Cowham | Bump up logging | ||
#9 | 8870 | Robert Cowham | Handle case where we see an integration and the source file has been obliterated. | ||
#8 | 8869 | Robert Cowham |
Added better logging. Added -repeat flag for regular transfers Repeat in any case if we see an exception (e.g. conneciton lost) |
||
#7 | 8857 | Robert Cowham | Sucessfully got the notifycation via mail working with Python3 - helps to read the docs more carefully! | ||
#6 | 8854 | Robert Cowham |
Added sync interval logging. Renamed myOptions to options. |
||
#5 | 8853 | Robert Cowham | Make loggin a configurable parameter in the options file | ||
#4 | 8818 | Robert Cowham | Change reporting interval | ||
#3 | 8817 | Robert Cowham | Improve status reporting | ||
#2 | 8809 | Robert Cowham |
Clarify Transfer vs Exchange. Whitespace tidied as per pylint warnings. |
||
#1 | 8808 | Robert Cowham | Branching using cowhamr.sven.utils | ||
//guest/sven_erik_knop/P4Pythonlib/scripts/PerforceTransfer.py | |||||
#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. |