Commit 6e8f1b65 authored by Qijiang Fan's avatar Qijiang Fan

Upstream version 1.2.1

97f2079e3778511944ffb6a662520580650a3993 1.0
09c016174e332eceda015d3f43d96d7e0228acf3 1.0.1
4359ddd73b009fbb356c52ea5d99cba25222ee7a 1.1
8e621dbb82d4363a85317638ad237e2817c56347 1.1.1
093ae2915b452539b44390ee4ea14987484e1eee 1.1.2
708234ad6c97fb52417e0b46a86c8373e25123a5 1.2
This diff is collapsed.
include Makefile *.rst
recursive-include tests *.py *.sh *.svndump *.txt
# Makefile for testing hgsubversion
.PHONY: all check check-demandimport check-subvertpy check-swig
@echo "Use the following commands to build and install hgsubversion:"
@echo "$$ cd $(PWD)"
@echo "$$ $(PYTHON) ./ install"
@exit 1
check: check-demandimport check-subvertpy check-swig
# verify that hgsubversion loads properly without bindings, but fails
# when actually used
hg --config extensions.hgsubversion=./hgsubversion \
version 2>&1 \
| egrep '(^abort:|failed to import extension)'
hg --config extensions.hgsubversion=./hgsubversion \
version --svn 2>&1 \
| egrep '(^abort:|failed to import extension)'
$(PYTHON) tests/ --all --bindings=subvertpy
$(PYTHON) tests/ --all --bindings=swig
.. -*-restructuredtext-*-
hgsubversion is an extension for Mercurial that allows using Mercurial
as a Subversion client.
At this point, hgsubversion is usable by users reasonably familiar with
Mercurial as a VCS. It's not recommended to dive into hgsubversion as an
introduction to Mercurial, since hgsubversion "bends the rules" a little
and violates some of the typical assumptions of early Mercurial users.
You need to have either have Subversion 1.5 (or later) installed along with
either Subvertpy 0.7.4 (or later) or the Subversion SWIG Python bindings. You
need Mercurial 1.3 or later.
.. _mercurial:
.. _mercurial-stable:
.. _crew:
.. _crew-stable:
If you are unfamiliar with installing Mercurial extensions, please see
the UsingExtensions_ page in the Mercurial wiki. Look at the example
for specifying an absolute path near the bottom of the page. You want
to give the path to the top level of your clone of this repository.
.. _UsingExtensions:
Before using hgsubversion, I *strongly* encourage you to run the
automated tests. Just use nose_ if you have it (or ``easy_install
nose`` if you want it), or use ``python tests/`` to run the
suite with the conventional test runner. Note that because I use nose,
there's a lot of stdout spew in the tests right now. The important
part is that all the tests pass.
.. _nose:
You can check that hgsubversion is installed and properly activated using the
following command::
$ hg version --svn
Mercurial Distributed SCM (version ...)
Copyright (C) 2005-2010 Matt Mackall <> and others
This is free software; see the source for copying conditions. There is NO
hgsubversion: ...
Subversion: ...
bindings: Subvertpy ...
If your bindings are listed as `SWIG`, please consider installing Subvertpy_.
.. _Subvertpy:
Further Reading
More information on how to use hgsubversion is available from within Mercurial
in the `subversion` help topic. To view it, use::
$ hg help subversion
The Restructured Text source for this topic is also available in the file
'''integration with Subversion repositories
hgsubversion is an extension for Mercurial that allows it to act as a Subversion
client, offering fast, incremental and bidirectional synchronisation.
At this point, hgsubversion is usable by users reasonably familiar with
Mercurial as a VCS. It's not recommended to dive into hgsubversion as an
introduction to Mercurial, since hgsubversion "bends the rules" a little
and violates some of the typical assumptions of early Mercurial users.
Before using hgsubversion, we *strongly* encourage running the
automated tests. See 'README' in the hgsubversion directory for
For more information and instructions, see :hg:`help subversion`.
import os
import sys
import traceback
from mercurial import commands
from mercurial import extensions
from mercurial import help
from mercurial import hg
from mercurial import util as hgutil
from mercurial import demandimport
from mercurial import templatekw
# force demandimport to load templatekw
except ImportError:
templatekw = None
from mercurial import revset
# force demandimport to load revset
except ImportError:
revset = None
from mercurial import subrepo
# require svnsubrepo and hg >= 1.7.1
except (ImportError, AttributeError), e:
subrepo = None
import svncommands
import util
import svnrepo
import wrappers
import svnexternals
svnopts = [
('', 'stupid', None,
'use slower, but more compatible, protocol for Subversion'),
# generic means it picks up all options from svnopts
# fixdoc means update the docstring
# TODO: fixdoc hoses l18n
wrapcmds = { # cmd: generic, target, fixdoc, ppopts, opts
'parents': (False, None, False, False, [
('', 'svn', None, 'show parent svn revision instead'),
'diff': (False, None, False, False, [
('', 'svn', None, 'show svn diffs against svn parent'),
'pull': (True, 'sources', True, True, []),
'push': (True, 'destinations', True, True, []),
'incoming': (False, 'sources', True, True, []),
'version': (False, None, False, False, [
('', 'svn', None, 'print hgsubversion information as well')]),
'clone': (False, 'sources', True, True, [
('T', 'tagpaths', '',
'list of paths to search for tags in Subversion repositories'),
('A', 'authors', '',
'file mapping Subversion usernames to Mercurial authors'),
('', 'filemap', '',
'file containing rules for remapping Subversion repository paths'),
('', 'layout', 'auto', ('import standard layout or single '
'directory? Can be standard, single, or auto.')),
('', 'branchmap', '', 'file containing rules for branch conversion'),
('', 'tagmap', '', 'file containing rules for renaming tags'),
('', 'startrev', '', ('convert Subversion revisions starting at the one '
'specified, either an integer revision or HEAD; '
'HEAD causes only the latest revision to be '
# only need the discovery variant of this code when we drop hg < 1.6
from mercurial import discovery
def findoutgoing(orig, *args, **opts):
capable = getattr(args[1], 'capable', lambda x: False)
if capable('subversion'):
return wrappers.outgoing(*args, **opts)
return orig(*args, **opts)
extensions.wrapfunction(discovery, 'findoutgoing', findoutgoing)
except ImportError:
def extsetup():
"""insert command wrappers for a bunch of commands"""
# add the ui argument to this function once we drop support for 1.3
docvals = {'extension': 'hgsubversion'}
for cmd, (generic, target, fixdoc, ppopts, opts) in wrapcmds.iteritems():
if fixdoc and wrappers.generic.__doc__:
docvals['command'] = cmd
docvals['Command'] = cmd.capitalize()
docvals['target'] = target
doc = wrappers.generic.__doc__.strip() % docvals
fn = getattr(commands, cmd)
fn.__doc__ = fn.__doc__.rstrip() + '\n\n ' + doc
wrapped = generic and wrappers.generic or getattr(wrappers, cmd)
entry = extensions.wrapcommand(commands.table, cmd, wrapped)
if ppopts:
if opts:
rebase = extensions.find('rebase')
if not rebase:
entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', wrappers.rebase)
entry[1].append(('', 'svn', None, 'automatic svn rebase'))
helpdir = os.path.join(os.path.dirname(__file__), 'help')
entries = (
"Working with Subversion Repositories",
lambda: open(os.path.join(helpdir, 'subversion.rst')).read()),
# in 1.6 and earler the help table is a tuple
if getattr(help.helptable, 'extend', None):
help.helptable = help.helptable + entries
if templatekw:
if revset:
if subrepo:
subrepo.types['hgsubversion'] = svnexternals.svnsubrepo
def reposetup(ui, repo):
if repo.local():
svnrepo.generate_repo_class(ui, repo)
_old_local = hg.schemes['file']
def _lookup(url):
if util.islocalrepo(url):
return svnrepo
return _old_local(url)
# install scheme handlers
hg.schemes.update({ 'file': _lookup, 'http': svnrepo, 'https': svnrepo,
'svn': svnrepo, 'svn+ssh': svnrepo, 'svn+http': svnrepo,
'svn+https': svnrepo})
commands.optionalrepo += ' svn'
cmdtable = {
[('u', 'svn-url', '', 'path to the Subversion server.'),
('', 'stupid', False, 'be stupid and use diffy replay.'),
('A', 'authors', '', 'username mapping filename'),
('', 'filemap', '',
'remap file to exclude paths or include only certain paths'),
('', 'force', False, 'force an operation to happen'),
('', 'username', '', 'username for authentication'),
('', 'password', '', 'password for authentication'),
('r', 'rev', '', 'Mercurial revision'),
'hg svn <subcommand> ...',
# only these methods are public
__all__ = ('cmdtable', 'reposetup', 'uisetup')
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
from mercurial import util as hgutil
import svnwrap
import svnexternals
import util
class NoFilesException(Exception):
"""Exception raised when you try and commit without files.
def _isdir(svn, branchpath, svndir):
path = ''
if branchpath:
path = branchpath + '/'
svn.list_dir('%s%s' % (path, svndir))
return True
except svnwrap.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 and path:
yield path
pos = path.rfind('/')
while pos != -1:
yield path[:pos]
pos = path.rfind('/', 0, pos)
# Include the root path, properties can be set explicitely on it
# (like externals), and you want to preserve it if there are any
# other child item still existing.
yield ''
def getctxdirs(ctx, keptdirs, extdirs):
dirs = {}
for f in ctx.manifest():
for d in finddirs(f):
if d in dirs:
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.
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):
for d in olddirs:
if d not in newdirs and _isdir(svn, branchpath, d):
return added, deleted
def commit(ui, repo, rev_ctx, meta, base_revision, svn):
"""Build and send a commit from Mercurial to Subversion.
file_data = {}
parent = rev_ctx.parents()[0]
parent_branch = rev_ctx.parents()[0].branch()
branch_path = 'trunk'
if meta.layout == 'single':
branch_path = ''
elif parent_branch and parent_branch != 'default':
branch_path = 'branches/%s' % parent_branch
extchanges = svnexternals.diff(svnexternals.parse(ui, parent),
svnexternals.parse(ui, 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 in util.ignoredfiles:
new_data = base_data = ''
action = ''
if file in rev_ctx:
fctx = rev_ctx.filectx(file)
new_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] + [''])
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'
pos = file.rfind('/')
if pos >= 0:
if file[:pos] in deleteddirs:
# This file will be removed when its directory is removed
action = 'delete'
file_data[file] = base_data, new_data, action
def svnpath(p):
return ('%s/%s' % (branch_path, p)).strip('/')
changeddirs = []
for d, v1, v2 in extchanges:
props.setdefault(svnpath(d), {})['svn:externals'] = v2
if d not in deleteddirs and d not in addeddirs:
# Now we are done with files, we can prune deleted directories
# against themselves: ignore a/b if a/ is already removed
deleteddirs2 = list(deleteddirs)
for d in deleteddirs2:
pos = d.rfind('/')
if pos >= 0 and d[:pos] in deleteddirs:
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 and tf != ntf:
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()
svn.commit(new_target_files, rev_ctx.description(), file_data,
base_revision, set(addeddirs), set(deleteddirs),
props, newcopies)
except svnwrap.SubversionException, e:
if len(e.args) > 0 and e.args[1] in (svnwrap.ERR_FS_TXN_OUT_OF_DATE,
raise hgutil.Abort('Outgoing changesets parent is not at '
'subversion HEAD\n'
'(pull again and rebase on a newer revision)')
return True
import errno
import traceback
from mercurial import revlog
from mercurial import node
from mercurial import context
from mercurial import util as hgutil
import svnexternals
import util
class MissingPlainTextError(Exception):
"""Exception raised when the repo lacks a source file required for replaying
a txdelta.
class ReplayException(Exception):
"""Exception raised when you try and commit but the replay encountered an
def updateexternals(ui, meta, current):
# TODO fix and re-enable externals for single-directory clones
if not current.externals or meta.layout == 'single':
# accumulate externals records for all branches
revnum = current.rev.revnum
branches = {}
for path, entry in current.externals.iteritems():
if not meta.is_path_valid(path):
ui.warn('WARNING: Invalid path %s in externals\n' % path)
p, b, bp = meta.split_branch_path(path)
if bp not in branches:
parent = meta.get_parent_revision(revnum, b)
pctx = meta.repo[parent]
branches[bp] = (svnexternals.parse(ui, pctx), pctx)
branches[bp][0][p] = entry
# register externals file changes
for bp, (external, pctx) in branches.iteritems():
if bp and bp[-1] != '/':
bp += '/'
updates = svnexternals.getchanges(ui, meta.repo, pctx, external)
for fn, data in updates.iteritems():
path = (bp and bp + fn) or fn
if data is not None:
current.set(path, data, False, False)
def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
editor = meta.editor
editor.current.rev = r
if firstrun and meta.revmap.oldest <= 0:
# We know nothing about this project, so fetch everything before
# trying to apply deltas.
ui.debug('replay: fetching full revision\n')
svn.get_revision(r.revnum, editor)
svn.get_replay(r.revnum, editor, meta.revmap.oldest)
current = editor.current
updateexternals(ui, meta, current)
if current.exception is not None: #pragma: no cover
raise ReplayException()
if current.missing:
raise MissingPlainTextError()
# paranoidly generate the list of files to commit
files_to_commit = set(current.files.keys())
# back to a list and sort so we get sane behavior
files_to_commit = list(files_to_commit)
branch_batches = {}
rev = current.rev
date = meta.fixdate(
# build up the branches that have files on them
for f in files_to_commit:
if not meta.is_path_valid(f):
p, b = meta.split_branch_path(f)[:2]
if b not in branch_batches:
branch_batches[b] = []
branch_batches[b].append((p, f))
closebranches = {}
for branch in tbdelta['branches'][1]:
branchedits = meta.revmap.branchedits(branch, rev)
if len(branchedits) < 1:
# can't close a branch that never existed
ha = branchedits[0][1]
closebranches[branch] = ha
extraempty = (set(tbdelta['branches'][0]) -
(set(current.emptybranches) | set(branch_batches.k