//private/robey/tools/bzr2p4/bzr2p4.py#1 - add change 140865 (text) #!/usr/bin/env python2.4 import logging import marshal import os import re import subprocess import sys import textwrap import time import StringIO if os.environ.get('BZRPATH', None) is not None: sys.path.append(os.environ['BZRPATH']) try: import bzrlib except ImportError: sys.stderr.write('\n') sys.stderr.write('Can\'t find bzrlib on the python path.\n') sys.stderr.write('Try putting it in either PYTHONPATH or BZRPATH.\n') sys.stderr.write('\n') sys.exit(1) import bzrlib.branch import bzrlib.diff import bzrlib.revision import bzrlib.textfile P4_EXE = 'p4' PATCH_EXE = 'patch' bzr_branch = '/Users/robey/danger/private/robey/imp' last_revid = None log = logging.getLogger('bzr2p4') def log_to(stream, level): h = logging.StreamHandler(stream) log.setLevel(level) h.setLevel(level) h.setFormatter(logging.Formatter('%(levelname)-.3s [%(asctime)s.%(msecs)03d] %(name)s: %(message)s', '%Y%m%d-%H:%M:%S')) log.addHandler(h) def patch(filename, diff): args = [ PATCH_EXE, '-s', '-t', filename ] try: log.debug('%r', args) process = subprocess.Popen(args, bufsize=-1, stdin=subprocess.PIPE, stdout=None, stderr=None, close_fds=True) process.stdin.write(diff) process.stdin.close() ret = process.wait() if ret != 0: raise Exception('patch returned %d' % (ret,)) except Exception, e: log.error('Exception executing (patch) %s: %s', ' '.join(args), e) def p4(*in_args, **kw): """ execute the string cmd as a perforce command, and return the output as an array of hashes. no interpretation of the results is done. any exceptions on popen are reraised. """ ret = [] if kw.get('raw', False): args = [ P4_EXE ] raw = True else: args = [ P4_EXE, '-G' ] raw = False args.extend(list(in_args)) if 'stdin' in kw: stdin_data = kw['stdin'] stdin = subprocess.PIPE else: stdin = None try: log.debug('%r', args) if stdin is not None: log.debug('stdin = %r', stdin_data) process = subprocess.Popen(args, bufsize=-1, stdin=stdin, stdout=subprocess.PIPE, stderr=None, close_fds=True) if stdin is not None: process.stdin.write(stdin_data) process.stdin.close() if raw: ret.append(process.stdout.read()) else: while True: try: ret.append(marshal.load(process.stdout)) except EOFError, e: break process.stdout.close() process.wait() except Exception, e: log.error('Exception executing (p4) %s: %s', ' '.join(args), e) return ret def p4_where(path): out = p4('where', path) return out[0]['data'].split(' ')[0] def make_p4_changelist(path, rev, comment=None): description = rev.message if not isinstance(description, list): description = description.split('\n') if (len(description) > 1) and (description[-1] == ''): description = description[:-1] if len(description) == 1: # robey likes to type really long -m commit lines; wrap them description = textwrap.wrap(description[0], 70) # add special notes: datetime = time.strftime('%d %b %Y %H:%M:%S', time.localtime(rev.timestamp)) description.append('') description.append('# bazaar revision import') description.append('# commit by: %s' % (rev.committer,)) description.append('# date/time: %s' % (datetime,)) description.append('# inventory: %s' % (rev.inventory_sha1,)) if comment is not None: description.append('# ' + comment) query = { 'Change': 'new', 'Description': '\n'.join(description) + '\n' } out = p4('change', '-i', stdin=marshal.dumps(query, 0)) m = re.match(r'Change (\d+) created', out[0]['data']) if m is None: log.debug('Bad response: %r', out) raise Exception('Can\'t parse changelist #.') return int(m.group(1)) def get_revision_path(branch, old_rev_id, new_rev_id): """ get the list of revision ids to traverse from one revision to another. """ base_ancestry = set(branch.repository.get_ancestry(old_rev_id)) return [r for r in branch.repository.get_ancestry(new_rev_id) if r not in base_ancestry] def tree_lines(tree, file_id): if not file_id in tree: return [] tree_file = bzrlib.textfile.text_file(tree.get_file(file_id)) return tree_file.readlines() def get_patch(file_id, old_tree, new_tree, old_path, new_path): """ @raise errors.BinaryFile: if it's not a text file """ old_lines = tree_lines(old_tree, file_id) new_lines = tree_lines(new_tree, file_id) buffer = StringIO.StringIO() bzrlib.diff.internal_diff(old_path, old_lines, new_path, new_lines, buffer) return buffer.getvalue() class DeltaWorker (object): def __init__(self, branch, rev_id): self.branch = branch self.rev_id = rev_id self.target = '.' self.get_delta() def get_delta(self): """ for a specific revision on a branch, get the Delta object describing the changes in that revision, and save copies of the prior and current revision tree. """ self.rev = self.branch.repository.get_revision(self.rev_id) self.rev_tree = self.branch.repository.revision_tree(self.rev_id) if self.rev.parent_ids: base_id = self.rev.parent_ids[-1] else: base_id = bzrlib.revision.NULL_REVISION self.base_tree = self.branch.repository.revision_tree(base_id) self.delta = self.rev_tree.changes_from(self.base_tree, want_unchanged=True, include_root=True) def set_target(self, path): self.target = path def process(self): # perforce can't handle renaming a file AND modifying it at the same # time. so if we have any of those, we need to break the commit into # two stages. self.two_stage = len([r for r in self.delta.renamed if r[4] == True]) > 0 if self.two_stage: self.process_renames_only() self.process_non_renames() else: self.process_all() def process_renames_only(self): self.changelist = make_p4_changelist(self.target, self.rev, comment='(phase 1/2)') for r in self.delta.renames: self.handle_rename(*r, **dict(stage=1)) def process_all(self): self.changelist = make_p4_changelist(self.target, self.rev) for a in self.delta.added: self.handle_add(*a) for r in self.delta.removed: self.handle_remove(*r) for r in self.delta.renamed: self.handle_rename(*r) for m in self.delta.modified: self.handle_modify(*m) # FIXME: commit def handle_add(self, path, id, kind): log.debug('add %r kind %r', path, kind) filename = os.path.join(self.target, path) if kind == 'directory': if not os.path.isdir(filename): os.mkdir(filename) return if kind != 'file': raise Exception('Don\'t know how to add objects of type %r' % (kind,)) f = open(filename, 'w') f.write(''.join(tree_lines(self.rev_tree, id))) f.close() if self.rev_tree.is_executable(id): os.chmod(filename, 0664) p4('add', '-c', str(self.changelist), filename) def handle_remove(self, path, id, kind): log.debug('remove %r kind %r', path, kind) filename = os.path.join(self.target, path) if kind == 'directory': try: os.path.rmdir(filename) except OSError, e: log.debug('error rmdir %r: %s', filename, e) return if kind != 'file': raise Exception('Don\'t know how to remove objects of type %r' % (kind,)) p4('delete', '-c', str(self.changelist), filename) def handle_rename(self, old_path, new_path, id, kind, text_modified, meta_modified, stage=0): log.debug('rename %r => %r, kind %r', old_path, new_path, kind) old_filename = os.path.join(self.target, old_path) new_filename = os.path.join(self.target, new_path) if kind == 'directory': # ignore return if kind != 'file': raise Exception('Don\'t know how to rename objects of type %r' % (kind,)) if (stage == 0) or (stage == 1): # move the file p4('integrate', '-c', str(self.changelist), old_filename, new_filename) p4('delete', '-c', str(self.changelist), old_filename) if (stage == 0) or (stage == 2): self.handle_modify(new_path, id, kind, text_modifed, meta_modified, old_path=old_path) def handle_modify(self, path, id, kind, text_modified, meta_modified, old_path=None): log.debug('modify %r kind %r', path, kind) filename = os.path.join(self.target, path) if kind != 'file': raise Exception('Don\'t know how to modify objects of type %r' % (kind,)) if old_path is None: old_path = path p4('edit', '-c', str(self.changelist), filename) patch(filename, get_patch(id, self.base_tree, self.rev_tree, old_path, path)) log_to(sys.stdout, logging.DEBUG) b = bzrlib.branch.Branch.open(bzr_branch) if last_revid is None: last_revid = bzrlib.revision.NULL_REVISION base_revision = bzrlib.revision.common_ancestor(b.last_revision(), last_revid, b.repository) if base_revision == bzrlib.revision.NULL_REVISION: base_revision = None path = get_revision_path(b, base_revision, b.last_revision()) print len(path) worker = DeltaWorker(b, path[1]) worker.set_target('/Users/robey/danger/private/robey/tools/bzr2p4/test') print worker.process()