mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 09:17:30 +03:00
8162f33ac0
Mercurial store knows only file-level copies, directory copies are handle with heuristics. Implement the former one in svn backends.
174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
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 = {}
|
|
copies = {}
|
|
for file in rev_ctx.files():
|
|
new_data = base_data = ''
|
|
action = ''
|
|
if file in rev_ctx:
|
|
fctx = rev_ctx.filectx(file)
|
|
new_data = fctx.data()
|
|
|
|
if 'x' in fctx.flags():
|
|
props.setdefault(file, {})['svn:executable'] = '*'
|
|
if 'l' in fctx.flags():
|
|
props.setdefault(file, {})['svn:special'] = '*'
|
|
|
|
if file not in parent:
|
|
renamed = fctx.renamed()
|
|
if renamed:
|
|
# TODO current model (and perhaps svn model) does not support
|
|
# this kind of renames: a -> b, b -> c
|
|
copies[file] = renamed[0]
|
|
base_data = parent[renamed[0]].data()
|
|
|
|
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
|
|
def svnpath(p):
|
|
return '%s/%s' % (branch_path, p)
|
|
|
|
newcopies = {}
|
|
for source, dest in copies.iteritems():
|
|
newcopies[svnpath(source)] = (svnpath(dest), base_revision)
|
|
|
|
new_target_files = [svnpath(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, newcopies)
|
|
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
|