#!/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 two sections: [server1] and [server2].
# Each section takes the following parameters:
# P4PORT
# P4CLIENT
# P4USER
# COUNTER
# P4PASSWD (optional)
#
# 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/PerforceExchange.py#2 $
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
CONFIG='transfer.cfg'
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
def __repr__(self):
return 'rev = %s action = %s type = %s depotFile = %s' \
% (self.rev, self.action, self.type, 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
def __str__(self):
return '[section = %s P4PORT = %s P4CLIENT = %s P4USER = %s P4PASSWD = %s COUNTER = %s]' \
% (
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', '...@%d,#head'
% (self.counter + 1))
changes.reverse()
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', '...@%s,%s' % (change, change))
change = self.p4.run_describe(change)[0]
files = []
for (n, rev) in enumerate(change['rev']):
# 'p4 where' tests if the file is mapped to this workspace
localFile = self.localmap.translate(change['depotFile'][n])
if 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'):
depotFile = self.p4.run_filelog('-m1', '%s#%s' % (chRev.depotFile, chRev.rev))[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
if chRev.action == 'move/add':
depotFile = self.p4.run_filelog('-m1', '%s#%s'
% (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:
print ('warning in ', where, ' : ', 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:
print( 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('-t', f.type, f.localFile)
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('-t', f.type, f.localFile)
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)
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)
# 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
return result[a]['submittedChange']
else:
return None
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(file.localFile)
else:
# something fishy going on. Just add the file
self.p4.run_add('-t', file.type, file.localFile)
else:
self.p4.run_integrate('-v', file.integration.localFile,
file.localFile)
if dirty:
self.p4.run_edit(file.localFile)
else:
self.p4.run_add('-t', file.type, file.localFile)
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:
print ('Cannot deal with ', 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 P4Exchange:
def __init__(self, *argv):
parser = argparse.ArgumentParser(
description="PerforceTransfer",
epilog="Copyright (C) 2011,2012 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('-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()
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)
self.server1 = P4Config('server1', self.myOptions)
self.server2 = P4Config('server2', self.myOptions)
self.readSection(self.server1)
self.readSection(self.server2)
print( 'server1 = %s' % self.server1 )
print( 'server2 = %s' % self.server2 )
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)
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"""
print( 'Configfile = %s' % self.myOptions.config )
self.readConfig()
self.server1.connect('server1 replicate')
self.server2.connect('server2 replicate')
print( 'server1 = %s' % self.server1.p4 )
print( 'server2 = %s' % self.server2.p4 )
# determine which version is newer
self.server1.counter = int(self.server1.COUNTER)
self.server2.counter = int(self.server2.COUNTER)
mv = self.server1.verifyCounter()
lv = self.server2.verifyCounter()
source = None
target = None
if mv and not lv:
print( 'Replicate from server1 to server2.' )
source = self.server1
target = self.server2
elif lv and not mv:
print( 'Replicate from server2 to server1.' )
source = self.server2
target = self.server1
elif lv and mv:
print( 'Both sides out of sync. Giving up.' )
sys.exit(4)
else:
print( 'Nothing to do.' )
sys.exit(0)
if not source.root == target.root:
print( 'server1 and server2 workspace root directories must be the same' )
sys.exit(5)
source.resetWorkspace()
for change in source.missingChanges():
print ('Processing : ', change['change'], change['desc'])
files = source.getChange(change['change'])
resultedChange = target.replicateChange(files, change,
source.p4.port)
if resultedChange:
self.setCounter(source.section, change['change'])
self.setCounter(target.section, resultedChange)
self.writeConfig()
source.disconnect()
target.disconnect()
if __name__ == '__main__':
prog = P4Exchange(*sys.argv[1:])
prog.replicate()
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #2 | 8369 | Sven Erik Knop | Renamed P4Transfer to P4Exchange | ||
| #1 | 8368 | Sven Erik Knop |
The old PerforceTransfer is now PerforceExchange. PerforceTransfer will be a one-directional migration tool from now on. |
||
| //guest/sven_erik_knop/P4Pythonlib/scripts/PerforceTransfer.py | |||||
| #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. |
||