#!/usr/bin/python # Copyright (C) 2006-2008, Christof Meerwald # http://cmeerw.org # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 dated June, 1991. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 # USA import md5, sqlite3, string, sys, time from P4 import P4 class Changeset: def __init__(self, be, p4c, p4rev, user, tstamp, descr): self._actions = 0 self._be = be self._p4c = p4c self._p4rev = p4rev self._user = user self._tstamp = tstamp self._descr = descr def new_revision(self): if self._actions == 0: self._be._new_revision(self._p4rev, self._tstamp, self._user, self._descr) self._actions += 1 def add_file(self, filename, data): self.new_revision() #print 'add file', filename self._be._add_file(filename) propdata = 'PROPS-END\n' self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-kind: file\n', 'Node-action: add\n', 'Text-content-length: %d\n' % (len(data),), 'Text-content-md5: %s\n' % (md5.md5(data).hexdigest(),), 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(data) + len(propdata),), propdata, data, '\n' ]) def change_file(self, filename, data): self.new_revision() #print 'change file', filename propdata = 'PROPS-END\n' self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-kind: file\n', 'Node-action: change\n', 'Text-content-length: %d\n' % (len(data),), 'Text-content-md5: %s\n' % (md5.md5(data).hexdigest(),), 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(data) + len(propdata),), propdata, data, '\n' ]) def branch_file(self, filename, p4file, data): # copy file self.new_revision() #print 'branch file', filename self._be._add_file(filename) propdata = 'PROPS-END\n' filelog = p4c.run_filelog('-m', '1', '%s@%d' % (p4file, self._p4rev))[0] srcp4name = filelog.depotFile srcsvnrev = 0 if len(filelog.revisions[0].integrations): srcp4rev = str(filelog.revisions[0].integrations[0].erev) diff = p4c.run_diff2('-t', srcp4name + srcp4rev, '%s@%d' % (p4file, self._p4rev))[0] if diff['status'] == 'identical': srcfilelog = p4c.run_filelog('-m', '1', srcp4name + srcp4rev)[0] srcp4change = string.atoi(srcfilelog['change'][0]) srcsvnrev = self._be.map_change2svn(srcp4change) if srcsvnrev == 0: self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-kind: file\n', 'Node-action: add\n', 'Text-content-length: %d\n' % (len(data),), 'Text-content-md5: %s\n' % (md5.md5(data).hexdigest(),), 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(data) + len(propdata),), propdata, data, '\n' ]) else: srcsvnname = self._be.map_file2svn(srcp4name) self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-kind: file\n', 'Node-action: add\n', 'Node-copyfrom-rev: %d\n' % (srcsvnrev,), 'Node-copyfrom-path: %s\n' % (srcsvnname,), 'Text-copy-source-md5: %s\n' % (md5.md5(data).hexdigest(),), 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(propdata),), propdata, '\n' ]) def integrate_file(self, filename, data): self.new_revision() #print 'integrate file', filename propdata = 'PROPS-END\n' self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-kind: file\n', 'Node-action: change\n', 'Text-content-length: %d\n' % (len(data),), 'Text-content-md5: %s\n' % (md5.md5(data).hexdigest(),), 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(data) + len(propdata),), propdata, data, '\n' ]) def delete_file(self, filename): self.new_revision() #print 'delete file', filename self._be._writelines([ 'Node-path: %s\n' % (filename,), 'Node-action: delete\n\n', ]) self._be._remove_file(filename) def commit(self): self._be._commit_revision(self._p4rev) class Backend: def __init__(self, pathconv): self._pathconv = pathconv self._dirs = {} self._db = sqlite3.Connection('py42svn.db', 60000) self._db.isolation_level = None self._db.cursor().execute('PRAGMA synchronous=NORMAL') self._svnrev = 1 self._p4rev = 0 self._svnrev = 1 cursor = self._db.cursor() cursor.execute('PRAGMA synchronous=off') for row in cursor.execute('SELECT MAX(p4change) FROM state'): if row[0] != None: self._p4rev = row[0] def _write(self, data): sys.stdout.write(data) def _writelines(self, data): sys.stdout.writelines(data) def _add_file(self, filename): name = filename.split('/') cursor = self._db.cursor() for i in range(2, len(name)): dirname = '/'.join(name[0:i]) try: entries = self._dirs[dirname] except KeyError: entries = 0 for row in cursor.execute('SELECT entries FROM directory WHERE name=? AND revision<=? ORDER BY revision DESC LIMIT 1', (dirname, self._svnrev)): entries = row[0] entries += 1 self._dirs[dirname] = entries if entries == 1: propdata = 'PROPS-END\n' self._writelines([ 'Node-path: %s\n' % (dirname,), 'Node-kind: dir\n', 'Node-action: add\n', 'Prop-content-length: %d\n' % (len(propdata),), 'Content-length: %d\n\n' % (len(propdata,)), propdata, '\n' ]) def _new_revision(self, p4rev, tstamp, user, descr): self._dirs = {} cursor = self._db.cursor() self._svnrev = 1 for row in cursor.execute('SELECT IFNULL(MAX(revision),0)+1 FROM state'): self._svnrev = row[0] t = time.gmtime(tstamp) date = '%04d-%02d-%02dT%02d:%02d:%02d.000000Z' % tuple(t[0:6]) propdata = [] propdata.append('K 7\nsvn:log\nV %d\n%s' % (len(descr), descr)) propdata.append('K 10\nsvn:author\nV %d\n%s' % (len(user), user)) propdata.append('K 8\nsvn:date\nV %d\n%s' % (len(date), date)) propdata = '\n'.join(propdata + ['PROPS-END\n']) self._writelines([ 'SVN-fs-dump-format-version: 3\n\n', 'Revision-number: %d\n' % (self._svnrev,), 'Prop-content-length: %d\n' % (len(propdata,)), 'Content-length: %d\n\n' % (len(propdata,)), propdata, '\n' ]) def _commit_revision(self, p4rev): cursor = self._db.cursor() cursor.execute('INSERT INTO state (revision, p4change) VALUES (?, ?)', (self._svnrev, p4rev)) for d, entries in self._dirs.items(): cursor.execute('INSERT INTO directory (name, entries, revision) VALUES (?, ?, ?)', (d, entries, self._svnrev)) self._p4rev = p4rev def _remove_file(self, filename): name = filename.split('/') cursor = self._db.cursor() for i in range(len(name) - 1, 1, -1): dirname = '/'.join(name[0:i]) try: entries = self._dirs[dirname] except KeyError: entries = 0 for row in cursor.execute('SELECT entries FROM directory WHERE name=? AND revision<=? ORDER BY revision DESC LIMIT 1', (dirname, self._svnrev)): entries = row[0] entries -= 1 self._dirs[dirname] = entries if entries == 0: self._writelines([ 'Node-path: %s\n' % (dirname,), 'Node-action: delete\n\n' ]) def changeset(self, p4c, revision, user, tstamp, descr): return Changeset(self, p4c, revision, user, tstamp, descr) def map_file2svn(self, filename): for p4path, svnpath in self._pathconv: if filename.startswith(p4path): return svnpath + filename[len(p4path):] return None def map_change2svn(self, p4change): cursor = self._db.cursor() svnrev = 0 for row in cursor.execute('SELECT revision FROM state WHERE p4change=?', (p4change,)): svnrev = row[0] return svnrev pathmaps = [] for arg in sys.argv[1:]: p4path, svnpath = arg.split('=', 1) if p4path[-1] != '/': p4path += '/' if svnpath[-1] != '/': svnpath += '/' pathmaps.append((p4path, svnpath)) be = Backend(pathmaps) p4c = P4() p4c.connect() changes = [] for p4path, svnpath in pathmaps: changes += p4c.run_changes('-s', 'submitted', '%s...@%d,#head' % (p4path, be._p4rev)) changes.sort(lambda x, y: cmp(string.atoi(x['change']), string.atoi(y['change']))) for change in changes: nr = string.atoi(change['change']) if nr <= be._p4rev: # not interested in old revisions continue descr = p4c.run_describe('-s', str(nr))[0] changeset = be.changeset(p4c, nr, descr['user'], string.atoi(descr['time']), descr['desc']) for action, p4file, filetype in zip(descr['action'], descr['depotFile'], descr['type']): svnfile = be.map_file2svn(p4file) if svnfile == None: continue if action != 'delete': fileinfo = p4c.run_print(p4file + '@' + str(nr)) filedata = ''.join(fileinfo[1:]) else: filedata = None if action == 'add': changeset.add_file(svnfile, filedata) elif action == 'edit': changeset.change_file(svnfile, filedata) elif action == 'delete': changeset.delete_file(svnfile) elif action == 'branch': changeset.branch_file(svnfile, p4file, filedata) elif action == 'integrate': changeset.integrate_file(svnfile, filedata) else: raise 'unknown action', action changeset.commit()