diff cmdutil.py @ 242:06130689a2c8

Move push into svncommands.
author Dirkjan Ochtman <dirkjan@ochtman.nl>
date Wed, 08 Apr 2009 17:53:48 +0200
parents 4950b18cf949
children 2027f851d60c
line wrap: on
line diff
--- a/cmdutil.py
+++ b/cmdutil.py
@@ -1,4 +1,21 @@
 
+from mercurial import util as hgutil
+
+from svn import core
+
+import svnwrap
+import svnexternals
+
+
+class BaseException(Exception):
+    pass
+
+
+class NoFilesException(BaseException):
+    """Exception raised when you try and commit without files.
+    """
+
+
 def replay_convert_rev(hg_editor, svn, r):
     hg_editor.set_current_rev(r)
     svn.get_replay(r.revnum, hg_editor)
@@ -30,3 +47,196 @@ def replay_convert_rev(hg_editor, svn, r
         hg_editor.missing_plaintexts = set()
         hg_editor.ui.note('\n')
     hg_editor.commit_current_delta()
+
+
+def _isdir(svn, branchpath, svndir):
+    try:
+        svn.list_dir('%s/%s' % (branchpath, svndir))
+        return True
+    except core.SubversionException:
+        return False
+
+
+def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges):
+    """Compute directories to add or delete when moving from parentctx
+    to ctx, assuming only 'changedfiles' files changed, and 'extchanges'
+    external references changed (as returned by svnexternals.diff()).
+
+    Return (added, deleted) where 'added' is the list of all added
+    directories and 'deleted' the list of deleted directories.
+    Intermediate directories are included: if a/b/c is new and requires
+    the addition of a/b and a, those will be listed too. Intermediate
+    deleted directories are also listed, but item order of undefined
+    in either list.
+    """
+    def finddirs(path, includeself=False):
+        if includeself:
+            yield path
+        pos = path.rfind('/')
+        while pos != -1:
+            yield path[:pos]
+            pos = path.rfind('/', 0, pos)
+
+    def getctxdirs(ctx, keptdirs, extdirs):
+        dirs = {}
+        for f in ctx.manifest():
+            for d in finddirs(f):
+                if d in dirs:
+                    break
+                if d in keptdirs:
+                    dirs[d] = 1
+        for extdir in extdirs:
+            for d in finddirs(extdir, True):
+                dirs[d] = 1
+        return dirs
+
+    deleted, added = [], []
+    changeddirs = {}
+    for f in changedfiles:
+        if f in parentctx and f in ctx:
+            # Updated files cannot cause directories to be created
+            # or removed.
+            continue
+        for d in finddirs(f):
+            changeddirs[d] = 1
+    for e in extchanges:
+        if not e[1] or not e[2]:
+            for d in finddirs(e[0], True):
+                changeddirs[d] = 1
+    if not changeddirs:
+        return added, deleted
+    olddirs = getctxdirs(parentctx, changeddirs,
+                         [e[0] for e in extchanges if e[1]])
+    newdirs = getctxdirs(ctx, changeddirs,
+                         [e[0] for e in extchanges if e[2]])
+
+    for d in newdirs:
+        if d not in olddirs and not _isdir(svn, branchpath, d):
+            added.append(d)
+
+    for d in olddirs:
+        if d not in newdirs and _isdir(svn, branchpath, d):
+            deleted.append(d)
+
+    return added, deleted
+
+
+def _externals(ctx):
+    ext = svnexternals.externalsfile()
+    if '.hgsvnexternals' in ctx:
+        ext.read(ctx['.hgsvnexternals'].data())
+    return ext
+
+
+def commit_from_rev(ui, repo, rev_ctx, hg_editor, svn_url, base_revision
+                    username, password):
+    """Build and send a commit from Mercurial to Subversion.
+    """
+    file_data = {}
+    svn = svnwrap.SubversionRepo(svn_url, username, password)
+    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
+
+    extchanges = list(svnexternals.diff(_externals(parent),
+                                        _externals(rev_ctx)))
+    addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx,
+                                            rev_ctx.files(), extchanges)
+    deleteddirs = set(deleteddirs)
+
+    props = {}
+    copies = {}
+    for file in rev_ctx.files():
+        if file == '.hgsvnexternals':
+            continue
+        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] + [''])
+            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:
+            pos = file.rfind('/')
+            if pos >= 0:
+                if file[:pos] in deleteddirs:
+                    # This file will be removed when its directory is removed
+                    continue
+            action = 'delete'
+        file_data[file] = base_data, new_data, action
+
+    def svnpath(p):
+        return '%s/%s' % (branch_path, p)
+
+    changeddirs = []
+    for d, v1, v2 in extchanges:
+        props.setdefault(svnpath(d), {})['svn:externals'] = v2
+        if d not in deleteddirs and d not in addeddirs:
+            changeddirs.append(svnpath(d))
+
+    # Now we are done with files, we can prune deleted directories
+    # against themselves: ignore a/b if a/ is already removed
+    deleteddirs2 = list(deleteddirs)
+    deleteddirs2.sort(reverse=True)
+    for d in deleteddirs2:
+        pos = d.rfind('/')
+        if pos >= 0 and d[:pos] in deleteddirs:
+            deleteddirs.remove(d[:pos])
+
+    newcopies = {}
+    for source, dest in copies.iteritems():
+        newcopies[svnpath(source)] = (svnpath(dest), base_revision)
+
+    new_target_files = [svnpath(f) for f in file_data]
+    for tf, ntf in zip(file_data, 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 hgutil.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]
+
+    addeddirs = [svnpath(d) for d in addeddirs]
+    deleteddirs = [svnpath(d) for d in deleteddirs]
+    new_target_files += addeddirs + deleteddirs + changeddirs
+    if not new_target_files:
+        raise NoFilesException()
+    try:
+        svn.commit(new_target_files, rev_ctx.description(), file_data,
+                   base_revision, set(addeddirs), set(deleteddirs),
+                   props, newcopies)
+    except core.SubversionException, e:
+        if hasattr(e, 'apr_err') and (e.apr_err == core.SVN_ERR_FS_TXN_OUT_OF_DATE
+                                      or e.apr_err == core.SVN_ERR_FS_CONFLICT):
+            raise hgutil.Abort('Base text was out of date, maybe rebase?')
+        else:
+            raise