Mercurial > hgsubversion
view push_cmd.py @ 69:63ece4ea25c9
hg_delta_editor: register copies only if files are unchanged between source and dest
Handle copies of items from revision X into revision Y where X is not the
parent of Y. This cannot happen in Mercurial because copies always happen
between parents and children. A file copy is recorded if:
1- Source and destination revs are in the same branch.
2- The file is unchanged (content, type, removal) through all revisions between
destination and source, not including source and destination.
Directory copies are registered only if the previous rules apply on all copied
items.
[1] is there because file copies across branches are meaningless in Mercurial
world. We could have tried to remap the source rev to a similar one in the
correct branch, but anyway the intent is wrong.
[2] is more questionable but I think it's better this way for we live in a
non-perfect svn world. In theory, 99% of copies out there should come from the
direct parent. But the direct parent is a fuzzy notion when you can have a
working directory composed of different directory at different revisions. So we
assume that stuff copied from past revisions exactly matching the content of
the direct parent revision is really copied from the parent revision. The
alternative would be to discard the copy, which would always happen unless
people kept updating the working directory after every commit (see
tests).
author | Patrick Mezard <pmezard@gmail.com> |
---|---|
date | Wed, 05 Nov 2008 13:37:08 +0100 |
parents | b33940d54fe2 |
children | 49b7cbe4c8e3 |
line wrap: on
line source
from mercurial import util as merc_util from mercurial import hg from svn import core import util import hg_delta_editor import svnwrap import fetch_command import utility_commands @util.register_subcommand('push') @util.register_subcommand('dcommit') # for git expats def push_revisions_to_subversion(ui, repo, hg_repo_path, svn_url, **opts): """Push revisions starting at a specified head back to Subversion. """ oldencoding = merc_util._encoding merc_util._encoding = 'UTF-8' hge = hg_delta_editor.HgChangeReceiver(hg_repo_path, ui_=ui) svn_commit_hashes = dict(zip(hge.revmap.itervalues(), hge.revmap.iterkeys())) # Strategy: # 1. Find all outgoing commits from this head outgoing = utility_commands.outgoing_revisions(ui, repo, hge, svn_commit_hashes) if not (outgoing and len(outgoing)): ui.status('No revisions to push.') return 0 if len(repo.parents()) != 1: ui.status('Cowardly refusing to push branch merge') return 1 while outgoing: oldest = outgoing.pop(-1) old_ctx = repo[oldest] if len(old_ctx.parents()) != 1: ui.status('Found a branch merge, this needs discussion and ' 'implementation.') return 1 base_n = old_ctx.parents()[0].node() old_children = repo[base_n].children() # 2. Commit oldest revision that needs to be pushed base_revision = svn_commit_hashes[old_ctx.parents()[0].node()][0] commit_from_rev(ui, repo, old_ctx, hge, svn_url, base_revision) # 3. Fetch revisions from svn r = fetch_command.fetch_revisions(ui, svn_url, hg_repo_path) assert not r or r == 0 # 4. Find the new head of the target branch repo = hg.repository(ui, hge.path) base_c = repo[base_n] replacement = [c for c in base_c.children() if c not in old_children and c.branch() == old_ctx.branch()] assert len(replacement) == 1 replacement = replacement[0] # 5. Rebase all children of the currently-pushing rev to the new branch heads = repo.heads(old_ctx.node()) for needs_transplant in heads: hg.clean(repo, needs_transplant) utility_commands.rebase_commits(ui, repo, hg_repo_path, **opts) repo = hg.repository(ui, hge.path) if needs_transplant in outgoing: hg.clean(repo, repo['tip'].node()) hge = hg_delta_editor.HgChangeReceiver(hg_repo_path, ui_=ui) svn_commit_hashes = dict(zip(hge.revmap.itervalues(), hge.revmap.iterkeys())) outgoing = utility_commands.outgoing_revisions(ui, repo, hge, svn_commit_hashes) merc_util._encoding = oldencoding return 0 def _findmissing(dirname, svn, branch_path): """Find missing directories in svn. dirname *must* end in a / """ assert dirname[-1] == '/' missing = [] keep_checking = True # check and see if the dir exists svn-side. path = dirname while keep_checking: try: assert svn.list_dir('%s/%s' % (branch_path, path)) keep_checking = False except core.SubversionException, e: # dir must not exist missing.append(path[:-1]) path = '/'.join(path.split('/')[:-2] + ['']) return missing def commit_from_rev(ui, repo, rev_ctx, hg_editor, svn_url, base_revision): """Build and send a commit from Mercurial to Subversion. """ file_data = {} svn = svnwrap.SubversionRepo(svn_url, username=merc_util.getuser()) parent = rev_ctx.parents()[0] parent_branch = rev_ctx.parents()[0].branch() branch_path = 'trunk' if parent_branch and parent_branch != 'default': branch_path = 'branches/%s' % parent_branch added_dirs = [] props = {} for file in rev_ctx.files(): new_data = base_data = '' action = '' if file in rev_ctx: new_data = rev_ctx.filectx(file).data() if 'x' in rev_ctx.filectx(file).flags(): props.setdefault(file, {})['svn:executable'] = '*' if 'l' in rev_ctx.filectx(file).flags(): props.setdefault(file, {})['svn:special'] = '*' if file not in parent: action = 'add' dirname = '/'.join(file.split('/')[:-1] + ['']) # check for new directories if not list(parent.walk(util.PrefixMatch(dirname))): added_dirs += _findmissing(dirname, svn, branch_path) else: base_data = parent.filectx(file).data() if ('x' in parent.filectx(file).flags() and 'x' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:executable'] = None if ('l' in parent.filectx(file).flags() and 'l' not in rev_ctx.filectx(file).flags()): props.setdefault(file, {})['svn:special'] = None action = 'modify' else: base_data = parent.filectx(file).data() action = 'delete' file_data[file] = base_data, new_data, action # TODO check for directory deletes here new_target_files = ['%s/%s' % (branch_path, f) for f in rev_ctx.files()] for tf, ntf in zip(rev_ctx.files(), new_target_files): if tf in file_data: file_data[ntf] = file_data[tf] if tf in props: props[ntf] = props[tf] del props[tf] if merc_util.binary(file_data[ntf][1]): props.setdefault(ntf, {}).update(props.get(ntf, {})) props.setdefault(ntf, {})['svn:mime-type'] = 'application/octet-stream' del file_data[tf] added_dirs = ['%s/%s' % (branch_path, f) for f in added_dirs] added_dirs = set(added_dirs) new_target_files += added_dirs try: svn.commit(new_target_files, rev_ctx.description(), file_data, base_revision, set(added_dirs), props) except core.SubversionException, e: if hasattr(e, 'apr_err') and e.apr_err == 160028: raise merc_util.Abort('Base text was out of date, maybe rebase?') else: raise