Commit f57bc4ab authored by Qijiang Fan's avatar Qijiang Fan

Upstream version 1.6

parent 206ab31e
repo: f2636cfed11500fdc47d1e3822d8e4a2bd636bf7
node: 77b22e5b4ea6c248e079afd0f1e544cb5690ce20
node: 7d47a0f731354505ed9ae8d60d2a6996e8c3294f
branch: default
tag: 1.5
tag: 1.6
......@@ -7,3 +7,5 @@
4bbc6bf947f56a92e95a04a27b94a9f72d5482d7 1.2.1
0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 1.3
07234759a3f750029ccaa001837d42fa12dd33ee 1.4
77b22e5b4ea6c248e079afd0f1e544cb5690ce20 1.5
d0f3a5c2cb56ce65d9ef1c611c8bfbebdc3bef34 1.5.1
include Makefile hgsubversion/help/*.rst
include Makefile hgsubversion/help/*.rst tests/fixtures/invalid_utf8.tar.gz
recursive-include tests *.py *.sh *.svndump *.txt
......@@ -16,7 +16,7 @@ Installation
------------
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.
need Mercurial 2.0 or later.
.. _mercurial: http://selenic.com/repo/hg
.. _mercurial-stable: http://selenic.com/repo/hg-stable
......
......@@ -84,6 +84,10 @@ wrapcmds = { # cmd: generic, target, fixdoc, ppopts, opts
'clone': (False, 'sources', True, True, [
('T', 'tagpaths', '',
'list of paths to search for tags in Subversion repositories'),
('', 'branchdir', '',
'path to search for branches in subversion repositories'),
('', 'infix', '',
'path relative to trunk, branch an tag dirs to import'),
('A', 'authors', '',
'file mapping Subversion usernames to Mercurial authors'),
('', 'filemap', '',
......@@ -180,6 +184,9 @@ def reposetup(ui, repo):
for tunnel in ui.configlist('hgsubversion', 'tunnels'):
hg.schemes['svn+' + tunnel] = svnrepo
if revset and ui.configbool('hgsubversion', 'nativerevs'):
extensions.wrapfunction(revset, 'stringset', util.revset_stringset)
_old_local = hg.schemes['file']
def _lookup(url):
if util.islocalrepo(url):
......
"""Functions to work around API changes inside Mercurial."""
def branchset(repo):
"""Return the set of branches present in a repo.
Works around branchtags() vanishing between 2.8 and 2.9.
"""
try:
return set(repo.branchmap())
except AttributeError:
return set(repo.branchtags())
......@@ -191,7 +191,7 @@ class HgEditor(svnwrap.Editor):
# A mapping of file paths to batons
self._openpaths = {}
self._deleted = set()
self._getctx = util.lrucachefunc(self.repo.changectx, 3)
self._getctx = hgutil.lrucachefunc(self.repo.changectx)
# A stack of opened directory (baton, path) pairs.
self._opendirs = []
self._missing = set()
......@@ -401,6 +401,10 @@ class HgEditor(svnwrap.Editor):
br_path, branch = self.meta.split_branch_path(path)[:2]
if br_path is not None:
if not copyfrom_path and not br_path:
# This handles the case where a branch root is
# replaced without copy info. It will show up as a
# deletion and then an add.
self.meta.closebranches.discard(branch)
self.current.emptybranches[branch] = True
else:
self.current.emptybranches[branch] = False
......@@ -570,13 +574,16 @@ class HgEditor(svnwrap.Editor):
msg += _TXDELT_WINDOW_HANDLER_FAILURE_MSG
e.args = (msg,) + others
raise e
# re-raising ensures that we show the full stack trace
raise
# window being None means commit this file
if not window:
self._openfiles[file_baton] = (
path, target, isexec, islink, copypath)
except svnwrap.SubversionException, e: # pragma: no cover
self.ui.traceback()
if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA:
self.addmissing(path)
else: # pragma: no cover
......
......@@ -32,7 +32,12 @@ all its tags and branches. In such cases you should clone from one level above
trunk, as in the example above. This is known as `standard layout`, and works
with repositories that use the conventional ``trunk``, ``tags`` and ``branches``
directories. By default, hgsubversion will use this layout whenever it finds any
of these directories at the specified directory on the server.
of these directories at the specified directory on the server. Standard layout
also supports alternate names for the ``branches`` directory and multiple tags
locations. Finally, Standard Layout supports selecting a subdirectory relative
to ``trunk``, and each branch and tag dir. This is useful if you have a single
``trunk``, ``branches``, and ``tags`` with several projects inside, and you wish
to import only a single project.
If you instead want to clone just a single directory or branch, clone the
specific directory path. In the example above, to get *only* trunk, you would
......@@ -45,6 +50,10 @@ hgsubversion.layout option (see below for detailed help) ::
$ hg clone --layout single svn+http://python-nose.googlecode.com/svn nose-hg
Finally, if you want to clone two or more directores as separate
branches, use the custom layout. See the documentation below for the
``hgsubversionbranch.*`` configuration for detailed help.
Pulling new revisions into an already-converted repository is the same
as from any other Mercurial source. Within the first example above,
the following three commands are all equivalent::
......@@ -296,6 +305,19 @@ settings:
Path to a file for changing branch names during the conversion from
Subversion to Mercurial.
``hgsubversion.branchdir``
Specifies the subdirectory to look for branches under. The
default is ``branches``. This option has no effect for
single-directory clones.
``hgsubversion.infix``
Specifies a path to strip between relative to the trunk/branch/tag
root as the mercurial root. This can be used to import a single
sub-project when you have several sub-projects under a single
trunk/branches/tags layout in subversion.
``hgsubversion.filemap``
Path to a file for filtering files during the conversion. Files may either
......@@ -357,7 +379,9 @@ The following options only have an effect on the initial clone of a repository:
repository is converted into a single branch. The default,
``auto``, causes hgsubversion to assume a standard layout if any
of trunk, branches, or tags exist within the specified directory
on the server.
on the server. ``custom`` causes hgsubversion to read the
``hgsubversionbranch`` config section to determine the repository
layout.
``hgsubversion.startrev``
......@@ -389,6 +413,27 @@ The following options only have an effect on the initial clone of a repository:
you use this option, be sure to carefully check the result of a
pull afterwards.
``hgsubversionbranch.*``
Use this config section with the custom layout to specify a cusomt
mapping of subversion path to Mercurial branch. This is useful if
your layout is substantially different from the standard
trunk/branches/tags layout and/or you are only interested in a few
branches.
Example config that pulls in trunk as the default branch,
personal/alice as the alice branch, and releases/2.0/2.7 as
release-2.7::
[hgsubversionbranch]
default = trunk
alice = personal/alice
release-2.7 = releases/2.0/2.7
Note that it is an error to specify more than one branch for a
given path, or to sepecify nested paths (e.g. releases/2.0 and
releases/2.0/2.7)
Please note that some of these options may be specified as command line options
as well, and when done so, will override the configuration. If an authormap,
filemap or branchmap is specified, its contents will be read and stored for use
......
"""Code for dealing with subversion layouts
This package is intended to encapsulate everything about subversion
layouts. This includes detecting the layout based on looking at
subversion, mapping subversion paths to hg branches, and doing any
other path translation necessary.
NB: this has a long way to go before it does everything it claims to
"""
from mercurial import util as hgutil
import custom
import detect
import persist
import single
import standard
__all__ = [
"detect",
"layout_from_name",
"persist",
]
# This is the authoritative store of what layouts are available.
# The intention is for extension authors who wish to build their own
# layout to add it to this dict.
NAME_TO_CLASS = {
"custom": custom.CustomLayout,
"single": single.SingleLayout,
"standard": standard.StandardLayout,
}
def layout_from_name(name, ui):
"""Returns a layout module given the layout name
You should use one of the layout.detect.* functions to get the
name to pass to this function.
"""
if name not in NAME_TO_CLASS:
raise hgutil.Abort('Unknown hgsubversion layout: %s' %name)
return NAME_TO_CLASS[name](ui)
"""Module to hold the base API for layout classes.
This module should not contain any implementation, just a definition
of the API concrete layouts are expected to implement.
"""
from mercurial import util as hgutil
class BaseLayout(object):
def __init__(self, ui):
self.ui = ui
def __unimplemented(self, method_name):
raise NotImplementedError(
"Incomplete layout implementation: %s.%s doesn't implement %s" %
(self.__module__, self.__name__, method_name))
def localname(self, path):
"""Compute the local name for a branch located at path.
path should be relative to the repo url.
"""
self.__unimplemented('localname')
def remotename(self, branch):
"""Compute a subversion path for a mercurial branch name
This should return a path relative to the repo url
Implementations may indicate that no mapping is possible for
the given branch by raising a KeyError.
"""
self.__unimplemented('remotename')
def remotepath(self, branch, subdir='/'):
"""Compute a subversion path for a mercurial branch name.
This should return an absolute path, assuming our repo root is at subdir
A false subdir shall be taken to mean /.
Implementations may indicate that no mapping is possible for
the given branch by raising a KeyError.
"""
self.__unimplemented('remotepath')
def taglocations(self, meta_data_dir):
"""Return a list of locations within svn to search for tags
Should be returned in reverse-sorted order.
"""
self.__unimplemented('tagpaths')
def get_path_tag(self, path, taglocations):
"""Get the tag name for the given svn path, if it is a possible tag.
This function should return None if the path cannot be a tag.
Returning a non-empty sring does not imply that the path is a
tag, only that it is a candidate to be a tag. Returning an
empty string is an error.
Path should be relative to the repo url.
taglocations should be as returned by self.taglocations()
"""
self.__unimplemented('get_path_tag')
def split_remote_name(self, path, known_branches):
"""Split the path into a branch component and a local component.
path should be relative to our repo url
returns (branch_path, local_path)
branch_path should be suitable to pass into localname,
i.e. branch_path should NOT have a leading or trailing /
local_path should be relative to the root of the Mercurial working dir
Note that it is permissible to return a longer branch_path
than is passed in iff the path that is passed in is a parent
directory of exactly one branch. This is intended to handle
the case where we are importing a particular subdirectory of
asubversion branch structure.
"""
self.__unimplemented('split_remote_name')
"""Layout that allows you to define arbitrary subversion to mercurial mappings.
This is the simplest layout to use if your layout is just plain weird.
Also useful if your layout is pretty normal, but you personally only
want a couple of branches.
"""
import base
class CustomLayout(base.BaseLayout):
def __init__(self, ui):
base.BaseLayout.__init__(self, ui)
self.svn_to_hg = {}
self.hg_to_svn = {}
for hg_branch, svn_path in ui.configitems('hgsubversionbranch'):
hg_branch = hg_branch.strip()
if hg_branch == 'default' or not hg_branch:
hg_branch = None
svn_path = svn_path.strip('/')
for other_svn in self.svn_to_hg:
if other_svn == svn_path:
msg = 'specified two hg branches for svn path %s: %s and %s'
raise hgutil.Abort(msg % (svn_path, other_hg, hg_branch))
if (other_svn.startswith(svn_path + '/') or
svn_path.startswith(other_svn + '/')):
msg = 'specified mappings for nested svn paths: %s and %s'
raise hgutl.Abort(msg % (svn_path, other_svn))
self.svn_to_hg[svn_path] = hg_branch
self.hg_to_svn[hg_branch] = svn_path
def localname(self, path):
if path in self.svn_to_hg:
return self.svn_to_hg[path]
children = []
for svn_path in self.svn_to_hg:
if svn_path.startswith(path + '/'):
children.append(svn_path)
if len(children) == 1:
return self.svn_to_hg[children[0]]
return '../%s' % path
def remotename(self, branch):
if branch =='default':
branch = None
if branch and branch.startswith('../'):
return branch[3:]
if branch not in self.hg_to_svn:
raise KeyError('Unknown mercurial branch: %s' % branch)
return self.hg_to_svn[branch]
def remotepath(self, branch, subdir='/'):
if not subdir.endswith('/'):
subdir += '/'
return subdir + self.remotename(branch)
def taglocations(self, meta_data_dir):
return []
def get_path_tag(self, path, taglocations):
return None
def split_remote_name(self, path, known_branches):
if path in self.svn_to_hg:
return path, ''
children = []
for svn_path in self.svn_to_hg:
if path.startswith(svn_path + '/'):
return svn_path, path[len(svn_path)+1:]
if svn_path.startswith(path + '/'):
children.append(svn_path)
# if the path represents the parent of exactly one of our svn
# branches, treat it as though it were that branch, because
# that means we are probably pulling in a subproject of an svn
# project, and someone copied the parent svn project.
if len(children) == 1:
return children[0], ''
for branch in known_branches:
if branch and branch.startswith('../'):
if path.startswith(branch[3:] + '/'):
# -3 for the leading ../, plus one for the trailing /
return branch[3:], path[len(branch) - 2:]
if branch[3:].startswith(path + '/'):
children.append(branch[3:])
if len(children) == 1:
return children[0], ''
# this splits on the rightmost '/' but considers the entire
# string to be the branch component of the path if there is no '/'
components = path.rsplit('/', 1)
return components[0], '/'.join(components[1:])
""" Layout detection for subversion repos.
Figure out what layout we should be using, based on config, command
line flags, subversion contents, and anything else we decide to base
it on.
"""
import os.path
from mercurial import util as hgutil
import __init__ as layouts
def layout_from_subversion(svn, revision=None, ui=None):
""" Guess what layout to use based on directories under the svn root.
This is intended for use during bootstrapping. It guesses which
layout to use based on the presence or absence of the conventional
trunk, branches, tags dirs immediately under the path your are
cloning.
Additionally, this will write the layout in use to the ui object
passed, if any.
"""
# import late to avoid trouble when running the test suite
try:
from hgext_hgsubversion import svnwrap
except ImportError:
from hgsubversion import svnwrap
try:
rootlist = svn.list_dir('', revision=revision)
except svnwrap.SubversionException, e:
err = "%s (subversion error: %d)" % (e.args[0], e.args[1])
raise hgutil.Abort(err)
if sum(map(lambda x: x in rootlist, ('branches', 'tags', 'trunk'))):
layout = 'standard'
else:
layout = 'single'
ui.setconfig('hgsubversion', 'layout', layout)
return layout
def layout_from_config(ui, allow_auto=False):
""" Load the layout we are using based on config
We will read the config from the ui object. Pass allow_auto=True
if you are doing bootstrapping and can detect the layout in
another manner if you get auto. Otherwise, we will abort if we
detect the layout as auto.
"""
layout = ui.config('hgsubversion', 'layout', default='auto')
if layout == 'auto' and not allow_auto:
raise hgutil.Abort('layout not yet determined')
elif layout not in layouts.NAME_TO_CLASS and layout != 'auto':
raise hgutil.Abort("unknown layout '%s'" % layout)
return layout
def layout_from_file(meta_data_dir, ui=None):
""" Load the layout in use from the metadata file.
If you pass the ui arg, we will also write the layout to the
config for that ui.
"""
layout = None
layoutfile = os.path.join(meta_data_dir, 'layout')
if os.path.exists(layoutfile):
f = open(layoutfile)
layout = f.read().strip()
f.close()
if ui:
ui.setconfig('hgsubversion', 'layout', layout)
return layout
def layout_from_commit(subdir, revpath, branch, ui):
""" Guess what the layout is based existing commit info
Specifically, this compares the subdir for the repository and the
revpath as extracted from the convinfo in the commit. If they
match, the layout is assumed to be single. Otherwise, it tries
the available layouts and selects the first one that would
translate the given branch to the given revpath.
"""
subdir = subdir or '/'
if subdir == revpath:
return 'single'
candidates = set()
for layout in layouts.NAME_TO_CLASS:
layoutobj = layouts.layout_from_name(layout, ui)
try:
remotepath = layoutobj.remotepath(branch, subdir)
except KeyError:
continue
if remotepath == revpath:
candidates.add(layout)
if len(candidates) == 1:
return candidates.pop()
elif candidates:
config_layout = layout_from_config(ui, allow_auto=True)
if config_layout in candidates:
return config_layout
return 'standard'
"""Code for persisting the layout config in various locations.
Basically, if you want to save the layout, this is where you should go
to do it.
"""
import os.path
def layout_to_file(meta_data_dir, layout):
"""Save the given layout to a file under the given meta_data_dir"""
layoutfile = os.path.join(meta_data_dir, 'layout')
f = open(layoutfile, 'w')
f.write(layout)
f.close()
import base
class SingleLayout(base.BaseLayout):
"""A layout with only the default branch"""
def localname(self, path):
return None
def remotename(self, branch):
return ''
def remotepath(self, branch, subdir='/'):
return subdir or '/'
def taglocations(self, meta_data_dir):
return []
def get_path_tag(self, path, taglocations):
return None
def split_remote_name(self, path, known_branches):
return '', path
import os.path
import pickle
import base
class StandardLayout(base.BaseLayout):
"""The standard trunk, branches, tags layout"""
def __init__(self, ui):
base.BaseLayout.__init__(self, ui)
self._tag_locations = None
self._branch_dir = ui.config('hgsubversion', 'branchdir', 'branches')
if self._branch_dir[0] == '/':
self._branch_dir = self._branch_dir[1:]
if self._branch_dir[-1] != '/':
self._branch_dir += '/'
self._infix = ui.config('hgsubversion', 'infix', '').strip('/')
if self._infix:
self._infix = '/' + self._infix
self._trunk = 'trunk%s' % self._infix
def localname(self, path):
if path == self._trunk:
return None
elif path.startswith(self._branch_dir) and path.endswith(self._infix):
path = path[len(self._branch_dir):]
if self._infix:
path = path[:-len(self._infix)]
return path
return '../%s' % path
def remotename(self, branch):
if branch == 'default' or branch is None:
path = self._trunk
elif branch.startswith('../'):
path = branch[3:]
else:
path = ''.join((self._branch_dir, branch, self._infix))
return path
def remotepath(self, branch, subdir='/'):
if subdir == '/':
subdir = ''
branchpath = self._trunk
if branch and branch != 'default':
if branch.startswith('../'):
branchpath = branch[3:]
else:
branchpath = ''.join((self._branch_dir, branch, self._infix))
return '%s/%s' % (subdir or '', branchpath)
def taglocations(self, meta_data_dir):
# import late to avoid trouble when running the test suite
from hgext_hgsubversion import util
if self._tag_locations is None:
tag_locations_file = os.path.join(meta_data_dir, 'tag_locations')
if os.path.exists(tag_locations_file):
f = open(tag_locations_file)
self._tag_locations = pickle.load(f)
f.close()
else:
self._tag_locations = self.ui.configlist('hgsubversion',
'tagpaths',
['tags'])
util.pickle_atomic(self._tag_locations, tag_locations_file)
# ensure nested paths are handled properly
self._tag_locations.sort()
self._tag_locations.reverse()
return self._tag_locations
def get_path_tag(self, path, taglocations):
for tagspath in taglocations:
if path.startswith(tagspath + '/'):
tag = path[len(tagspath) + 1:]
if tag:
return tag
return None
def split_remote_name(self, path, known_branches):
# this odd evolution is how we deal with people doing things like
# creating brances (note the typo), committing to a branch under it,
# and then moving it to branches
# we need to find the ../foo branch names, if they exist, before
# trying to create a normally-named branch
components = path.split('/')
candidate = ''
while self.localname(candidate) not in known_branches and components:
if not candidate:
candidate = components.pop(0)
else:
candidate += '/'
candidate += components.pop(0)
if self.localname(candidate) in known_branches:
return candidate, '/'.join(components)
if path == 'trunk' or path.startswith('trunk/'):
return self._trunk, path[len(self._trunk) + 1:]
if path.startswith(self._branch_dir):
path = path[len(self._branch_dir):]
components = path.split('/', 1)
branch_path = ''.join((self._branch_dir, components[0]))
if len(components) == 1:
local_path = ''
else:
local_path = components[1]