summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTristan Seligmann <mithrandi@debian.org>2017-08-11 09:51:59 +0000
committerTristan Seligmann <mithrandi@debian.org>2017-08-11 09:51:59 +0000
commita234ef0228929919399bc8adad6f8d8bb8dee7bc (patch)
tree4f9185baa7e0e6d37529ae2072e1c970244f6e14
parentb7339d75dcc99eeba499b376700e9e5ff545baff (diff)
parentf505a84d33c238a892064774ca31854d3b5b1df2 (diff)
New upstream version (hg snapshot).
-rw-r--r--.hg_archival.txt8
-rw-r--r--.hgignore21
-rw-r--r--.hgsigs1
-rw-r--r--.hgtags2
-rw-r--r--debian/changelog11
-rw-r--r--debian/control3
-rw-r--r--debian/watch2
-rw-r--r--hgsubversion/__init__.py49
-rw-r--r--hgsubversion/editor.py9
-rw-r--r--hgsubversion/help/subversion.rst19
-rw-r--r--hgsubversion/layouts/custom.py4
-rw-r--r--hgsubversion/maps.py872
-rw-r--r--hgsubversion/replay.py17
-rw-r--r--hgsubversion/stupid.py8
-rw-r--r--hgsubversion/svncommands.py46
-rw-r--r--hgsubversion/svnexternals.py111
-rw-r--r--hgsubversion/svnmeta.py97
-rw-r--r--hgsubversion/svnrepo.py2
-rw-r--r--hgsubversion/svnwrap/common.py3
-rw-r--r--hgsubversion/svnwrap/subvertpy_wrapper.py30
-rw-r--r--hgsubversion/svnwrap/svn_swig_wrapper.py31
-rw-r--r--hgsubversion/util.py72
-rw-r--r--hgsubversion/wrappers.py67
-rw-r--r--tests/comprehensive/test_obsstore_on.py40
-rw-r--r--tests/comprehensive/test_rebuildmeta.py17
-rw-r--r--tests/comprehensive/test_sqlite_revmap.py77
-rw-r--r--tests/comprehensive/test_verify_and_startrev.py1
-rw-r--r--tests/fixtures/dir_removal.svndump103
-rw-r--r--tests/fixtures/rename-closed-branch-dir.sh69
-rw-r--r--tests/fixtures/rename-closed-branch-dir.svndump296
-rwxr-xr-xtests/run.py82
-rw-r--r--tests/test_fetch_branches.py3
-rw-r--r--tests/test_fetch_command.py2
-rw-r--r--tests/test_fetch_dir_removal.py17
-rw-r--r--tests/test_fetch_mappings.py51
-rw-r--r--tests/test_fetch_renames.py33
-rw-r--r--tests/test_helpers.py1
-rw-r--r--tests/test_hooks.py2
-rw-r--r--tests/test_pull.py16
-rw-r--r--tests/test_pull_fallback.py4
-rw-r--r--tests/test_push_command.py30
-rw-r--r--tests/test_revmap_migrate.py72
-rw-r--r--tests/test_svnwrap.py14
-rw-r--r--tests/test_tags.py13
-rw-r--r--tests/test_urls.py55
-rw-r--r--tests/test_util.py62
-rw-r--r--tests/test_utility_commands.py6
-rw-r--r--tox.ini18
48 files changed, 2020 insertions, 549 deletions
diff --git a/.hg_archival.txt b/.hg_archival.txt
index bf70e8c..5b05a57 100644
--- a/.hg_archival.txt
+++ b/.hg_archival.txt
@@ -1,4 +1,6 @@
repo: f2636cfed11500fdc47d1e3822d8e4a2bd636bf7
-node: 49d324e11856f5d65bd7f83f7ffb3a2f07bf82c1
-branch: stable
-tag: 1.8.6
+node: b3e41b0d50a2fefdfea1d465016699cfdcf04dcc
+branch: default
+latesttag: 1.8.7
+latesttagdistance: 23
+changessincelatesttag: 149
diff --git a/.hgignore b/.hgignore
index a7780ee..8b816a0 100644
--- a/.hgignore
+++ b/.hgignore
@@ -1,21 +1,22 @@
syntax:glob
-build
+*.egg-info
+*.orig
+*.py,cover
*.pyc
*.pyo
-.DS_Store
*.swp
*~
+.DS_Store
.coverage
-cover
-*.py,cover
+.noseids
+.project
+.pydevproject
+.settings
+.tox
MANIFEST
+build
+cover
dist
-*.egg-info
hgsubversion/__version__.py
nbproject
-.project
-.pydevproject
-.settings
-*.orig
-.noseids
tests/fixtures/temp
diff --git a/.hgsigs b/.hgsigs
index 3a28608..0782724 100644
--- a/.hgsigs
+++ b/.hgsigs
@@ -1,3 +1,4 @@
41ff2014d125cfe6eda39a79dc1c7ca9c99bf785 0 iQIcBAABAgAGBQJTY6GOAAoJELnJ3IJKpb3VrTcP/iIvysZP7BMyBW6QfAZMl9vRz06BlOgGIapNFcI2E12yC6j8oADymz2QZCtj21YyyuxIeuPLya5LQ1OoGHsZk/iW700UxUdrUzW3QbKiTfKrNXN915aRE1rYznzetgQc+xqGV4Hk7P5GarAfBcZtCVtc7/Gmo9AMkU1QbLzhwDGMLLUDt1Gsu9Vn3JugVN6O10NqxxGvU18ahVMsy+qyaJBZ5fJUKnBAmwHa3DCP5DRfDNKUaAl7xOK09gvRi6h6dPl8HPrx6tN3wkQ0QRB6kD06yrJejnQ7eM/1fxLNovZ0MZe2+bHMeXWnzGseQsrlvZSi3GWch4xSW+Tqa/pRo0qbbQ8Tq3Hj7J2Ip482nEwzEZZiWcw/1vAIraYkinGuTcw0Kq+IsKh8zBRN5jiyq3NNQYXraC93KL9/raVbssPG6KAP7LBiN0cYM51T3lL1ER1/oMJJfdX4OWn6dCRptlqfwhW9hxK/R+dbm2lOcT6EENtYp4nq5amqym0i4zx/bAJmw4mpNMEmZdAIBCgjxxb01ez1IQFzvg+/+DIr62n+YGlhsYVJTDRVAdwaLXQKnSo7gIowpsWU9WRejxaISuVr6aQbuMtbtZIjqUFSIC6UR1d9GQdkiXWv+tCP29UHqTV+81BaFAyTe6s4zc5IhEZudwdN6f1bBtYD3BN3vX9M
5cdc58c1c9a7ed24a7e206150032a9443a59c569 0 iQIcBAABAgAGBQJT25kVAAoJELnJ3IJKpb3Vxm0QAMQApF/pARrrb3k1ZholO0o4y1AU9551r3yeFRS9U6kqofqspSOKx0rrzw2RraSuMP++eznjfpbv1njM/Zd+oEFgCEHxq6lrkafxxq8oKj07IVqjaRshleBiVbr9/Hrko7W4zjhV6ROIpM2g1o6Sx8qXqRkzAB3YbDppxnI5lVOVZGnKDTcInw+eaDRX5S8clXdd+xbRBSRd+gM6VFsUrP9963S7siZU/u8Lmo5fUZdKFC5iNLPjTuhEhw7FWg4TzeAyWedvv0WFl14TJpdJRlmdwptVlR48PkjFZUhwcYZJ4LE2HUclpDmNRpR1PMrZK5FdH42vM9OQfB2c+0whDWUo0UaM1QnEc1NKj5828FmknfwrwwNrcanHl+Ig4ccmVJeDZDtZDg0TyUBPfnUVBvNFTpJqPo7MlAeQD1e5YflHSZR+CBzG8UTQssJbfiwO2o+bKVrqjHxjvsIqjdvi2Ueeh0fZw09l/zzz17I4RymwQTD+laEXQ7Xk+0VwQyYBfeR/nwYORcWDVVrATxROvG3lqKqGbU0VnzUoGF7P+bRbWEX8Y+noSKPYiqoDunIv4su9IOMBG0Y672X+ETta8ALKWKAx8cSD/kVzyLqQPWryvgUv6eG6iJpDN/uw7F23gRkKdBhOH9N9TSBCfA6ZUvTTgsDz5HLsEiOMCDQKXzv5
e7d84481cf76a1a839b4ab2ebb0a081b01658fe0 0 iQIcBAABAgAGBQJUy6pRAAoJELnJ3IJKpb3VNMkP/RwDENQbqcbT9HxmbmynPQy5s7G4jUBD35h67GKE18hNAPTCe1eO6sTlwEBNGsmFov+E/0PHr+GGb6wQDK/hetcTIFSMaQNDkTh4H1Znp/wsXr+j1nrDBPNMlrrNBNMWCMKYPsLI/XsGCBOnxiA4pa6GNOI1G424wlfsKtt9DiE8jVd0AqUENQBCCbX4QML8+aCrkjxEU6LhQbZMMqCMbeZhAlgxnQ6WiM09DTy2lJFgvZCqR1UFNmDaC+bXwL08443RIzsBmzi+XunL57wjRd4azRrUudufOe1/0Eg6UYREcoQiawU+4j6++Vo0lEJQqf3BKXCylWbviNXy02FdZv9ER725pJ2PXEa1nJ5qoO4/KWef/Rlqe0q5c1K/0FkriJRp3cltq2JBdHJEBYekiCTlqOEgPVGvLRdMiftGQwXbrI60NZ9O1SI7bq8peciljF+/CJXSjfUefCk6TpAZKLJDF2ALkFC0XlbI6LwSztGX7i+07Us8FelvBj5KWdKMkU4/n9pM1ipQdJ8x/z+J7A5MaMVUU0yTp2T8YU8QKCymybPhzJE6b6USDAn0nc37l1BZtajenJB7YCcwcypiWZY6x08vKhNNapYLf19Nkpug+NeSPGMa7x4x31Rylr3bgPmBI5FtNZWbuEP/M7iMgmsF605ZptW9UYaOzy9/sgrR
+051a517b473b7bbb7f2176ed38a34fcee96fceee 0 iQIcBAABCAAGBQJYoNNkAAoJELnJ3IJKpb3VjncP/2jqEWRuxEFQs1Er3yPPi+Uj+NRRS6g7wI2VSGsgi6MVSUQcqH9BfHGciVVyzOck/m3F5RtTBNizhkqVWL7zYjoQtdUShkIDI9ac8m8pzSXJbJeZ7Lum4xzYdgJimqGlW4TC80ri8909al83yPVUKtjdF4G7h/ZhJz9aaDXsKtpSnwgXsXZilO6r18G3g8MRcWjxBnuY0+lb/vPIH01tx3QrTFQNIv1xjrV8pLF4g7e6MLsXrKVstC0PsBPs24tBXchbTbA5xuncgGIGSTep6gth6XrBEQIqZ3uenUC2uABeuK9YCujWqce/EKCKKD/im3zK9og61nR+sSkd6jitITKcWpTT4LsD7FQukpVBeAfbWE/8WHhdAICryG/3qEd3i841DANsXpsTO/BNBvRrlwVpbpw62k9aCXugPb88nt3HhNh6XPEMW4yAMpOkbmw0y6fSW9se8WbgGZntoYG3+AS0OkGKLkHVhJn5vGX4kUWL/rcbxIiTxSL3DA2gt5OcKVjQ7D7FPzZ1Ws9XQcy3/ii0qjZEUiWgRWEg1FMwV2sNz8CvXGhwh2D+8lBz5QSwN4NENcx0/2NQ5xZgIk8eb7T580dirC+N5PIHYqcxr7y6fDICMt3na0+goUyJJayAYkopAdkeGW94Xv68u6ceALuwKZSRMdJ2FCVwAEdaHAJG
diff --git a/.hgtags b/.hgtags
index bc59dc3..93a50e4 100644
--- a/.hgtags
+++ b/.hgtags
@@ -20,3 +20,5 @@ dde1ade36a49d3d0e1b4b8bd384a6797665b5081 1.8.1
759cafce6becef077fb1a152b554a05ff66b04cd 1.8.3
89997a5fc18163c5f65b83272b4521cdbf29984e 1.8.4
bd979667611d9df733c61251e7668899f3e77a8f 1.8.5
+49d324e11856f5d65bd7f83f7ffb3a2f07bf82c1 1.8.6
+051a517b473b7bbb7f2176ed38a34fcee96fceee 1.8.7
diff --git a/debian/changelog b/debian/changelog
index 339b7e8..c00ba71 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,14 @@
+hgsubversion (1.8.7+1517-b3e41b0d50a2-1) unstable; urgency=medium
+
+ * New upstream snapshot.
+ - 1.8.7 does not work with latest Mercurial.
+ * Fix watch file.
+ * Bump Standards-Version to 4.0.0 (no changes).
+ * Add myself to Uploaders (taking over Mercurial from Javi Merino;
+ thanks for your past work!)
+
+ -- Tristan Seligmann <mithrandi@debian.org> Fri, 11 Aug 2017 11:50:14 +0200
+
hgsubversion (1.8.6-1) unstable; urgency=medium
* New upstream release
diff --git a/debian/control b/debian/control
index cb42f44..bd2d947 100644
--- a/debian/control
+++ b/debian/control
@@ -4,6 +4,7 @@ Priority: extra
Maintainer: Qijiang Fan <fqj1994@gmail.com>
Uploaders:
Javi Merino <vicho@debian.org>,
+ Tristan Seligmann <mithrandi@debian.org>,
Build-Depends:
debhelper (>= 9),
dh-python,
@@ -11,7 +12,7 @@ Build-Depends:
mercurial,
subversion,
python-subvertpy,
-Standards-Version: 3.9.8
+Standards-Version: 4.0.0
Homepage: https://bitbucket.org/durin42/hgsubversion
Vcs-Svn: svn://anonscm.debian.org/collab-maint/deb-maint/hgsubversion/trunk
Vcs-Browser: https://anonscm.debian.org/viewvc/collab-maint/deb-maint/hgsubversion/trunk
diff --git a/debian/watch b/debian/watch
index 2f902b9..041170c 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,2 +1,2 @@
version=3
-https://bitbucket.org/durin42/hgsubversion/downloads /durin42/hgsubversion/get/([0-9\.]*)\.tar\.gz
+https://bitbucket.org/durin42/hgsubversion/downloads/?tab=tags /durin42/hgsubversion/get/([0-9\.]*)\.tar\.gz
diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py
index 024c1d4..93037c8 100644
--- a/hgsubversion/__init__.py
+++ b/hgsubversion/__init__.py
@@ -196,34 +196,41 @@ hg.schemes.update({ 'file': _lookup, 'http': svnrepo, 'https': svnrepo,
if hgutil.safehasattr(commands, 'optionalrepo'):
commands.optionalrepo += ' svn'
-cmdtable = {
- "svn":
- (svncommands.svn,
- [('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'),
- ('', 'unsafe-skip-uuid-check', False,
- 'skip repository uuid check in rebuildmeta'),
- ],
- 'hg svn <subcommand> ...',
- ),
-}
+svncommandopts = [
+ ('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'),
+ ('', 'unsafe-skip-uuid-check', False,
+ 'skip repository uuid check in rebuildmeta'),
+]
+svnusage = 'hg svn <subcommand> ...'
# only these methods are public
__all__ = ('cmdtable', 'reposetup', 'uisetup')
-# set up templatekeywords (written this way to maintain backwards compatibility
-# until we drop support for 3.7)
+# set up commands and templatekeywords (written this way to maintain backwards
+# compatibility until we drop support for 3.7 for templatekeywords and 4.3 for
+# commands)
+cmdtable = {
+ "svn": (svncommands.svn, svncommandopts, svnusage),
+}
try:
from mercurial import registrar
templatekeyword = registrar.templatekeyword()
loadkeyword = lambda registrarobj: None # no-op
+
+ if hgutil.safehasattr(registrar, 'command'):
+ cmdtable = {}
+ command = registrar.command(cmdtable)
+ @command('svn', svncommandopts, svnusage)
+ def svncommand(*args, **kwargs):
+ return svncommands.svn(*args, **kwargs)
except (ImportError, AttributeError):
# registrar.templatekeyword isn't available = loading by old hg
@@ -269,6 +276,8 @@ def svnuuidkw(**args):
""":svnuuid: String. Converted subversion revision repository identifier."""
return _templatehelper(args['ctx'], 'svnuuid')
+loadkeyword(templatekeyword)
+
def listsvnkeys(repo):
keys = {}
repo = repo.local()
diff --git a/hgsubversion/editor.py b/hgsubversion/editor.py
index f50f928..7b1b632 100644
--- a/hgsubversion/editor.py
+++ b/hgsubversion/editor.py
@@ -106,6 +106,9 @@ class RevisionData(object):
self.clear()
def clear(self):
+ oldstore = getattr(self, 'store', None)
+ if oldstore is not None:
+ oldstore.close()
self.store = FileStore(util.getfilestoresize(self.ui))
self.added = set()
self.deleted = {}
@@ -578,6 +581,12 @@ class HgEditor(svnwrap.Editor):
try:
if not self.meta.is_path_valid(path):
return
+
+ # are we skipping this branch entirely?
+ br_path, branch = self.meta.split_branch_path(path)[:2]
+ if self.meta.skipbranch(branch):
+ return
+
try:
handler(window)
except AssertionError, e: # pragma: no cover
diff --git a/hgsubversion/help/subversion.rst b/hgsubversion/help/subversion.rst
index 7c65acb..263ab52 100644
--- a/hgsubversion/help/subversion.rst
+++ b/hgsubversion/help/subversion.rst
@@ -352,6 +352,25 @@ settings:
Password stores are only supported with the SWIG bindings.
+ ``hgsubversion.revmapimpl``
+
+ Set the revision map implementation. ``plain`` which is simple and works
+ well for small repos. ``sqlite`` is a sqlite based implementation that
+ works better for large repos with a lot of revisions. The default is
+ ``plain``.
+
+ If it is set to an implementation different from what the repo is using,
+ a migration will run automatically when the revision map is accessed.
+
+ ``hgsubversion.sqlitepragmas``
+
+ A list of sqlite PRAGMA statements to tweak sqlite. Each item should be
+ in the format ``key=value`` without ``PRAGMA``, or spaces, or quotation
+ marks. Refer to https://www.sqlite.org/pragma.html for possible options.
+
+ For example, setting it to ``synchronous=0, journal_mode=memory`` will
+ give you better performance at the cost of possible database corruption.
+
``hgsubversion.stupid``
Setting this boolean option to true will force using a slower method for
pulling revisions from Subversion. This method is compatible with servers
diff --git a/hgsubversion/layouts/custom.py b/hgsubversion/layouts/custom.py
index 6e0588f..7c58542 100644
--- a/hgsubversion/layouts/custom.py
+++ b/hgsubversion/layouts/custom.py
@@ -18,7 +18,9 @@ class CustomLayout(base.BaseLayout):
self.svn_to_hg = {}
self.hg_to_svn = {}
- for hg_branch, svn_path in meta.ui.configitems('hgsubversionbranch'):
+ meta._gen_cachedconfig('custombranches', {}, configname='hgsubversionbranch')
+
+ for hg_branch, svn_path in meta.custombranches.iteritems():
hg_branch = hg_branch.strip()
if hg_branch == 'default' or not hg_branch:
diff --git a/hgsubversion/maps.py b/hgsubversion/maps.py
index a3eb700..3fc6a5c 100644
--- a/hgsubversion/maps.py
+++ b/hgsubversion/maps.py
@@ -1,92 +1,236 @@
''' Module for self-contained maps. '''
+import collections
+import contextlib
import errno
import os
+import re
+import sqlite3
+import sys
+import weakref
+from mercurial import error
from mercurial import util as hgutil
from mercurial.node import bin, hex, nullid
-import svncommands
+import subprocess
import util
-class AuthorMap(dict):
- '''A mapping from Subversion-style authors to Mercurial-style
- authors, and back. The data is stored persistently on disk.
-
- If the 'hgsubversion.defaultauthors' configuration option is set to false,
- attempting to obtain an unknown author will fail with an Abort.
+class BaseMap(dict):
+ '''A base class for the different type of mappings: author, branch, and
+ tags.'''
+ def __init__(self, ui, filepath):
+ super(BaseMap, self).__init__()
+ self._ui = ui
- If the 'hgsubversion.caseignoreauthors' configuration option is set to true,
- the userid from Subversion is always compared lowercase.
- '''
+ self._commentre = re.compile(r'((^|[^\\])(\\\\)*)#.*')
+ self.syntaxes = ('re', 'glob')
- def __init__(self, meta):
- '''Initialise a new AuthorMap.
+ self._filepath = filepath
+ self.load(filepath)
- The ui argument is used to print diagnostic messages.
+ # Append mappings specified from the commandline. A little
+ # magic here: our name in the config mapping is the same as
+ # the class name lowercased.
+ clmap = util.configpath(self._ui, self.mapname())
+ if clmap:
+ self.load(clmap)
- The path argument is the location of the backing store,
- typically .hg/svn/authors.
+ @classmethod
+ def mapname(cls):
+ return cls.__name__.lower()
+
+ def _findkey(self, key):
+ '''Takes a string and finds the first corresponding key that matches
+ via regex'''
+ if not key:
+ return None
+
+ # compile a new regex key if we're given a string; can't use
+ # hgutil.compilere since we need regex.sub
+ k = key
+ if isinstance(key, str):
+ k = re.compile(re.escape(key))
+
+ # preference goes to matching the exact pattern, i.e. 'foo' should
+ # first match 'foo' before trying regexes
+ for regex in self:
+ if regex.pattern == k.pattern:
+ return regex
+
+ # if key isn't a string, then we are done; nothing matches
+ if not isinstance(key, str):
+ return None
+
+ # now we test the regex; the above loop will be faster and is
+ # equivalent to not having regexes (i.e. just doing string compares)
+ for regex in self:
+ if regex.search(key):
+ return regex
+ return None
+
+ def get(self, key, default=None):
+ '''Similar to dict.get, except we use our own matcher, _findkey.'''
+ if self._findkey(key):
+ return self[key]
+ return default
+
+ def __getitem__(self, key):
+ '''Similar to dict.get, except we use our own matcher, _findkey. If the key is
+ a string, then we can use our regex matching to map its value.
'''
- self.meta = meta
- self.defaulthost = ''
- if meta.defaulthost:
- self.defaulthost = '@%s' % meta.defaulthost.lstrip('@')
+ k = self._findkey(key)
+ val = super(BaseMap, self).__getitem__(k)
- self.super = super(AuthorMap, self)
- self.super.__init__()
- self.load(self.meta.authors_file)
+ # if key is a string then we can transform it using our regex, else we
+ # don't have enough information, so we just return the val
+ if isinstance(key, str):
+ val = k.sub(val, key)
- # append authors specified from the commandline
- clmap = util.configpath(self.meta.ui, 'authormap')
- if clmap:
- self.load(clmap)
+ return val
- def load(self, path):
- ''' Load mappings from a file at the specified path. '''
+ def __setitem__(self, key, value):
+ '''Similar to dict.__setitem__, except we compile the string into a regex, if
+ need be.
+ '''
+ # try to find the regex already in the map
+ k = self._findkey(key)
+ # if we found one, then use it
+ if k:
+ key = k
+ # else make a new regex
+ if isinstance(key, str):
+ key = re.compile(re.escape(key))
+ super(BaseMap, self).__setitem__(key, value)
+
+ def __contains__(self, key):
+ '''Similar to dict.get, except we use our own matcher, _findkey.'''
+ return self._findkey(key) is not None
+ def load(self, path):
+ '''Load mappings from a file at the specified path.'''
path = os.path.expandvars(path)
if not os.path.exists(path):
return
writing = False
- if path != self.meta.authors_file:
- writing = open(self.meta.authors_file, 'a')
+ mapfile = self._filepath
+ if path != mapfile:
+ writing = open(mapfile, 'a')
- self.meta.ui.debug('reading authormap from %s\n' % path)
+ self._ui.debug('reading %s from %s\n' % (self.mapname() , path))
f = open(path, 'r')
- for number, line_org in enumerate(f):
+ syntax = ''
+ for number, line in enumerate(f):
- line = line_org.split('#')[0]
- if not line.strip():
+ if writing:
+ writing.write(line)
+
+ # strip out comments
+ if "#" in line:
+ # remove comments prefixed by an even number of escapes
+ line = self._commentre.sub(r'\1', line)
+ # fixup properly escaped comments that survived the above
+ line = line.replace("\\#", "#")
+ line = line.rstrip()
+ if not line:
continue
+ if line.startswith('syntax:'):
+ s = line[7:].strip()
+ syntax = ''
+ if s in self.syntaxes:
+ syntax = s
+ continue
+ pat = syntax
+ for s in self.syntaxes:
+ if line.startswith(s + ':'):
+ pat = s
+ line = line[len(s) + 1:]
+ break
+
+ # split on the first '='
try:
src, dst = line.split('=', 1)
except (IndexError, ValueError):
- msg = 'ignoring line %i in author map %s: %s\n'
- self.meta.ui.status(msg % (number, path, line.rstrip()))
+ msg = 'ignoring line %i in %s %s: %s\n'
+ self._ui.status(msg % (number, self.mapname(), path,
+ line.rstrip()))
continue
src = src.strip()
dst = dst.strip()
- if self.meta.caseignoreauthors:
- src = src.lower()
-
- if writing:
- if not src in self:
- self.meta.ui.debug('adding author %s to author map\n' % src)
- elif dst != self[src]:
- msg = 'overriding author: "%s" to "%s" (%s)\n'
- self.meta.ui.status(msg % (self[src], dst, src))
- writing.write(line_org)
-
+ if pat != 're':
+ src = re.escape(src)
+ if pat == 'glob':
+ src = src.replace('\\*', '.*')
+ src = re.compile(src)
+
+ if src not in self:
+ self._ui.debug('adding %s to %s\n' % (src, self.mapname()))
+ elif dst != self[src]:
+ msg = 'overriding %s: "%s" to "%s" (%s)\n'
+ self._ui.status(msg % (self.mapname(), self[src], dst, src))
self[src] = dst
f.close()
if writing:
writing.close()
+class AuthorMap(BaseMap):
+ '''A mapping from Subversion-style authors to Mercurial-style
+ authors, and back. The data is stored persistently on disk.
+
+ If the 'hgsubversion.defaultauthors' configuration option is set to false,
+ attempting to obtain an unknown author will fail with an Abort.
+
+ If the 'hgsubversion.caseignoreauthors' configuration option is set to true,
+ the userid from Subversion is always compared lowercase.
+ '''
+
+ def __init__(self, ui, filepath, defaulthost, caseignoreauthors,
+ mapauthorscmd, defaultauthors):
+ '''Initialise a new AuthorMap.
+
+ The ui argument is used to print diagnostic messages.
+
+ The path argument is the location of the backing store,
+ typically .hg/svn/authors.
+ '''
+ if defaulthost:
+ self.defaulthost = '@%s' % defaulthost.lstrip('@')
+ else:
+ self.defaulthost = ''
+ self._caseignoreauthors = caseignoreauthors
+ self._mapauthorscmd = mapauthorscmd
+ self._defaulthost = defaulthost
+ self._defaultauthors = defaultauthors
+
+ super(AuthorMap, self).__init__(ui, filepath)
+
+ def _lowercase(self, key):
+ '''Determine whether or not to lowercase a str or regex using the
+ meta.caseignoreauthors.'''
+ k = key
+ if self._caseignoreauthors:
+ if isinstance(key, str):
+ k = key.lower()
+ else:
+ k = re.compile(key.pattern.lower())
+ return k
+
+ def __setitem__(self, key, value):
+ '''Similar to dict.__setitem__, except we check caseignoreauthors to
+ use lowercase string or not
+ '''
+ super(AuthorMap, self).__setitem__(self._lowercase(key), value)
+
+ def __contains__(self, key):
+ '''Similar to dict.__contains__, except we check caseignoreauthors to
+ use lowercase string or not
+ '''
+ return super(AuthorMap, self).__contains__(self._lowercase(key))
+
def __getitem__(self, author):
''' Similar to dict.__getitem__, except in case of an unknown author.
In such cases, a new value is generated and added to the dictionary
@@ -94,20 +238,34 @@ class AuthorMap(dict):
if author is None:
author = '(no author)'
+ if not isinstance(author, str):
+ return super(AuthorMap, self).__getitem__(author)
+
search_author = author
- if self.meta.caseignoreauthors:
+ if self._caseignoreauthors:
search_author = author.lower()
+ result = None
if search_author in self:
- result = self.super.__getitem__(search_author)
- elif self.meta.defaultauthors:
- self[author] = result = '%s%s' % (author, self.defaulthost)
- msg = 'substituting author "%s" for default "%s"\n'
- self.meta.ui.debug(msg % (author, result))
- else:
- msg = 'author %s has no entry in the author map!'
- raise hgutil.Abort(msg % author)
- self.meta.ui.debug('mapping author "%s" to "%s"\n' % (author, result))
+ result = super(AuthorMap, self).__getitem__(search_author)
+ elif self._mapauthorscmd:
+ cmd = self._mapauthorscmd % author
+ process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
+ output, err = process.communicate()
+ retcode = process.poll()
+ if retcode:
+ msg = 'map author command "%s" exited with error'
+ raise hgutil.Abort(msg % cmd)
+ self[author] = result = output.strip()
+ if not result:
+ if self._defaultauthors:
+ self[author] = result = '%s%s' % (author, self.defaulthost)
+ msg = 'substituting author "%s" for default "%s"\n'
+ self._ui.debug(msg % (author, result))
+ else:
+ msg = 'author %s has no entry in the author map!'
+ raise hgutil.Abort(msg % author)
+ self._ui.debug('mapping author "%s" to "%s"\n' % (author, result))
return result
def reverselookup(self, author):
@@ -127,24 +285,22 @@ class Tags(dict):
"""
VERSION = 2
- def __init__(self, meta, endrev=None):
+ def __init__(self, ui, filepath, endrev=None):
dict.__init__(self)
- self.meta = meta
+ self._filepath = filepath
+ self._ui = ui
self.endrev = endrev
- if os.path.isfile(self.meta.tagfile):
+ if os.path.isfile(self._filepath):
self._load()
else:
self._write()
def _load(self):
- f = open(self.meta.tagfile)
+ f = open(self._filepath)
ver = int(f.readline())
if ver < self.VERSION:
- self.meta.ui.status('tag map outdated, running rebuildmeta...\n')
- f.close()
- os.unlink(self.meta.tagfile)
- svncommands.rebuildmeta(self.meta.ui, self.meta.repo, ())
- return
+ raise error.Abort(
+ 'tag map outdated, please run `hg svn rebuildmeta`')
elif ver != self.VERSION:
raise hgutil.Abort('tagmap too new -- please upgrade')
for l in f:
@@ -160,7 +316,7 @@ class Tags(dict):
def _write(self):
assert self.endrev is None
- f = open(self.meta.tagfile, 'w')
+ f = open(self._filepath, 'w')
f.write('%s\n' % self.VERSION)
f.close()
@@ -181,7 +337,7 @@ class Tags(dict):
if not tag:
raise hgutil.Abort('tag cannot be empty')
ha, revision = info
- f = open(self.meta.tagfile, 'a')
+ f = open(self._filepath, 'a')
f.write('%s %s %s\n' % (hex(ha), revision, tag))
f.close()
dict.__setitem__(self, tag, ha)
@@ -191,44 +347,97 @@ class RevMap(dict):
VERSION = 1
- def __init__(self, meta):
+ lastpulled = util.fileproperty('_lastpulled', lambda x: x._lastpulled_file,
+ default=0, deserializer=int)
+
+ def __init__(self, revmap_path, lastpulled_path):
dict.__init__(self)
- self.meta = meta
+ self._filepath = revmap_path
+ self._lastpulled_file = lastpulled_path
self._hashes = None
+ # disable iteration to have a consistent interface with SqliteRevMap
+ # it's less about performance since RevMap needs iteration internally
+ self._allowiter = False
- if os.path.isfile(self.meta.revmap_file):
+ self.firstpulled = 0
+ if os.path.isfile(self._filepath):
self._load()
else:
self._write()
def hashes(self):
if self._hashes is None:
- self._hashes = dict((v, k) for (k, v) in self.iteritems())
+ self._hashes = dict((v, k) for (k, v) in self._origiteritems())
return self._hashes
- def branchedits(self, branch, rev):
- check = lambda x: x[0][1] == branch and x[0][0] < rev.revnum
- return sorted(filter(check, self.iteritems()), reverse=True)
+ def branchedits(self, branch, revnum):
+ check = lambda x: x[0][1] == branch and x[0][0] < revnum
+ return sorted(filter(check, self._origiteritems()), reverse=True)
- @classmethod
- def readmapfile(cls, path, missingok=True):
+ def branchmaxrevnum(self, branch, maxrevnum):
+ result = 0
+ for num, br in self._origiterkeys():
+ if br == branch and num <= maxrevnum and num > result:
+ result = num
+ return result
+
+ @property
+ def lasthash(self):
+ lines = list(self._readmapfile())
+ if not lines:
+ return None
+ return bin(lines[-1].split(' ', 2)[1])
+
+ def revhashes(self, revnum):
+ for key, value in self._origiteritems():
+ if key[0] == revnum:
+ yield value
+
+ def clear(self):
+ self._write()
+ dict.clear(self)
+ self._hashes = None
+
+ def batchset(self, items, lastpulled):
+ '''Set items in batches
+
+ items is an array of (rev num, branch, binary hash)
+
+ For performance reason, internal in-memory state is not updated.
+ To get an up-to-date RevMap, reconstruct the object.
+ '''
+ with open(self._filepath, 'a') as f:
+ f.write(''.join('%s %s %s\n' % (revnum, hex(binhash), br or '')
+ for revnum, br, binhash in items))
+ self.lastpulled = lastpulled
+
+ def _readmapfile(self):
+ path = self._filepath
try:
f = open(path)
except IOError, err:
- if not missingok or err.errno != errno.ENOENT:
+ if err.errno != errno.ENOENT:
raise
return iter([])
ver = int(f.readline())
- if ver != cls.VERSION:
+ if ver == SqliteRevMap.VERSION:
+ revmap = SqliteRevMap(self._filepath, self._lastpulled_file)
+ tmppath = '%s.tmp' % self._filepath
+ revmap.exportrevmapv1(tmppath)
+ os.rename(tmppath, self._filepath)
+ hgutil.unlinkpath(revmap._dbpath)
+ hgutil.unlinkpath(revmap._rowcountpath, ignoremissing=True)
+ return self._readmapfile()
+ if ver != self.VERSION:
raise hgutil.Abort('revmap too new -- please upgrade')
return f
@util.gcdisable
def _load(self):
- lastpulled = self.meta.lastpulled
- firstpulled = self.meta.firstpulled
+ lastpulled = self.lastpulled
+ firstpulled = self.firstpulled
setitem = dict.__setitem__
- for l in self.readmapfile(self.meta.revmap_file):
+ for l in self._readmapfile():
revnum, ha, branch = l.split(' ', 2)
if branch == '\n':
branch = None
@@ -240,34 +449,349 @@ class RevMap(dict):
if revnum < firstpulled or not firstpulled:
firstpulled = revnum
setitem(self, (revnum, branch), bin(ha))
- self.meta.lastpulled = lastpulled
- self.meta.firstpulled = firstpulled
+ if self.lastpulled != lastpulled:
+ self.lastpulled = lastpulled
+ self.firstpulled = firstpulled
def _write(self):
- f = open(self.meta.revmap_file, 'w')
- f.write('%s\n' % self.VERSION)
- f.close()
+ with open(self._filepath, 'w') as f:
+ f.write('%s\n' % self.VERSION)
def __setitem__(self, key, ha):
revnum, branch = key
- f = open(self.meta.revmap_file, 'a')
b = branch or ''
- f.write(str(revnum) + ' ' + hex(ha) + ' ' + b + '\n')
- f.close()
- if revnum > self.meta.lastpulled or not self.meta.lastpulled:
- self.meta.lastpulled = revnum
- if revnum < self.meta.firstpulled or not self.meta.firstpulled:
- self.meta.firstpulled = revnum
+ with open(self._filepath, 'a') as f:
+ f.write(str(revnum) + ' ' + hex(ha) + ' ' + b + '\n')
+ if revnum > self.lastpulled or not self.lastpulled:
+ self.lastpulled = revnum
+ if revnum < self.firstpulled or not self.firstpulled:
+ self.firstpulled = revnum
dict.__setitem__(self, (revnum, branch), ha)
if self._hashes is not None:
self._hashes[ha] = (revnum, branch)
+ @classmethod
+ def _wrapitermethods(cls):
+ def wrap(orig):
+ def wrapper(self, *args, **kwds):
+ if not self._allowiter:
+ raise NotImplementedError(
+ 'Iteration methods on RevMap are disabled ' +
+ 'to avoid performance issues on SqliteRevMap')
+ return orig(self, *args, **kwds)
+ return wrapper
+ methodre = re.compile(r'^_*(?:iter|view)?(?:keys|items|values)?_*$')
+ for name in filter(methodre.match, dir(cls)):
+ orig = getattr(cls, name)
+ setattr(cls, '_orig%s' % name, orig)
+ setattr(cls, name, wrap(orig))
+
+RevMap._wrapitermethods()
+
+
+class SqliteRevMap(collections.MutableMapping):
+ """RevMap backed by sqlite3.
+
+ It tries to address performance issues for a very large rev map.
+ As such iteration is unavailable for both the map itself and the
+ reverse map (self.hashes).
+
+ It migrates from the old RevMap upon first use. Then it will bump the
+ version of revmap so RevMap no longer works. The real database is a
+ separated file which has a ".db" suffix.
+ """
+
+ VERSION = 2
+
+ TABLESCHEMA = [
+ '''CREATE TABLE IF NOT EXISTS revmap (
+ rev INTEGER NOT NULL,
+ branch TEXT NOT NULL DEFAULT '',
+ hash BLOB NOT NULL)''',
+ ]
+
+ INDEXSCHEMA = [
+ 'CREATE UNIQUE INDEX IF NOT EXISTS revbranch ON revmap (rev,branch);',
+ 'CREATE INDEX IF NOT EXISTS hash ON revmap (hash);',
+ ]
+
+ # "bytes" in Python 2 will get truncated at '\0' when storing as sqlite
+ # blobs. "buffer" does not have this issue. Python 3 does not have "buffer"
+ # but "bytes" won't get truncated.
+ sqlblobtype = bytes if sys.version_info >= (3, 0) else buffer
+
+ class ReverseRevMap(object):
+ # collections.Mapping is not suitable since we don't want 2/3 of
+ # its required interfaces: __iter__, __len__.
+ def __init__(self, revmap):
+ self.revmap = weakref.proxy(revmap)
+ self._cache = {}
+
+ def get(self, key, default=None):
+ if key not in self._cache:
+ result = None
+ for row in self.revmap._query(
+ 'SELECT rev, branch FROM revmap WHERE hash=?',
+ (SqliteRevMap.sqlblobtype(key),)):
+ result = (row[0], row[1] or None)
+ break
+ self._cache[key] = result
+ return self._cache[key] or default
+
+ def __contains__(self, key):
+ return self.get(key) != None
+
+ def __getitem__(self, key):
+ dummy = self._cache
+ item = self.get(key, dummy)
+ if item == dummy:
+ raise KeyError(key)
+ else:
+ return item
+
+ def keys(self):
+ for row in self.revmap._query('SELECT hash FROM revmap'):
+ yield bytes(row[0])
+
+ lastpulled = util.fileproperty('_lastpulled', lambda x: x._lastpulledpath,
+ default=0, deserializer=int)
+ rowcount = util.fileproperty('_rowcount', lambda x: x._rowcountpath,
+ default=0, deserializer=int)
+
+ def __init__(self, revmap_path, lastpulled_path, sqlitepragmas=None):
+ self._filepath = revmap_path
+ self._dbpath = revmap_path + '.db'
+ self._rowcountpath = self._dbpath + '.rowcount'
+ self._lastpulledpath = lastpulled_path
+
+ self._db = None
+ self._hashes = None
+ self._sqlitepragmas = sqlitepragmas
+ self.firstpulled = 0
+ self._updatefirstlastpulled()
+ # __iter__ is expensive and thus disabled by default
+ # it should only be enabled for testing
+ self._allowiter = False
+
+ def hashes(self):
+ if self._hashes is None:
+ self._hashes = self.ReverseRevMap(self)
+ return self._hashes
+
+ def branchedits(self, branch, revnum):
+ return [((r[0], r[1] or None), bytes(r[2])) for r in
+ self._query('SELECT rev, branch, hash FROM revmap ' +
+ 'WHERE rev < ? AND branch = ? ' +
+ 'ORDER BY rev DESC, branch DESC',
+ (revnum, branch or ''))]
+
+ def branchmaxrevnum(self, branch, maxrev):
+ for row in self._query('SELECT rev FROM revmap ' +
+ 'WHERE rev <= ? AND branch = ? ' +
+ 'ORDER By rev DESC LIMIT 1',
+ (maxrev, branch or '')):
+ return row[0]
+ return 0
+
+ @property
+ def lasthash(self):
+ for row in self._query('SELECT hash FROM revmap ORDER BY rev DESC'):
+ return bytes(row[0])
+ return None
+
+ def revhashes(self, revnum):
+ for row in self._query('SELECT hash FROM revmap WHERE rev = ?',
+ (revnum,)):
+ yield bytes(row[0])
+
+ def clear(self):
+ hgutil.unlinkpath(self._filepath, ignoremissing=True)
+ hgutil.unlinkpath(self._dbpath, ignoremissing=True)
+ hgutil.unlinkpath(self._rowcountpath, ignoremissing=True)
+ self._db = None
+ self._hashes = None
+ self._firstpull = None
+ self._lastpull = None
+
+ def batchset(self, items, lastpulled):
+ with self._transaction():
+ self._insert(items)
+ self.lastpulled = lastpulled
+
+ def __getitem__(self, key):
+ for row in self._querybykey('SELECT hash', key):
+ return bytes(row[0])
+ raise KeyError(key)
+
+ def __iter__(self):
+ if not self._allowiter:
+ raise NotImplementedError(
+ 'SqliteRevMap.__iter__ is not implemented intentionally ' +
+ 'to avoid performance issues')
+ # collect result to avoid nested transaction issues
+ rows = []
+ for row in self._query('SELECT rev, branch FROM revmap'):
+ rows.append((row[0], row[1] or None))
+ return iter(rows)
+
+ def __len__(self):
+ # rowcount is faster than "SELECT COUNT(1)". the latter is not O(1)
+ return self.rowcount
+
+ def __setitem__(self, key, binha):
+ revnum, branch = key
+ with self._transaction():
+ self._insert([(revnum, branch, binha)])
+ if revnum < self.firstpulled or not self.firstpulled:
+ self.firstpulled = revnum
+ if revnum > self.lastpulled or not self.lastpulled:
+ self.lastpulled = revnum
+ if self._hashes is not None:
+ self._hashes._cache[binha] = key
+
+ def __delitem__(self, key):
+ for row in self._querybykey('DELETE', key):
+ if self.rowcount > 0:
+ self.rowcount -= 1
+ return
+ # For performance reason, self._hashes is not updated
+ raise KeyError(key)
+
+ @contextlib.contextmanager
+ def _transaction(self, mode='IMMEDIATE'):
+ if self._db is None:
+ self._opendb()
+ with self._db as db:
+ # wait indefinitely for database lock
+ while True:
+ try:
+ db.execute('BEGIN %s' % mode)
+ break
+ except sqlite3.OperationalError as ex:
+ if str(ex) != 'database is locked':
+ raise
+ yield db
+
+ def _query(self, sql, params=()):
+ with self._transaction() as db:
+ cur = db.execute(sql, params)
+ try:
+ for row in cur:
+ yield row
+ finally:
+ cur.close()
+
+ def _querybykey(self, prefix, key):
+ revnum, branch = key
+ return self._query(
+ '%s FROM revmap WHERE rev=? AND branch=?'
+ % prefix, (revnum, branch or ''))
+
+ def _insert(self, rows):
+ # convert to a safe type so '\0' does not truncate the blob
+ if rows and type(rows[0][-1]) is not self.sqlblobtype:
+ rows = [(r, b, self.sqlblobtype(h)) for r, b, h in rows]
+ self._db.executemany(
+ 'INSERT OR REPLACE INTO revmap (rev, branch, hash) ' +
+ 'VALUES (?, ?, ?)', rows)
+ # If REPLACE happens, rowcount can be wrong. But it is only used to
+ # calculate how many revisions pulled, and during pull we don't
+ # replace rows. So it is fine.
+ self.rowcount += len(rows)
+
+ def _opendb(self):
+ '''Open the database and make sure the table is created on demand.'''
+ version = None
+ try:
+ version = int(open(self._filepath).read(2))
+ except (ValueError, IOError):
+ pass
+ if version and version not in [RevMap.VERSION, self.VERSION]:
+ raise error.Abort('revmap too new -- please upgrade')
+
+ if self._db:
+ self._db.close()
+
+ # if version mismatch, the database is considered invalid
+ if version != self.VERSION:
+ hgutil.unlinkpath(self._dbpath, ignoremissing=True)
+
+ self._db = sqlite3.connect(self._dbpath)
+ self._db.text_factory = bytes
+
+ # cache size affects random accessing (e.g. index building)
+ # performance greatly. default is 2MB (2000 KB), we want to have
+ # a big enough cache that can hold the entire map.
+ cachesize = 2000
+ for path, ratio in [(self._filepath, 1.7), (self._dbpath, 1)]:
+ if os.path.exists(path):
+ cachesize += os.stat(path).st_size * ratio // 1000
+ self._db.execute('PRAGMA cache_size=%d' % (-cachesize))
+
+ # PRAGMA statements provided by the user
+ for pragma in (self._sqlitepragmas or []):
+ # drop malicious ones
+ if re.match(r'\A\w+=\w+\Z', pragma):
+ self._db.execute('PRAGMA %s' % pragma)
+
+ # disable auto-commit. everything is inside a transaction
+ self._db.isolation_level = 'DEFERRED'
+
+ with self._transaction('EXCLUSIVE'):
+ map(self._db.execute, self.TABLESCHEMA)
+ if version == RevMap.VERSION:
+ self.rowcount = 0
+ self._importrevmapv1()
+ elif not self.rowcount:
+ self.rowcount = self._db.execute(
+ 'SELECT COUNT(1) FROM revmap').fetchone()[0]
+
+ # "bulk insert; then create index" is about 2.4x as fast as
+ # "create index; then bulk insert" on a large repo
+ map(self._db.execute, self.INDEXSCHEMA)
+
+ # write a dummy rev map file with just the revision number
+ if version != self.VERSION:
+ f = open(self._filepath, 'w')
+ f.write('%s\n' % self.VERSION)
+ f.close()
+
+ def _updatefirstlastpulled(self):
+ sql = 'SELECT rev FROM revmap ORDER BY rev %s LIMIT 1'
+ for row in self._query(sql % 'ASC'):
+ self.firstpulled = row[0]
+ for row in self._query(sql % 'DESC'):
+ if row[0] > self.lastpulled:
+ self.lastpulled = row[0]
+
+ @util.gcdisable
+ def _importrevmapv1(self):
+ with open(self._filepath, 'r') as f:
+ # 1st line is version
+ assert(int(f.readline())) == RevMap.VERSION
+ data = {}
+ for line in f:
+ revnum, ha, branch = line[:-1].split(' ', 2)
+ # ignore malicious lines
+ if len(ha) != 40:
+ continue
+ data[revnum, branch or None] = bin(ha)
+ self._insert([(r, b, h) for (r, b), h in data.iteritems()])
+
+ @util.gcdisable
+ def exportrevmapv1(self, path):
+ with open(path, 'w') as f:
+ f.write('%s\n' % RevMap.VERSION)
+ for row in self._query('SELECT rev, branch, hash FROM revmap'):
+ rev, br, ha = row
+ f.write('%s %s %s\n' % (rev, hex(ha), br))
+
class FileMap(object):
VERSION = 1
- def __init__(self, meta):
+ def __init__(self, ui, filepath):
'''Initialise a new FileMap.
The ui argument is used to print diagnostic messages.
@@ -275,16 +799,17 @@ class FileMap(object):
The path argument is the location of the backing store,
typically .hg/svn/filemap.
'''
- self.meta = meta
+ self._filename = filepath
+ self._ui = ui
self.include = {}
self.exclude = {}
- if os.path.isfile(self.meta.filemap_file):
+ if os.path.isfile(self._filename):
self._load()
else:
self._write()
# append file mapping specified from the commandline
- clmap = util.configpath(self.meta.ui, 'filemap')
+ clmap = util.configpath(self._ui, 'filemap')
if clmap:
self.load(clmap)
@@ -326,22 +851,20 @@ class FileMap(object):
mapping = getattr(self, m)
if path in mapping:
msg = 'duplicate %s entry in %s: "%s"\n'
- self.meta.ui.status(msg % (m, fn, path))
+ self._ui.status(msg % (m, fn, path))
return
bits = m.rstrip('e'), path
- self.meta.ui.debug('%sing %s\n' % bits)
+ self._ui.debug('%sing %s\n' % bits)
# respect rule order
mapping[path] = len(self)
- if fn != self.meta.filemap_file:
- f = open(self.meta.filemap_file, 'a')
- f.write(m + ' ' + path + '\n')
- f.close()
+ if fn != self._filename:
+ with open(self._filename, 'a') as f:
+ f.write(m + ' ' + path + '\n')
def load(self, fn):
- self.meta.ui.debug('reading file map from %s\n' % fn)
- f = open(fn, 'r')
- self.load_fd(f, fn)
- f.close()
+ self._ui.debug('reading file map from %s\n' % fn)
+ with open(fn, 'r') as f:
+ self.load_fd(f, fn)
def load_fd(self, f, fn):
for line in f:
@@ -354,26 +877,24 @@ class FileMap(object):
if cmd in ('include', 'exclude'):
self.add(fn, cmd, path)
continue
- self.meta.ui.warn('unknown filemap command %s\n' % cmd)
+ self._ui.warn('unknown filemap command %s\n' % cmd)
except IndexError:
msg = 'ignoring bad line in filemap %s: %s\n'
- self.meta.ui.warn(msg % (fn, line.rstrip()))
+ self._ui.warn(msg % (fn, line.rstrip()))
def _load(self):
- self.meta.ui.debug('reading in-repo file map from %s\n' % self.meta.filemap_file)
- f = open(self.meta.filemap_file)
- ver = int(f.readline())
- if ver != self.VERSION:
- raise hgutil.Abort('filemap too new -- please upgrade')
- self.load_fd(f, self.meta.filemap_file)
- f.close()
+ self._ui.debug('reading in-repo file map from %s\n' % self._filename)
+ with open(self._filename) as f:
+ ver = int(f.readline())
+ if ver != self.VERSION:
+ raise hgutil.Abort('filemap too new -- please upgrade')
+ self.load_fd(f, self._filename)
def _write(self):
- f = open(self.meta.filemap_file, 'w')
- f.write('%s\n' % self.VERSION)
- f.close()
+ with open(self._filename, 'w') as f:
+ f.write('%s\n' % self.VERSION)
-class BranchMap(dict):
+class BranchMap(BaseMap):
'''Facility for controlled renaming of branch names. Example:
oldname = newname
@@ -383,63 +904,7 @@ class BranchMap(dict):
changes on other will now be on default (have no branch name set).
'''
- def __init__(self, meta):
- self.meta = meta
- self.super = super(BranchMap, self)
- self.super.__init__()
- self.load(self.meta.branchmap_file)
-
- # append branch mapping specified from the commandline
- clmap = util.configpath(self.meta.ui, 'branchmap')
- if clmap:
- self.load(clmap)
-
- def load(self, path):
- '''Load mappings from a file at the specified path.'''
- if not os.path.exists(path):
- return
-
- writing = False
- if path != self.meta.branchmap_file:
- writing = open(self.meta.branchmap_file, 'a')
-
- self.meta.ui.debug('reading branchmap from %s\n' % path)
- f = open(path, 'r')
- for number, line in enumerate(f):
-
- if writing:
- writing.write(line)
-
- line = line.split('#')[0]
- if not line.strip():
- continue
-
- try:
- src, dst = line.split('=', 1)
- except (IndexError, ValueError):
- msg = 'ignoring line %i in branch map %s: %s\n'
- self.meta.ui.status(msg % (number, path, line.rstrip()))
- continue
-
- src = src.strip()
- dst = dst.strip()
- self.meta.ui.debug('adding branch %s to branch map\n' % src)
-
- if not dst:
- # prevent people from assuming such lines are valid
- raise hgutil.Abort('removing branches is not supported, yet\n'
- '(line %i in branch map %s)'
- % (number, path))
- elif src in self and dst != self[src]:
- msg = 'overriding branch: "%s" to "%s" (%s)\n'
- self.meta.ui.status(msg % (self[src], dst, src))
- self[src] = dst
-
- f.close()
- if writing:
- writing.close()
-
-class TagMap(dict):
+class TagMap(BaseMap):
'''Facility for controlled renaming of tags. Example:
oldname = newname
@@ -448,54 +913,3 @@ class TagMap(dict):
The oldname tag from SVN will be represented as newname in the hg tags;
the other tag will not be reflected in the hg repository.
'''
-
- def __init__(self, meta):
- self.meta = meta
- self.super = super(TagMap, self)
- self.super.__init__()
- self.load(self.meta.tagmap_file)
-
- # append tag mapping specified from the commandline
- clmap = util.configpath(self.meta.ui, 'tagmap')
- if clmap:
- self.load(clmap)
-
- def load(self, path):
- '''Load mappings from a file at the specified path.'''
- if not os.path.exists(path):
- return
-
- writing = False
- if path != self.meta.tagmap_file:
- writing = open(self.meta.tagmap_file, 'a')
-
- self.meta.ui.debug('reading tag renames from %s\n' % path)
- f = open(path, 'r')
- for number, line in enumerate(f):
-
- if writing:
- writing.write(line)
-
- line = line.split('#')[0]
- if not line.strip():
- continue
-
- try:
- src, dst = line.split('=', 1)
- except (IndexError, ValueError):
- msg = 'ignoring line %i in tag renames %s: %s\n'
- self.meta.ui.status(msg % (number, path, line.rstrip()))
- continue
-
- src = src.strip()
- dst = dst.strip()
- self.meta.ui.debug('adding tag %s to tag renames\n' % src)
-
- if src in self and dst != self[src]:
- msg = 'overriding tag rename: "%s" to "%s" (%s)\n'
- self.meta.ui.status(msg % (self[src], dst, src))
- self[src] = dst
-
- f.close()
- if writing:
- writing.close()
diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py
index ee754ce..777fee8 100644
--- a/hgsubversion/replay.py
+++ b/hgsubversion/replay.py
@@ -65,13 +65,13 @@ def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
editor.current.rev = r
editor.setsvn(svn)
- if firstrun and meta.firstpulled <= 0:
+ if firstrun and meta.revmap.firstpulled <= 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)
else:
- svn.get_replay(r.revnum, editor, meta.firstpulled)
+ svn.get_replay(r.revnum, editor, meta.revmap.firstpulled)
editor.close()
current = editor.current
@@ -103,7 +103,7 @@ def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
closebranches = {}
for branch in tbdelta['branches'][1]:
- branchedits = meta.revmap.branchedits(branch, rev)
+ branchedits = meta.revmap.branchedits(branch, rev.revnum)
if len(branchedits) < 1:
# can't close a branch that never existed
continue
@@ -121,6 +121,12 @@ def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
if branch in current.emptybranches and files:
del current.emptybranches[branch]
+ if meta.skipbranch(branch):
+ # make sure we also get rid of it from emptybranches
+ if branch in current.emptybranches:
+ del current.emptybranches[branch]
+ continue
+
files = dict(files)
parents = meta.get_parent_revision(rev.revnum, branch), revlog.nullid
if parents[0] in closedrevs and branch in meta.closebranches:
@@ -175,6 +181,8 @@ def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
copied=copied)
meta.mapbranch(extra)
+ if 'branch' not in extra:
+ extra['branch'] = 'default'
current_ctx = context.memctx(meta.repo,
parents,
meta.getmessage(rev),
@@ -195,6 +203,9 @@ def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
# 2. handle branches that need to be committed without any files
for branch in current.emptybranches:
+ if meta.skipbranch(branch):
+ continue
+
ha = meta.get_parent_revision(rev.revnum, branch)
if ha == node.nullid:
continue
diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py
index abb38e7..9c004c0 100644
--- a/hgsubversion/stupid.py
+++ b/hgsubversion/stupid.py
@@ -568,7 +568,7 @@ def fetch_branchrev(svn, meta, branch, branchpath, r, parentctx):
return files, filectxfn
def checkbranch(meta, r, branch):
- branchedits = meta.revmap.branchedits(branch, r)
+ branchedits = meta.revmap.branchedits(branch, r.revnum)
if not branchedits:
return None
branchtip = branchedits[0][1]
@@ -689,6 +689,10 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
date = meta.fixdate(r.date)
check_deleted_branches = set(tbdelta['branches'][1])
for b in branches:
+
+ if meta.skipbranch(b):
+ continue
+
parentctx = meta.repo[meta.get_parent_revision(r.revnum, b)]
tag = meta.get_path_tag(meta.remotename(b))
kind = svn.checkpath(branches[b], r.revnum)
@@ -704,7 +708,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
# path does not support this case with svn >= 1.7. We can fix
# it, or we can force the existing fetch_branchrev() path. Do
# the latter for now.
- incremental = (meta.firstpulled > 0 and
+ incremental = (meta.revmap.firstpulled > 0 and
parentctx.rev() != node.nullrev and
not firstrun)
diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py
index abd93c7..ba3cb22 100644
--- a/hgsubversion/svncommands.py
+++ b/hgsubversion/svncommands.py
@@ -64,8 +64,12 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
youngest = 0
startrev = 0
- sofar = []
branchinfo = {}
+
+ if not partial:
+ hgutil.unlinkpath(meta.revmap_file, ignoremissing=True)
+
+ revmap = meta.revmap
if partial:
try:
# we can't use meta.lastpulled here because we are bootstraping the
@@ -75,9 +79,8 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
youngestpath = os.path.join(meta.metapath, 'lastpulled')
if os.path.exists(youngestpath):
youngest = util.load(youngestpath)
- sofar = list(maps.RevMap.readmapfile(meta.revmap_file))
- if sofar and len(sofar[-1].split(' ', 2)) > 1:
- lasthash = sofar[-1].split(' ', 2)[1]
+ lasthash = revmap.lasthash
+ if len(revmap) > 0 and lasthash:
startrev = repo[lasthash].rev() + 1
branchinfo = util.load(meta.branch_info_file)
foundpartialinfo = True
@@ -91,9 +94,9 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
except AttributeError:
ui.status('no metadata available -- doing a full rebuild\n')
- revmap = open(meta.revmap_file, 'w')
- revmap.write('%d\n' % maps.RevMap.VERSION)
- revmap.writelines(sofar)
+ if not partial:
+ revmap.clear()
+
last_rev = -1
if not partial and os.path.exists(meta.tagfile):
os.unlink(meta.tagfile)
@@ -107,13 +110,8 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
# it would make us use O(revisions^2) time, so we perform an extra traversal
# of the repository instead. During this traversal, we find all converted
# changesets that close a branch, and store their first parent
- for rev in xrange(startrev, len(repo)):
- ui.progress('prepare', rev - startrev, total=numrevs)
- try:
- ctx = repo[rev]
- except error.RepoError:
- # this revision is hidden
- continue
+ for ctx in util.get_contexts(repo, startrev):
+ ui.progress('prepare', ctx.rev() - startrev, total=numrevs)
convinfo = util.getsvnrev(ctx, None)
if not convinfo:
@@ -137,16 +135,11 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
else:
closed.add(parentctx.rev())
- meta.lastpulled = youngest
ui.progress('prepare', None, total=numrevs)
- for rev in xrange(startrev, len(repo)):
- ui.progress('rebuild', rev-startrev, total=numrevs)
- try:
- ctx = repo[rev]
- except error.RepoError:
- # this revision is hidden
- continue
+ revmapbuf = []
+ for ctx in util.get_contexts(repo, startrev):
+ ui.progress('rebuild', ctx.rev() - startrev, total=numrevs)
convinfo = util.getsvnrev(ctx, None)
if not convinfo:
@@ -226,7 +219,7 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
continue
branch = meta.layoutobj.localname(commitpath)
- revmap.write('%s %s %s\n' % (revision, ctx.hex(), branch or ''))
+ revmapbuf.append((revision, branch, ctx.node()))
revision = int(revision)
if revision > last_rev:
@@ -254,7 +247,7 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
branch = meta.layoutobj.localname(parentpath)
break
- if rev in closed:
+ if ctx.rev() in closed:
# a direct child of this changeset closes the branch; drop it
branchinfo.pop(branch, None)
elif ctx.extra().get('close'):
@@ -276,6 +269,7 @@ def _buildmeta(ui, repo, args, partial=False, skipuuid=False):
int(parentrev),
revision)
+ revmap.batchset(revmapbuf, youngest)
ui.progress('rebuild', None, total=numrevs)
# save off branch info
@@ -347,7 +341,7 @@ def genignore(ui, repo, force=False, **opts):
raise error.RepoError("There is no Mercurial repository"
" here (.hg not found)")
- ignpath = repo.wjoin('.hgignore')
+ ignpath = repo.wvfs.join('.hgignore')
if not force and os.path.exists(ignpath):
raise hgutil.Abort('not overwriting existing .hgignore, try --force?')
svn = svnrepo.svnremoterepo(repo.ui).svn
@@ -369,7 +363,7 @@ def genignore(ui, repo, force=False, **opts):
lines = props['svn:ignore'].strip().split('\n')
ignorelines += [dir and (dir + '/' + prop) or prop for prop in lines if prop.strip()]
- repo.wopener('.hgignore', 'w').write('\n'.join(ignorelines) + '\n')
+ repo.wvfs('.hgignore', 'w').write('\n'.join(ignorelines) + '\n')
def info(ui, repo, **opts):
diff --git a/hgsubversion/svnexternals.py b/hgsubversion/svnexternals.py
index f738aa2..fc32e12 100644
--- a/hgsubversion/svnexternals.py
+++ b/hgsubversion/svnexternals.py
@@ -88,7 +88,7 @@ class BadDefinition(Exception):
pass
re_defold = re.compile(r'^\s*(.*?)\s+(?:-r\s*(\d+|\{REV\})\s+)?([a-zA-Z+]+://.*)\s*$')
-re_defnew = re.compile(r'^\s*(?:-r\s*(\d+|\{REV\})\s+)?((?:[a-zA-Z+]+://|\^/).*)\s+(\S+)\s*$')
+re_defnew = re.compile(r'^\s*(?:-r\s*(\d+|\{REV\})\s+)?((?:[a-zA-Z+]+://|\^/)\S*)\s+(\S+)\s*$')
re_scheme = re.compile(r'^[a-zA-Z+]+://')
def parsedefinition(line):
@@ -120,13 +120,84 @@ def parsedefinition(line):
class RelativeSourceError(Exception):
pass
+def resolvedots(url):
+ """
+ Fix references that include .. entries.
+ Scans a URL for .. type entries and resolves them but will not allow any
+ number of ..s to take us out of domain so http://.. will raise an exception.
+
+ Tests, (Don't know how to construct a round trip for this so doctest):
+ >>> # Relative URL within servers svn area
+ >>> resolvedots(
+ ... "http://some.svn.server/svn/some_repo/../other_repo")
+ 'http://some.svn.server/svn/other_repo'
+ >>> # Complex One
+ >>> resolvedots(
+ ... "http://some.svn.server/svn/repo/../other/repo/../../other_repo")
+ 'http://some.svn.server/svn/other_repo'
+ >>> # Another Complex One
+ >>> resolvedots(
+ ... "http://some.svn.server/svn/repo/dir/subdir/../../../other_repo/dir")
+ 'http://some.svn.server/svn/other_repo/dir'
+ >>> # Last Complex One - SVN Allows this & seen it used even if it is BAD!
+ >>> resolvedots(
+ ... "http://svn.server/svn/my_repo/dir/subdir/../../other_dir")
+ 'http://svn.server/svn/my_repo/other_dir'
+ >>> # Outside the SVN Area might be OK
+ >>> resolvedots(
+ ... "http://svn.server/svn/some_repo/../../other_svn_repo")
+ 'http://svn.server/other_svn_repo'
+ >>> # Complex One
+ >>> resolvedots(
+ ... "http://some.svn.server/svn/repo/../other/repo/../../other_repo")
+ 'http://some.svn.server/svn/other_repo'
+ >>> # On another server is not a relative URL should give an exception
+ >>> resolvedots(
+ ... "http://some.svn.server/svn/some_repo/../../../other_server")
+ Traceback (most recent call last):
+ ...
+ RelativeSourceError: Relative URL cannot be to another server
+ """
+ orig = url.split('/')
+ fixed = []
+ for item in orig:
+ if item != '..':
+ fixed.append(item)
+ elif len(fixed) > 3: # Don't allow things to go out of domain
+ fixed.pop()
+ else:
+ raise RelativeSourceError(
+ 'Relative URL cannot be to another server')
+ return '/'.join(fixed)
+
+
+
def resolvesource(ui, svnroot, source):
+ """ Resolve the source as either matching the scheme re or by resolving
+ relative URLs which start with ^ and my include relative .. references.
+
+ >>> root = 'http://some.svn.server/svn/some_repo'
+ >>> resolvesource(None, root, 'http://other.svn.server')
+ 'http://other.svn.server'
+ >>> resolvesource(None, root, 'ssh://other.svn.server')
+ 'ssh://other.svn.server'
+ >>> resolvesource(None, root, '^/other_repo')
+ 'http://some.svn.server/svn/some_repo/other_repo'
+ >>> resolvesource(None, root, '^/sub_repo')
+ 'http://some.svn.server/svn/some_repo/sub_repo'
+ >>> resolvesource(None, root, '^/../other_repo')
+ 'http://some.svn.server/svn/other_repo'
+ >>> resolvesource(None, root, '^/../../../server/other_repo')
+ Traceback (most recent call last):
+ ...
+ RelativeSourceError: Relative URL cannot be to another server
+ """
if re_scheme.search(source):
return source
if source.startswith('^/'):
if svnroot is None:
raise RelativeSourceError()
- return svnroot + source[1:]
+ return resolvedots(svnroot + source[1:])
ui.warn(_('ignoring unsupported non-fully qualified external: %r\n'
% source))
return None
@@ -218,7 +289,7 @@ class externalsupdater:
self.ui = ui
def update(self, wpath, rev, source, pegrev):
- path = self.repo.wjoin(wpath)
+ path = self.repo.wvfs.join(wpath)
revspec = []
if rev:
revspec = ['-r', rev]
@@ -232,7 +303,7 @@ class externalsupdater:
if source == exturl:
if extrev != rev:
self.ui.status(_('updating external on %s@%s\n') %
- (wpath, rev or 'HEAD'))
+ (wpath, rev or pegrev or 'HEAD'))
cwd = os.path.join(self.repo.root, path)
self.svn(['update'] + revspec, cwd)
return
@@ -245,11 +316,12 @@ class externalsupdater:
pegrev = rev
if pegrev:
source = '%s@%s' % (source, pegrev)
- self.ui.status(_('fetching external %s@%s\n') % (wpath, rev or 'HEAD'))
+ self.ui.status(_('fetching external %s@%s\n') %
+ (wpath, rev or pegrev or 'HEAD'))
self.svn(['co'] + revspec + [source, dest], cwd)
def delete(self, wpath):
- path = self.repo.wjoin(wpath)
+ path = self.repo.wvfs.join(wpath)
if os.path.isdir(path):
self.ui.status(_('removing external %s\n') % wpath)
@@ -268,12 +340,12 @@ class externalsupdater:
def svn(self, args, cwd):
args = ['svn'] + args
- self.ui.debug(_('updating externals: %r, cwd=%s\n') % (args, cwd))
+ self.ui.note(_('updating externals: %r, cwd=%s\n') % (args, cwd))
shell = os.name == 'nt'
p = subprocess.Popen(args, cwd=cwd, shell=shell,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
for line in p.stdout:
- self.ui.note(line)
+ self.ui.debug(line)
p.wait()
if p.returncode != 0:
raise hgutil.Abort("subprocess '%s' failed" % ' '.join(args))
@@ -296,7 +368,7 @@ def updateexternals(ui, args, repo, **opts):
# Retrieve current externals status
try:
- oldext = file(repo.join('svn/externals'), 'rb').read()
+ oldext = file(repo.vfs.join('svn/externals'), 'rb').read()
except IOError:
oldext = ''
newext = ''
@@ -314,7 +386,7 @@ def updateexternals(ui, args, repo, **opts):
else:
raise hgutil.Abort(_('unknown update actions: %r') % action)
- file(repo.join('svn/externals'), 'wb').write(newext)
+ file(repo.vfs.join('svn/externals'), 'wb').write(newext)
def getchanges(ui, repo, parentctx, exts):
"""Take a parent changectx and the new externals definitions as an
@@ -421,18 +493,21 @@ class svnsubrepo(subrepo.svnsubrepo):
state = (source, state[1])
return super(svnsubrepo, self).get(state, *args, **kwargs)
- def dirty(self, ignoreupdate=False):
+ def dirty(self, ignoreupdate=False, missing=False):
# You cannot compare anything with HEAD. Just accept it
# can be anything.
- if hasattr(self, '_wcrevs'):
+ if hgutil.safehasattr(self, '_wcrevs'):
wcrevs = self._wcrevs()
else:
wcrev = self._wcrev()
wcrevs = (wcrev, wcrev)
- if (('HEAD' in wcrevs or self._state[1] == 'HEAD' or
- self._state[1] in wcrevs or ignoreupdate)
- and not self._wcchanged()[0]):
- return False
+ shouldcheck = ('HEAD' in wcrevs or self._state[1] == 'HEAD' or
+ self._state[1] in wcrevs or ignoreupdate or missing)
+ if shouldcheck:
+ changes, extchanges, wcmissing = self._wcchanged()
+ changed = changes or (missing and wcmissing)
+ if not changed:
+ return False
return True
def commit(self, text, user, date):
@@ -447,3 +522,7 @@ class svnsubrepo(subrepo.svnsubrepo):
if self._state[1] == 'HEAD':
return 'HEAD'
return super(svnsubrepo, self).basestate()
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py
index a7d652c..7f5a79a 100644
--- a/hgsubversion/svnmeta.py
+++ b/hgsubversion/svnmeta.py
@@ -26,8 +26,7 @@ class SVNMeta(object):
# simple and public variables
self.ui = repo.ui
self.repo = repo
- self.path = os.path.normpath(repo.join('..'))
- self.firstpulled = 0
+ self.path = os.path.normpath(repo.vfs.join('..'))
self.lastdate = '1970-01-01 00:00:00 -0000'
self.addedtags = {}
self.deletedtags = {}
@@ -52,9 +51,9 @@ class SVNMeta(object):
self.subdir = subdir
# generated properties that have a persistent file stored on disk
- self._gen_cachedconfig('lastpulled', 0, configname=False)
self._gen_cachedconfig('defaultauthors', True)
self._gen_cachedconfig('caseignoreauthors', False)
+ self._gen_cachedconfig('mapauthorscmd', None)
self._gen_cachedconfig('defaulthost', self.uuid)
self._gen_cachedconfig('usebranchnames', True)
self._gen_cachedconfig('defaultmessage', '')
@@ -69,7 +68,7 @@ class SVNMeta(object):
"""Return a cached value for a config option. If the cache is uninitialized
then try to read its value from disk. Option can be overridden by the
commandline.
- name: property name, e.g. 'lastpulled'
+ name: property name, e.g. 'defaultauthors'
filename: name of file in .hg/svn
configname: commandline option name
default: default value
@@ -94,6 +93,8 @@ class SVNMeta(object):
c = self.ui.configint('hgsubversion', configname, default)
elif isinstance(default, list):
c = self.ui.configlist('hgsubversion', configname, default)
+ elif isinstance(default, dict):
+ c = dict(self.ui.configitems(configname))
else:
c = self.ui.config('hgsubversion', configname, default)
@@ -136,14 +137,14 @@ class SVNMeta(object):
filename = name
if configname is None:
configname = name
- prop = property(lambda x: self._get_cachedconfig(name,
- filename,
- configname,
- default,
- pre=pre),
- lambda x, y: self._set_cachedconfig(y,
- name,
- filename))
+ prop = property(lambda x: x._get_cachedconfig(name,
+ filename,
+ configname,
+ default,
+ pre=pre),
+ lambda x, y: x._set_cachedconfig(y,
+ name,
+ filename))
setattr(SVNMeta, name, prop)
def layout_from_subversion(self, svn, revision=None):
@@ -218,7 +219,7 @@ class SVNMeta(object):
@property
def editor(self):
- if not hasattr(self, '_editor'):
+ if not hgutil.safehasattr(self, '_editor'):
self._editor = editor.HgEditor(self)
return self._editor
@@ -284,13 +285,15 @@ class SVNMeta(object):
return os.path.join(self.metapath, 'branch_info')
@property
- def authors_file(self):
+ def authormap_file(self):
return os.path.join(self.metapath, 'authors')
@property
def authors(self):
if self._authors is None:
- self._authors = maps.AuthorMap(self)
+ self._authors = maps.AuthorMap(
+ self.ui, self.authormap_file, self.defaulthost,
+ self.caseignoreauthors, self.mapauthorscmd, self.defaultauthors)
return self._authors
@property
@@ -300,7 +303,7 @@ class SVNMeta(object):
@property
def filemap(self):
if self._filemap is None:
- self._filemap = maps.FileMap(self)
+ self._filemap = maps.FileMap(self.ui, self.filemap_file)
return self._filemap
@property
@@ -310,7 +313,7 @@ class SVNMeta(object):
@property
def branchmap(self):
if self._branchmap is None:
- self._branchmap = maps.BranchMap(self)
+ self._branchmap = maps.BranchMap(self.ui, self.branchmap_file)
return self._branchmap
@property
@@ -321,7 +324,7 @@ class SVNMeta(object):
@property
def tags(self):
if self._tags is None:
- self._tags = maps.Tags(self)
+ self._tags = maps.Tags(self.ui, self.tagfile)
return self._tags
@property
@@ -332,7 +335,7 @@ class SVNMeta(object):
@property
def tagmap(self):
if self._tagmap is None:
- self._tagmap = maps.TagMap(self)
+ self._tagmap = maps.TagMap(self.ui, self.tagmap_file)
return self._tagmap
@property
@@ -342,9 +345,34 @@ class SVNMeta(object):
@property
def revmap(self):
if self._revmap is None:
- self._revmap = maps.RevMap(self)
+ lastpulled_path = os.path.join(self.metapath, 'lastpulled')
+ opts = {}
+ if self.revmapclass is maps.SqliteRevMap:
+ # sqlite revmap takes an optional option: sqlitepragmas
+ opts['sqlitepragmas'] = self.ui.configlist(
+ 'hgsubversion', 'sqlitepragmas')
+ self._revmap = self.revmapclass(
+ self.revmap_file, lastpulled_path, **opts)
return self._revmap
+ @property
+ def revmapexists(self):
+ return os.path.exists(self.revmap_file)
+
+ _defaultrevmapclass = maps.RevMap
+
+ @property
+ def revmapclass(self):
+ impl = self.ui.config('hgsubversion', 'revmapimpl')
+ if impl == 'plain':
+ return maps.RevMap
+ elif impl == 'sqlite':
+ return maps.SqliteRevMap
+ elif impl is None:
+ return self._defaultrevmapclass
+ else:
+ raise hgutil.Abort('unknown revmapimpl: %s' % impl)
+
def fixdate(self, date):
if date is not None:
date = date.replace('T', ' ').replace('Z', '').split('.')[0]
@@ -388,6 +416,19 @@ class SVNMeta(object):
}
return extra
+ def skipbranch(self, name):
+ '''Returns whether or not we're skipping a branch.'''
+ # sometimes it's easier to pass the path instead of just the branch
+ # name, so we test for that here
+ if name:
+ bname = self.split_branch_path(name)
+ if bname != (None, None, None):
+ name = bname[1]
+
+ # if the mapped branch == '' and the original branch name == '' then we
+ # won't commit this branch
+ return name and not self.branchmap.get(name, True)
+
def mapbranch(self, extra, close=False):
if close:
extra['close'] = 1
@@ -440,6 +481,13 @@ class SVNMeta(object):
path = self.normalize(path)
return self.layoutobj.get_path_tag(path, self.layoutobj.taglocations)
+ def get_tag_path(self, name):
+ """Return a path corresponding to the given tag name"""
+ try:
+ return self.layoutobj.taglocations[0] + '/' + name
+ except IndexError:
+ return None
+
def split_branch_path(self, path, existing=True):
"""Figure out which branch inside our repo this path represents, and
also figure out which path inside that branch it is.
@@ -531,12 +579,7 @@ class SVNMeta(object):
"""
if (number, branch) in self.revmap:
return number, branch
- real_num = 0
- for num, br in self.revmap.iterkeys():
- if br != branch:
- continue
- if num <= number and num > real_num:
- real_num = num
+ real_num = self.revmap.branchmaxrevnum(branch, number)
if branch in self.branches:
parent_branch = self.branches[branch][0]
parent_branch_rev = self.branches[branch][1]
@@ -576,7 +619,7 @@ class SVNMeta(object):
return node.hex(self.revmap[tagged])
tag = fromtag
# Reference an existing tag
- limitedtags = maps.Tags(self, endrev=number - 1)
+ limitedtags = maps.Tags(self.ui, self.tagfile, endrev=number - 1)
if tag in limitedtags:
return limitedtags[tag]
r, br = self.get_parent_svn_branch_and_rev(number - 1, branch, exact)
diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py
index 1f8f817..1a962b3 100644
--- a/hgsubversion/svnrepo.py
+++ b/hgsubversion/svnrepo.py
@@ -157,7 +157,7 @@ class svnremoterepo(peerrepository):
@property
def svnurl(self):
- return self.svn.svn_url
+ return self.svnauth[0]
@propertycache
def svn(self):
diff --git a/hgsubversion/svnwrap/common.py b/hgsubversion/svnwrap/common.py
index 7c59986..ebcae14 100644
--- a/hgsubversion/svnwrap/common.py
+++ b/hgsubversion/svnwrap/common.py
@@ -55,7 +55,8 @@ class Revision(tuple):
_paths = {}
if paths:
for p in paths:
- _paths[p[len(strip_path):]] = paths[p]
+ if p.startswith(strip_path):
+ _paths[p[len(strip_path):]] = paths[p]
return tuple.__new__(self,
(revnum, author, message, date, _paths))
diff --git a/hgsubversion/svnwrap/subvertpy_wrapper.py b/hgsubversion/svnwrap/subvertpy_wrapper.py
index 03a167e..35dc450 100644
--- a/hgsubversion/svnwrap/subvertpy_wrapper.py
+++ b/hgsubversion/svnwrap/subvertpy_wrapper.py
@@ -22,6 +22,7 @@ try:
from subvertpy import delta
from subvertpy import properties
from subvertpy import ra
+ from subvertpy import repos
import subvertpy
except ImportError:
raise ImportError('Subvertpy %d.%d.%d or later required, but not found'
@@ -51,6 +52,18 @@ def version():
svnvers += '-' + subversion_version[3]
return (svnvers, 'Subvertpy ' + _versionstr(subvertpy.__version__))
+def create_and_load(repopath, dumpfd):
+ ''' create a new repository at repopath and load the given dump into it '''
+ repo = repos.create(repopath)
+
+ nullfd = open(os.devnull, 'w')
+
+ try:
+ repo.load_fs(dumpfd, nullfd, repos.LOAD_UUID_FORCE)
+ finally:
+ dumpfd.close()
+ nullfd.close()
+
# exported values
ERR_FS_ALREADY_EXISTS = subvertpy.ERR_FS_ALREADY_EXISTS
ERR_FS_CONFLICT = subvertpy.ERR_FS_CONFLICT
@@ -186,7 +199,8 @@ class SubversionRepo(object):
Note that password stores do not work, the parameter is only here
to ensure that the API is the same as for the SWIG wrapper.
"""
- def __init__(self, url='', username='', password='', head=None, password_stores=None):
+ def __init__(self, url='', username='', password='', head=None,
+ password_stores=None):
parsed = common.parse_url(url, username, password)
# --username and --password override URL credentials
self.username = parsed[0]
@@ -450,11 +464,17 @@ class SubversionRepo(object):
else:
# visiting a directory
- if path in addeddirs:
- direditor = editor.add_directory(path)
- elif path in deleteddirs:
+ if path in deleteddirs:
direditor = editor.delete_entry(path, base_revision)
- continue
+
+ if path not in addeddirs:
+ continue
+
+ if path in addeddirs:
+ frompath, fromrev = copies.get(path, (None, -1))
+ if frompath:
+ frompath = self.path2url(frompath)
+ direditor = editor.add_directory(path, frompath, fromrev)
else:
direditor = editor.open_directory(path)
diff --git a/hgsubversion/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py
index ccc18c0..3455b34 100644
--- a/hgsubversion/svnwrap/svn_swig_wrapper.py
+++ b/hgsubversion/svnwrap/svn_swig_wrapper.py
@@ -21,6 +21,7 @@ try:
from svn import core
from svn import delta
from svn import ra
+ from svn import repos
subversion_version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR,
core.SVN_VER_MICRO)
@@ -36,6 +37,21 @@ if subversion_version < required_bindings: # pragma: no cover
def version():
return '%d.%d.%d' % subversion_version, 'SWIG'
+def create_and_load(repopath, dumpfd):
+ ''' create a new repository at repopath and load the given dump into it '''
+ pool = core.Pool()
+ r = repos.svn_repos_create(repopath, '', '', None, None, pool)
+
+ try:
+ repos.svn_repos_load_fs2(r, dumpfd, None,
+ repos.svn_repos_load_uuid_force,
+ '', False, False, None, pool)
+ finally:
+ dumpfd.close()
+
+ pool.destroy()
+
+
# exported values
ERR_FS_ALREADY_EXISTS = core.SVN_ERR_FS_ALREADY_EXISTS
ERR_FS_CONFLICT = core.SVN_ERR_FS_CONFLICT
@@ -170,7 +186,7 @@ def _create_auth_baton(pool, password_stores):
providers.append(p)
else:
for p in platform_specific:
- if hasattr(core, p):
+ if getattr(core, p, None) is not None:
try:
providers.append(getattr(core, p)())
except RuntimeError:
@@ -205,7 +221,8 @@ class SubversionRepo(object):
It uses the SWIG Python bindings, see above for requirements.
"""
- def __init__(self, url='', username='', password='', head=None, password_stores=None):
+ def __init__(self, url='', username='', password='', head=None,
+ password_stores=None):
parsed = common.parse_url(url, username, password)
# --username and --password override URL credentials
self.username = parsed[0]
@@ -400,10 +417,16 @@ class SubversionRepo(object):
if path in deleteddirs:
bat = editor.delete_entry(path, base_revision, parent, pool)
batons.append(bat)
- return bat
+
+ if path not in addeddirs:
+ return bat
+
if path not in file_data:
if path in addeddirs:
- bat = editor.add_directory(path, parent, None, -1, pool)
+ frompath, fromrev = copies.get(path, (None, -1))
+ if frompath:
+ frompath = self.path2url(frompath)
+ bat = editor.add_directory(path, parent, frompath, fromrev, pool)
else:
bat = editor.open_directory(path, parent, base_revision, pool)
batons.append(bat)
diff --git a/hgsubversion/util.py b/hgsubversion/util.py
index 0a157c2..94c97af 100644
--- a/hgsubversion/util.py
+++ b/hgsubversion/util.py
@@ -23,6 +23,12 @@ try:
except ImportError:
pass
+try:
+ from mercurial import smartset
+ smartset.baseset # force demandimport to load the module now
+except ImportError:
+ smartset = None
+
import maps
ignoredfiles = set(['.hgtags', '.hgsvnexternals', '.hgsub', '.hgsubstate'])
@@ -44,6 +50,27 @@ def configpath(ui, name):
path = ui.config('hgsubversion', name)
return path and hgutil.expandpath(path)
+def fileproperty(fname, pathfunc, default=None,
+ serializer=str, deserializer=str):
+ """define a property that is backed by a file"""
+ def fget(self):
+ if not hgutil.safehasattr(self, fname):
+ path = pathfunc(self)
+ if os.path.exists(path):
+ with open(path, 'r') as f:
+ setattr(self, fname, deserializer(f.read()))
+ else:
+ setattr(self, fname, default)
+ return getattr(self, fname)
+
+ def fset(self, value):
+ setattr(self, fname, value)
+ path = pathfunc(self)
+ with open(path, 'w') as f:
+ f.write(serializer(value))
+
+ return property(fget, fset)
+
def filterdiff(diff, oldrev, newrev):
diff = newfile_devnull_re.sub(r'--- \1\t(revision 0)' '\n'
r'+++ \1\t(working copy)',
@@ -320,16 +347,14 @@ def revset_fromsvn(repo, subset, x):
rev = repo.changelog.rev
bin = node.bin
meta = repo.svnmeta(skiperrorcheck=True)
- try:
- svnrevs = set(rev(bin(l.split(' ', 2)[1]))
- for l in maps.RevMap.readmapfile(meta.revmap_file,
- missingok=False))
- return filter(svnrevs.__contains__, subset)
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
+ if not meta.revmapexists:
raise hgutil.Abort("svn metadata is missing - "
"run 'hg svn rebuildmeta' to reconstruct it")
+ svnrevs = set(rev(h) for h in meta.revmap.hashes().keys())
+ filteredrevs = filter(svnrevs.__contains__, subset)
+ if smartset is not None:
+ filteredrevs = smartset.baseset(filteredrevs)
+ return filteredrevs
def revset_svnrev(repo, subset, x):
'''``svnrev(number)``
@@ -344,22 +369,18 @@ def revset_svnrev(repo, subset, x):
except ValueError:
raise error.ParseError("the argument to svnrev() must be a number")
- rev = rev + ' '
- revs = []
meta = repo.svnmeta(skiperrorcheck=True)
- try:
- for l in maps.RevMap.readmapfile(meta.revmap_file, missingok=False):
- if l.startswith(rev):
- n = l.split(' ', 2)[1]
- r = repo[node.bin(n)].rev()
- if r in subset:
- revs.append(r)
- return revs
- except IOError, err:
- if err.errno != errno.ENOENT:
- raise
+ if not meta.revmapexists:
raise hgutil.Abort("svn metadata is missing - "
"run 'hg svn rebuildmeta' to reconstruct it")
+ revs = []
+ for n in meta.revmap.revhashes(revnum):
+ r = repo[n].rev()
+ if r in subset:
+ revs.append(r)
+ if smartset is not None:
+ revs = smartset.baseset(revs)
+ return revs
revsets = {
'fromsvn': revset_fromsvn,
@@ -388,3 +409,12 @@ def parse_revnum(svnrepo, r):
return svnrepo.last_changed_rev
else:
raise error.RepoLookupError("unknown Subversion revision %r" % r)
+
+def get_contexts(repo, fromrev=0):
+ """Generator yielding contexts from the repository."""
+
+ for rev in xrange(fromrev, len(repo)):
+ try:
+ yield repo[rev]
+ except error.RepoLookupError:
+ pass
diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py
index 8a74ed7..f818a6f 100644
--- a/hgsubversion/wrappers.py
+++ b/hgsubversion/wrappers.py
@@ -1,3 +1,6 @@
+import inspect
+import os
+
from hgext import rebase as hgrebase
from mercurial import cmdutil
@@ -18,8 +21,8 @@ from mercurial import repair
from mercurial import revset
from mercurial import scmutil
+import inspect
import layouts
-import os
import replay
import pushmod
import stupid as stupidmod
@@ -103,7 +106,7 @@ def incoming(orig, ui, repo, origsource='default', **opts):
meta = repo.svnmeta(svn.uuid, svn.subdir)
ui.status('incoming changes from %s\n' % other.svnurl)
- svnrevisions = list(svn.revisions(start=meta.lastpulled))
+ svnrevisions = list(svn.revisions(start=meta.revmap.lastpulled))
if opts.get('newest_first'):
svnrevisions.reverse()
# Returns 0 if there are incoming changes, 1 otherwise.
@@ -133,7 +136,12 @@ def findcommonoutgoing(repo, other, onlyheads=None, force=False,
common, heads = util.outgoing_common_and_heads(repo, hashes, parent)
outobj = getattr(discovery, 'outgoing', None)
if outobj is not None:
- # Mercurial 2.1 and later
+ argspec = inspect.getargspec(outobj.__init__)
+ if 'repo' in argspec[0]:
+ # Starting from Mercurial 3.9.1 outgoing.__init__ accepts
+ # `repo` object instead of a `changelog`
+ return outobj(repo, common, heads)
+ # Mercurial 2.1 through 3.9
return outobj(repo.changelog, common, heads)
# Mercurial 2.0 and earlier
return common, heads
@@ -199,11 +207,13 @@ def push(repo, dest, force, revs):
old_encoding = util.swap_out_encoding()
try:
- hasobsolete = obsolete._enabled
+ hasobsolete = (obsolete._enabled or
+ obsolete.isenabled(repo, obsolete.createmarkersopt))
except:
hasobsolete = False
temporary_commits = []
+ obsmarkers = []
try:
# TODO: implement --rev/#rev support
# TODO: do credentials specified in the URL still work?
@@ -300,9 +310,7 @@ def push(repo, dest, force, revs):
if meta.get_source_rev(ctx=c)[0] == pushedrev.revnum:
# This is corresponds to the changeset we just pushed
if hasobsolete:
- ui.note('marking %s as obsoleted by %s\n' %
- (original_ctx.hex(), c.hex()))
- obsolete.createmarkers(repo, [(original_ctx, [c])])
+ obsmarkers.append([(original_ctx, [c])])
tip_ctx = c
@@ -343,10 +351,18 @@ def push(repo, dest, force, revs):
finally:
util.swap_out_encoding()
- if not hasobsolete:
- # strip the original changesets since the push was
- # successful and changeset obsolescence is unavailable
- util.strip(ui, repo, outgoing, "all")
+ with repo.lock():
+ if hasobsolete:
+ for marker in obsmarkers:
+ obsolete.createmarkers(repo, marker)
+ beforepush = marker[0][0]
+ afterpush = marker[0][1][0]
+ ui.note('marking %s as obsoleted by %s\n' %
+ (beforepush.hex(), afterpush.hex()))
+ else:
+ # strip the original changesets since the push was
+ # successful and changeset obsolescence is unavailable
+ util.strip(ui, repo, outgoing, "all")
finally:
try:
# It's always safe to delete the temporary commits.
@@ -358,11 +374,12 @@ def push(repo, dest, force, revs):
parent = repo[None].p1()
if parent.node() in temporary_commits:
hg.update(repo, parent.p1().node())
- if hasobsolete:
- relations = ((repo[n], ()) for n in temporary_commits)
- obsolete.createmarkers(repo, relations)
- else:
- util.strip(ui, repo, temporary_commits, backup=None)
+ with repo.lock():
+ if hasobsolete:
+ relations = ((repo[n], ()) for n in temporary_commits)
+ obsolete.createmarkers(repo, relations)
+ else:
+ util.strip(ui, repo, temporary_commits, backup=None)
finally:
util.swap_out_encoding(old_encoding)
@@ -413,8 +430,7 @@ def pull(repo, source, heads=[], force=False, meta=None):
meta.branchmap['default'] = meta.branch
ui = repo.ui
- start = meta.lastpulled
- origrevcount = len(meta.revmap)
+ start = meta.revmap.lastpulled
if start <= 0:
# we are initializing a new repository
@@ -507,7 +523,7 @@ def pull(repo, source, heads=[], force=False, meta=None):
util.swap_out_encoding(old_encoding)
if lastpulled is not None:
- meta.lastpulled = lastpulled
+ meta.revmap.lastpulled = lastpulled
revisions = len(meta.revmap) - oldrevisions
if revisions == 0:
@@ -590,6 +606,7 @@ def rebase(orig, ui, repo, **opts):
optionmap = {
'tagpaths': ('hgsubversion', 'tagpaths'),
'authors': ('hgsubversion', 'authormap'),
+ 'mapauthorscmd': ('hgsubversion', 'mapauthorscmd'),
'branchdir': ('hgsubversion', 'branchdir'),
'trunkdir': ('hgsubversion', 'trunkdir'),
'infix': ('hgsubversion', 'infix'),
@@ -646,9 +663,13 @@ def clone(orig, ui, source, dest=None, **opts):
# calling hg.clone directoly to get the repository instances it returns,
# breaks in subtle ways, so we double-wrap
- orighgclone = extensions.wrapfunction(hg, 'clone', hgclonewrapper)
- orig(ui, source, dest, **opts)
- hg.clone = orighgclone
+ orighgclone = None
+ try:
+ orighgclone = extensions.wrapfunction(hg, 'clone', hgclonewrapper)
+ orig(ui, source, dest, **opts)
+ finally:
+ if orighgclone:
+ hg.clone = orighgclone
# do this again; the ui instance isn't shared between the wrappers
if data.get('branches'):
@@ -660,7 +681,7 @@ def clone(orig, ui, source, dest=None, **opts):
if dstrepo.local() and srcrepo.capable('subversion'):
dst = dstrepo.local()
- fd = dst.opener("hgrc", "a", text=True)
+ fd = dst.vfs("hgrc", "a", text=True)
preservesections = set(s for s, v in optionmap.itervalues())
preservesections |= extrasections
for section in preservesections:
diff --git a/tests/comprehensive/test_obsstore_on.py b/tests/comprehensive/test_obsstore_on.py
new file mode 100644
index 0000000..7fd4a25
--- /dev/null
+++ b/tests/comprehensive/test_obsstore_on.py
@@ -0,0 +1,40 @@
+import os
+import sys
+
+# wrapped in a try/except because of weirdness in how
+# run.py works as compared to nose.
+try:
+ import test_util
+except ImportError:
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+ import test_util
+
+import test_push_command
+
+
+class ObsstoreOnMixIn(object):
+ # do not double the test size by being wrapped again
+ obsolete_mode_tests = False
+ stupid_mode_tests = False
+
+ def setUp(self):
+ super(ObsstoreOnMixIn, self).setUp()
+ hgrcpath = os.environ.get('HGRCPATH')
+ assert hgrcpath
+ with open(hgrcpath, 'a') as f:
+ f.write('\n[experimental]\nevolution=createmarkers\n')
+
+ def shortDescription(self):
+ text = super(ObsstoreOnMixIn, self).shortDescription()
+ if text:
+ text += ' (obsstore on)'
+ return text
+
+
+def buildtestclass(cls):
+ name = 'ObsstoreOn%s' % cls.__name__
+ newcls = type(name, (ObsstoreOnMixIn, cls,), {})
+ globals()[name] = newcls
+
+
+buildtestclass(test_push_command.PushTests)
diff --git a/tests/comprehensive/test_rebuildmeta.py b/tests/comprehensive/test_rebuildmeta.py
index e4e486b..e05f712 100644
--- a/tests/comprehensive/test_rebuildmeta.py
+++ b/tests/comprehensive/test_rebuildmeta.py
@@ -129,10 +129,14 @@ def _run_assertions(self, name, single, src, dest, u):
old, new = util.load(stf, resave=False), util.load(dtf, resave=False)
if tf == 'lastpulled' and (name,
self.stupid, single) in expect_youngest_skew:
- self.assertNotEqual(old, new,
- 'rebuildmeta unexpected match on youngest rev!')
+ self.assertNotEqual(
+ old, new,
+ 'rebuildmeta unexpected match on lastpulled: '
+ 'old %d new %d, case %r %r %r' % (
+ old, new, name, self.stupid, single))
continue
- self.assertEqual(old, new, tf + ' differs')
+ self.assertEqual(
+ old, new, '%s differs old: %r new %r'% (tf, old, new))
try:
self.assertEqual(src.branchmap(), dest.branchmap())
except AttributeError:
@@ -141,7 +145,12 @@ def _run_assertions(self, name, single, src, dest, u):
srcbi = util.load(os.path.join(src.path, 'svn', 'branch_info'))
destbi = util.load(os.path.join(dest.path, 'svn', 'branch_info'))
self.assertEqual(sorted(srcbi.keys()), sorted(destbi.keys()))
- revkeys = svnmeta.SVNMeta(dest).revmap.keys()
+ revmap = svnmeta.SVNMeta(dest).revmap
+ # revmap disables __iter__ intentionally to avoid possible slow code
+ # (not using database index in SqliteRevMap)
+ # we need to fetch all keys so enable it by setting _allowiter
+ revmap._allowiter = True
+ revkeys = revmap.keys()
for branch in destbi:
srcinfo = srcbi[branch]
destinfo = destbi[branch]
diff --git a/tests/comprehensive/test_sqlite_revmap.py b/tests/comprehensive/test_sqlite_revmap.py
new file mode 100644
index 0000000..0c0f2ec
--- /dev/null
+++ b/tests/comprehensive/test_sqlite_revmap.py
@@ -0,0 +1,77 @@
+import os
+import unittest
+import sys
+
+# wrapped in a try/except because of weirdness in how
+# run.py works as compared to nose.
+try:
+ import test_util
+except ImportError:
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
+ import test_util
+
+# interesting and fast tests
+import test_fetch_mappings
+import test_fetch_renames
+import test_pull
+import test_template_keywords
+import test_utility_commands
+
+# comprehensive tests
+try:
+ import test_custom_layout
+except ImportError:
+ sys.path.insert(0, os.path.dirname(__file__))
+ import test_custom_layout
+
+import test_rebuildmeta
+import test_updatemeta
+
+from hgsubversion import svnmeta, maps
+
+
+class SqliteRevMapMixIn(object):
+ # do not double the test size by being wrapped again
+ obsolete_mode_tests = False
+ stupid_mode_tests = False
+
+ def setUp(self):
+ assert svnmeta.SVNMeta._defaultrevmapclass is maps.RevMap
+ svnmeta.SVNMeta._defaultrevmapclass = maps.SqliteRevMap
+ super(SqliteRevMapMixIn, self).setUp()
+
+ def tearDown(self):
+ assert svnmeta.SVNMeta._defaultrevmapclass is maps.SqliteRevMap
+ svnmeta.SVNMeta._defaultrevmapclass = maps.RevMap
+ super(SqliteRevMapMixIn, self).tearDown()
+
+ def shortDescription(self):
+ text = super(SqliteRevMapMixIn, self).shortDescription()
+ if text:
+ text += ' (sqlite revmap)'
+ return text
+
+def buildtestclass(cls, selector=None):
+ name = 'SqliteRevMap%s' % cls.__name__
+ newcls = type(name, (SqliteRevMapMixIn, cls,), {})
+
+ # remove test cases not selected by selector
+ if selector:
+ for name in dir(newcls):
+ if name.startswith('test_') and not selector(name[5:]):
+ setattr(newcls, name, None)
+
+ globals()[name] = newcls
+
+def svndumpselector(name):
+ return name in ['branch_rename_to_trunk',
+ 'tag_name_same_as_branch']
+
+buildtestclass(test_fetch_mappings.MapTests)
+buildtestclass(test_fetch_renames.TestFetchRenames)
+buildtestclass(test_pull.TestPull)
+buildtestclass(test_template_keywords.TestLogKeywords)
+buildtestclass(test_utility_commands.UtilityTests)
+
+buildtestclass(test_rebuildmeta.RebuildMetaTests, svndumpselector)
+buildtestclass(test_updatemeta.UpdateMetaTests, svndumpselector)
diff --git a/tests/comprehensive/test_verify_and_startrev.py b/tests/comprehensive/test_verify_and_startrev.py
index 7581df0..8ad22c2 100644
--- a/tests/comprehensive/test_verify_and_startrev.py
+++ b/tests/comprehensive/test_verify_and_startrev.py
@@ -34,6 +34,7 @@ _skipstandard = set([
'correct.svndump',
'corrupt.svndump',
'emptyrepo2.svndump',
+ 'dir_removal.svndump',
])
def _do_case(self, name, layout):
diff --git a/tests/fixtures/dir_removal.svndump b/tests/fixtures/dir_removal.svndump
new file mode 100644
index 0000000..a278802
--- /dev/null
+++ b/tests/fixtures/dir_removal.svndump
@@ -0,0 +1,103 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 5554378f-55cc-437f-9045-6148f657307d
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2017-05-23T11:48:49.399395Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 103
+Content-length: 103
+
+K 10
+svn:author
+V 8
+testuser
+K 8
+svn:date
+V 27
+2017-05-23T11:49:53.974692Z
+K 7
+svn:log
+V 1
+1
+PROPS-END
+
+Node-path: dir1
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir1/1.txt
+Node-kind: file
+Node-action: add
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Prop-content-length: 10
+Text-content-length: 0
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir2
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: dir2/2.txt
+Node-kind: file
+Node-action: add
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Prop-content-length: 10
+Text-content-length: 0
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 103
+Content-length: 103
+
+K 10
+svn:author
+V 8
+testuser
+K 8
+svn:date
+V 27
+2017-05-23T11:50:14.852941Z
+K 7
+svn:log
+V 1
+2
+PROPS-END
+
+Node-path: dir1/dir2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: dir2
+
+
+Node-path: dir2
+Node-action: delete
+
+
diff --git a/tests/fixtures/rename-closed-branch-dir.sh b/tests/fixtures/rename-closed-branch-dir.sh
new file mode 100644
index 0000000..d0ae264
--- /dev/null
+++ b/tests/fixtures/rename-closed-branch-dir.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+#
+# Generate rename-closed-branch-dir.svndump
+#
+
+mkdir temp
+cd temp
+
+mkdir project
+cd project
+mkdir trunk
+mkdir branches
+mkdir tags
+cd ..
+
+svnadmin create testrepo
+CURRENT_DIR=`pwd`
+svnurl=file://"$CURRENT_DIR"/testrepo
+#svn import project-orig $svnurl -m "init project"
+
+svn co $svnurl project
+cd project
+svn add *
+svn ci -m "init project"
+
+cd trunk
+echo a > a.txt
+svn add a.txt
+svn ci -m "add a.txt in trunk"
+
+# Create a branch
+svn up
+cd ../branches
+svn copy ../trunk async-db
+svn ci -m "add branch async-db"
+svn up
+
+# Implement feature
+cd async-db
+echo b > b.txt
+svn add b.txt
+svn ci -m "Async functionality"
+
+# Merge feature branch
+cd ../../trunk
+svn merge $svnurl/branches/async-db
+svn ci -m "Merged branch async-db"
+cd ..
+svn up
+
+# Create branch folder for unnecessary branches
+svn mkdir $svnurl/branches/dead -m "Create branch folder for unnecessary branches"
+svn up
+
+# We don't need the 'async-db' branch, anymore.
+svn copy $svnurl/branches/async-db $svnurl/branches/dead -m "We don't need the 'async-db' branch, anymore."
+svn up
+
+# Rename 'dead' folder to 'closed'
+svn move $svnurl/branches/dead $svnurl/branches/closed -m "Renamed 'dead' folder to 'closed'"
+svn up
+
+# Move 'branches/closed' to 'tags/closed'
+svn move $svnurl/branches/closed $svnurl/tags/closed -m "Moved 'branches/closed' to 'tags/closed'."
+svn up
+
+# Dump repository
+cd ..
+svnadmin dump testrepo > ../rename-closed-branch-dir.svndump
diff --git a/tests/fixtures/rename-closed-branch-dir.svndump b/tests/fixtures/rename-closed-branch-dir.svndump
new file mode 100644
index 0000000..aa327fe
--- /dev/null
+++ b/tests/fixtures/rename-closed-branch-dir.svndump
@@ -0,0 +1,296 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 2efdcfe9-9dfd-40a7-a9cc-bf5b70806ff3
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2016-01-27T15:35:29.673334Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 112
+Content-length: 112
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:30.079847Z
+K 7
+svn:log
+V 12
+init project
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 118
+Content-length: 118
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:31.065912Z
+K 7
+svn:log
+V 18
+add a.txt in trunk
+PROPS-END
+
+Node-path: trunk/a.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3
+Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b
+Content-length: 12
+
+PROPS-END
+a
+
+
+Revision-number: 3
+Prop-content-length: 119
+Content-length: 119
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:34.051261Z
+K 7
+svn:log
+V 19
+add branch async-db
+PROPS-END
+
+Node-path: branches/async-db
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 2
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 119
+Content-length: 119
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:36.101507Z
+K 7
+svn:log
+V 19
+Async functionality
+PROPS-END
+
+Node-path: branches/async-db/b.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 2
+Text-content-md5: 3b5d5c3712955042212316173ccf37be
+Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+Content-length: 12
+
+PROPS-END
+b
+
+
+Revision-number: 5
+Prop-content-length: 122
+Content-length: 122
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:38.055736Z
+K 7
+svn:log
+V 22
+Merged branch async-db
+PROPS-END
+
+Node-path: trunk
+Node-kind: dir
+Node-action: change
+Prop-content-length: 57
+Content-length: 57
+
+K 13
+svn:mergeinfo
+V 22
+/branches/async-db:3-4
+PROPS-END
+
+
+Node-path: trunk/b.txt
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: branches/async-db/b.txt
+Text-copy-source-md5: 3b5d5c3712955042212316173ccf37be
+Text-copy-source-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b
+
+
+Revision-number: 6
+Prop-content-length: 145
+Content-length: 145
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:40.046670Z
+K 7
+svn:log
+V 45
+Create branch folder for unnecessary branches
+PROPS-END
+
+Node-path: branches/dead
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 7
+Prop-content-length: 145
+Content-length: 145
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:41.048576Z
+K 7
+svn:log
+V 45
+We don't need the 'async-db' branch, anymore.
+PROPS-END
+
+Node-path: branches/dead/async-db
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 6
+Node-copyfrom-path: branches/async-db
+
+
+Revision-number: 8
+Prop-content-length: 133
+Content-length: 133
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:42.046536Z
+K 7
+svn:log
+V 33
+Renamed 'dead' folder to 'closed'
+PROPS-END
+
+Node-path: branches/closed
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: branches/dead
+
+
+Node-path: branches/dead
+Node-action: delete
+
+
+Revision-number: 9
+Prop-content-length: 141
+Content-length: 141
+
+K 10
+svn:author
+V 5
+augie
+K 8
+svn:date
+V 27
+2016-01-27T15:35:43.048056Z
+K 7
+svn:log
+V 41
+Moved 'branches/closed' to 'tags/closed'.
+PROPS-END
+
+Node-path: branches/closed
+Node-action: delete
+
+
+Node-path: tags/closed
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 8
+Node-copyfrom-path: branches/closed
+
+
diff --git a/tests/run.py b/tests/run.py
index aef61f9..497f4f2 100755
--- a/tests/run.py
+++ b/tests/run.py
@@ -5,53 +5,6 @@ import os
import sys
import unittest
-import test_util
-test_util.SkipTest = None
-
-def tests():
- import test_binaryfiles
- import test_diff
- import test_externals
- import test_fetch_branches
- import test_fetch_command
- import test_fetch_command_regexes
- import test_fetch_exec
- import test_fetch_mappings
- import test_fetch_renames
- import test_fetch_symlinks
- import test_fetch_truncated
- import test_hooks
- import test_svn_pre_commit_hooks
- import test_pull
- import test_pull_fallback
- import test_push_command
- import test_push_renames
- import test_push_dirs
- import test_push_eol
- import test_push_autoprops
- import test_single_dir_clone
- import test_single_dir_push
- import test_svnwrap
- import test_tags
- import test_template_keywords
- import test_utility_commands
- import test_unaffected_core
- import test_urls
-
- sys.path.append(os.path.dirname(__file__))
- sys.path.append(os.path.join(os.path.dirname(__file__), 'comprehensive'))
-
- import test_rebuildmeta
- import test_stupid_pull
- import test_updatemeta
- import test_verify_and_startrev
-
- return locals()
-
-def comprehensive(mod):
- dir = os.path.basename(os.path.dirname(mod.__file__))
- return dir == 'comprehensive'
-
if __name__ == '__main__':
description = ("This script runs the hgsubversion tests. If no tests are "
"specified, all known tests are implied.")
@@ -100,26 +53,33 @@ if __name__ == '__main__':
import tempfile
sys.stdout = tempfile.TemporaryFile()
- all_tests = tests()
-
- args = [i.split('.py')[0].replace('-', '_') for i in args]
+ args = [os.path.basename(os.path.splitext(arg)[0]).replace('-', '_')
+ for arg in args]
loader = unittest.TestLoader()
suite = unittest.TestSuite()
+ if sys.version_info[:2] < (2, 7):
+ import glob
+ def discover(start_dir, pattern='test*.py', top_level_dir=None):
+ tests = []
+ sys.path.append(start_dir)
+ for path in glob.glob(os.path.join(start_dir, pattern)):
+ name = os.path.splitext(os.path.basename(path))[0]
+ tests.append(loader.loadTestsFromModule(__import__(name)))
+ return tests
+ loader.discover = discover
+
if not args:
- check = lambda x: options.comprehensive or not comprehensive(x)
- suite.addTests(loader.loadTestsFromModule(m)
- for (n, m) in sorted(all_tests.iteritems())
- if check(m))
+ suite.addTests(loader.discover('.'))
+
+ if options.comprehensive:
+ suite.addTests(loader.discover('comprehensive',
+ top_level_dir='comprehensive'))
else:
- for arg in args:
- if arg == 'test_util':
- continue
- elif arg not in all_tests:
- print >> sys.stderr, 'test module %s not available' % arg
- else:
- suite.addTest(loader.loadTestsFromModule(all_tests[arg]))
+ sys.path.append(os.path.join(os.path.dirname(__file__), 'comprehensive'))
+
+ suite.addTests(loader.loadTestsFromNames(args))
runner = unittest.TextTestRunner(**testargs)
result = runner.run(suite)
diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py
index 45991ae..781f023 100644
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -41,7 +41,8 @@ class TestFetchBranches(test_util.TestBase):
heads = dict([(ctx.branch(), ctx) for ctx in heads])
# Let these tests disabled yet as the fix is not obvious
self.assertEqual(heads['branch1'].manifest().keys(), ['b'])
- self.assertEqual(heads['branch2'].manifest().keys(), ['a', 'b'])
+ self.assertEqual(sorted(heads['branch2'].manifest().keys()),
+ ['a', 'b'])
def test_unorderedbranch(self):
repo = self._load_fixture_and_fetch('unorderedbranch.svndump')
diff --git a/tests/test_fetch_command.py b/tests/test_fetch_command.py
index 4649555..745a4a4 100644
--- a/tests/test_fetch_command.py
+++ b/tests/test_fetch_command.py
@@ -93,7 +93,7 @@ class TestBasicRepoLayout(test_util.TestBase):
'test_files_copied_from_outside_btt.svndump')
self.assertEqual(node.hex(repo['tip'].node()),
'3c78170e30ddd35f2c32faa0d8646ab75bba4f73')
- self.assertEqual(test_util.repolen(repo.changelog), 2)
+ self.assertEqual(test_util.repolen(repo), 2)
def test_file_renamed_in_from_outside_btt(self):
repo = self._load_fixture_and_fetch(
diff --git a/tests/test_fetch_dir_removal.py b/tests/test_fetch_dir_removal.py
new file mode 100644
index 0000000..24a795d
--- /dev/null
+++ b/tests/test_fetch_dir_removal.py
@@ -0,0 +1,17 @@
+import test_util
+
+import sys
+import unittest
+
+class TestFetchDirectoryRemoval(test_util.TestBase):
+ stupid_mode_tests = True
+
+ def test_removal(self):
+ repo = self._load_fixture_and_fetch('dir_removal.svndump',
+ layout='single',
+ subdir='dir1')
+ self.assertEqual(sorted(repo['tip'].manifest().keys()),
+ ['1.txt', 'dir2/2.txt'])
+ extra = repo['tip'].extra().copy()
+ extra.pop('convert_revision', None)
+ self.assertEqual(extra, {'branch': 'default'})
diff --git a/tests/test_fetch_mappings.py b/tests/test_fetch_mappings.py
index f022c6f..f4e373c 100644
--- a/tests/test_fetch_mappings.py
+++ b/tests/test_fetch_mappings.py
@@ -92,7 +92,10 @@ class MapTests(test_util.TestBase):
new = open(os.path.join(repopath, 'authors'), 'w')
new.write(open(orig).read())
new.close()
- test = maps.AuthorMap(self.repo.svnmeta(skiperrorcheck=True))
+ meta = self.repo.svnmeta(skiperrorcheck=True)
+ test = maps.AuthorMap(
+ meta.ui, meta.authormap_file, meta.defaulthost,
+ meta.caseignoreauthors, meta.mapauthorscmd, meta.defaultauthors)
fromself = set(test)
test.load(orig)
all_tests = set(test)
@@ -114,6 +117,15 @@ class MapTests(test_util.TestBase):
self.assertEqual(self.repo['tip'].user(),
'evil@5b65bade-98f3-4993-a01f-b7a6710da339')
+ def test_author_map_mapauthorscmd(self):
+ repo_path = self.load_svndump('replace_trunk_with_branch.svndump')
+ ui = self.ui()
+ ui.setconfig('hgsubversion', 'mapauthorscmd', 'echo "svn: %s"')
+ commands.clone(ui, test_util.fileurl(repo_path),
+ self.wc_path)
+ self.assertEqual(self.repo[0].user(), 'svn: Augie')
+ self.assertEqual(self.repo['tip'].user(), 'svn: evil')
+
def _loadwithfilemap(self, svndump, filemapcontent,
failonmissing=True):
repo_path = self.load_svndump(svndump)
@@ -149,9 +161,9 @@ class MapTests(test_util.TestBase):
# The exclusion of alpha is overridden by the later rule to
# include all of '.', whereas gamma should remain excluded
# because it's excluded after the root directory.
- self.assertEqual(self.repo[0].manifest().keys(),
+ self.assertEqual(sorted(self.repo[0].manifest().keys()),
['alpha', 'beta'])
- self.assertEqual(self.repo['default'].manifest().keys(),
+ self.assertEqual(sorted(self.repo['default'].manifest().keys()),
['alpha', 'beta'])
@test_util.requiresreplay
@@ -200,6 +212,22 @@ class MapTests(test_util.TestBase):
self.assert_('good-name' in branches)
self.assertEquals(self.repo[2].branch(), 'default')
+ def test_branchmap_regex_and_glob(self):
+ repo_path = self.load_svndump('branchmap.svndump')
+ branchmap = open(self.branchmap, 'w')
+ branchmap.write("syntax:re\n")
+ branchmap.write("bad(.*) = good-\\1 # stuffy\n")
+ branchmap.write("glob:feat* = default\n")
+ branchmap.close()
+ ui = self.ui()
+ ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
+ commands.clone(ui, test_util.fileurl(repo_path),
+ self.wc_path, branchmap=self.branchmap)
+ branches = set(self.repo[i].branch() for i in self.repo)
+ self.assert_('badname' not in branches)
+ self.assert_('good-name' in branches)
+ self.assertEquals(self.repo[2].branch(), 'default')
+
def test_branchmap_tagging(self):
'''test tagging a renamed branch, which used to raise an exception'''
repo_path = self.load_svndump('commit-to-tag.svndump')
@@ -290,6 +318,23 @@ class MapTests(test_util.TestBase):
for r in repo:
self.assertEquals(verify.verify(ui, repo, rev=r), 0)
+ def test_branchmap_no_replacement(self):
+ '''test that empty mappings are accepted
+
+ Empty mappings are lines like 'this ='. We check that such branches are
+ not converted.
+ '''
+ repo_path = self.load_svndump('branchmap.svndump')
+ branchmap = open(self.branchmap, 'w')
+ branchmap.write("badname =\n")
+ branchmap.close()
+ ui = self.ui()
+ ui.setconfig('hgsubversion', 'branchmap', self.branchmap)
+ commands.clone(ui, test_util.fileurl(repo_path),
+ self.wc_path, branchmap=self.branchmap)
+ branches = set(self.repo[i].branch() for i in self.repo)
+ self.assertEquals(sorted(branches), ['default', 'feature'])
+
def test_tagmap(self):
repo_path = self.load_svndump('basic_tag_tests.svndump')
tagmap = open(self.tagmap, 'w')
diff --git a/tests/test_fetch_renames.py b/tests/test_fetch_renames.py
index fad681d..caeeeea 100644
--- a/tests/test_fetch_renames.py
+++ b/tests/test_fetch_renames.py
@@ -34,12 +34,16 @@ class TestFetchRenames(test_util.TestBase):
repo = self._load_fixture_and_fetch('renames_with_prefix.svndump',
subdir='prefix',
config=config)
- self._run_assertions(repo)
+ self._run_assertions(repo, prefix=True)
- def _run_assertions(self, repo):
+ def _run_assertions(self, repo, prefix=False):
# Map revnum to mappings of dest name to (source name, dest content)
+ if prefix:
+ prefixlen = len('svn:ae30a990-0fd3-493e-b5d7-883bdd606745/prefix')
+ else:
+ prefixlen = len('svn:ae30a990-0fd3-493e-b5d7-883bdd606745')
copies = {
- 4: {
+ '/trunk@6': {
'a1': ('a', 'a\n'),
'linka1': ('linka', 'a'),
'a2': ('a', 'a\n'),
@@ -55,32 +59,35 @@ class TestFetchRenames(test_util.TestBase):
'da2/db/dbf': ('da/db/dbf', 'd\n'),
'da2/db/dblink': ('da/db/dblink', '../daf'),
},
- 5: {
+ '/branches/branch1@6': {
'c1': ('c', 'c\nc\n'),
'linkc1': ('linkc', 'cc'),
},
- 9: {
+ '/trunk@10': {
'unchanged2': ('unchanged', 'unchanged\n'),
'unchangedlink2': ('unchangedlink', 'unchanged'),
'unchangeddir2/f': ('unchangeddir/f', 'unchanged2\n'),
'unchangeddir2/link': ('unchangeddir/link', 'f'),
},
- 10: {
+ '/trunk@11': {
'groupdir2/b': ('groupdir/b', 'b\n'),
'groupdir2/linkb': ('groupdir/linkb', 'b'),
},
}
for rev in repo:
ctx = repo[rev]
- copymap = copies.get(rev, {})
+ copymap = copies.get(ctx.extra()['convert_revision'][prefixlen:],
+ {})
for f in ctx.manifest():
cp = ctx[f].renamed()
- self.assertEqual(bool(cp), bool(copymap.get(f)),
- 'copy records differ for %s in %d' % (f, rev))
- if not cp:
- continue
- self.assertEqual(cp[0], copymap[f][0])
- self.assertEqual(ctx[f].data(), copymap[f][1])
+ want = copymap.get(f)
+ self.assertEqual(
+ bool(cp), bool(want),
+ 'copy records differ for %s in %d (want %r, got %r)' % (
+ f, rev, want, cp))
+ if cp:
+ self.assertEqual(cp[0], want[0])
+ self.assertEqual(ctx[f].data(), want[1])
self.assertEqual(repo['tip']['changed3'].data(), 'changed\nchanged3\n')
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 219db36..527faea 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -30,3 +30,4 @@ class TestHelpers(unittest.TestCase):
fs.popfile('bb')
self.assertEqual([], os.listdir(fs._tempdir))
self.assertRaises(editor.EditingError, lambda: fs.getfile('bb'))
+ fs.close()
diff --git a/tests/test_hooks.py b/tests/test_hooks.py
index ffd3edc..66197df 100644
--- a/tests/test_hooks.py
+++ b/tests/test_hooks.py
@@ -17,7 +17,7 @@ class TestHooks(test_util.TestBase):
def test_updatemetahook(self):
repo, repo_path = self._loadupdate('single_rev.svndump')
- state = repo.parents()
+ state = repo[None].parents()
self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
commands.pull(self.repo.ui, self.repo)
diff --git a/tests/test_pull.py b/tests/test_pull.py
index 3a1a217..d038094 100644
--- a/tests/test_pull.py
+++ b/tests/test_pull.py
@@ -20,37 +20,37 @@ class TestPull(test_util.TestBase):
def test_nochanges(self):
self._loadupdate('single_rev.svndump')
- state = self.repo.parents()
+ state = self.repo[None].parents()
commands.pull(self.repo.ui, self.repo)
- self.assertEqual(state, self.repo.parents())
+ self.assertEqual(state, self.repo[None].parents())
def test_onerevision_noupdate(self):
repo, repo_path = self._loadupdate('single_rev.svndump')
- state = repo.parents()
+ state = repo[None].parents()
self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
commands.pull(self.repo.ui, repo)
- self.assertEqual(state, repo.parents())
+ self.assertEqual(state, repo[None].parents())
self.assertTrue('tip' not in repo['.'].tags())
def test_onerevision_doupdate(self):
repo, repo_path = self._loadupdate('single_rev.svndump')
- state = repo.parents()
+ state = repo[None].parents()
self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
commands.pull(self.repo.ui, repo, update=True)
- self.failIfEqual(state, repo.parents())
+ self.failIfEqual(state, repo[None].parents())
self.assertTrue('tip' in repo['.'].tags())
def test_onerevision_divergent(self):
repo, repo_path = self._loadupdate('single_rev.svndump')
self.commitchanges((('alpha', 'alpha', 'Changed another way'),))
- state = repo.parents()
+ state = repo[None].parents()
self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed one way'})
try:
commands.pull(self.repo.ui, repo, update=True)
except hgutil.Abort:
# hg < 1.9 raised when crossing branches
pass
- self.assertEqual(state, repo.parents())
+ self.assertEqual(state, repo[None].parents())
self.assertTrue('tip' not in repo['.'].tags())
self.assertEqual(len(repo.heads()), 2)
diff --git a/tests/test_pull_fallback.py b/tests/test_pull_fallback.py
index a2979ff..5d94b16 100644
--- a/tests/test_pull_fallback.py
+++ b/tests/test_pull_fallback.py
@@ -37,14 +37,14 @@ class TestPullFallback(test_util.TestBase):
# Passing stupid=True doesn't seem to be working - force it
repo.ui.setconfig('hgsubversion', 'stupid', "true")
- state = repo.parents()
+ state = repo[None].parents()
calls, replaced = _monkey_patch(to_patch)
try:
self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'})
commands.pull(self.repo.ui, repo, update=True)
- self.failIfEqual(state, repo.parents())
+ self.failIfEqual(state, repo[None].parents())
self.assertTrue('tip' in repo[None].tags())
self.assertEqual(expected_calls, calls)
diff --git a/tests/test_push_command.py b/tests/test_push_command.py
index a62f3ce..6debbb2 100644
--- a/tests/test_push_command.py
+++ b/tests/test_push_command.py
@@ -138,7 +138,33 @@ class PushTests(test_util.TestBase):
open(os.path.join(repo_path, 'conf', 'svnserve.conf'),
'w').write('[general]\nanon-access=write\n[sasl]\n')
self.port = random.randint(socket.IPPORT_USERRESERVED, 65535)
- self.host = 'localhost'
+ self.host = socket.gethostname()
+
+ # The `svnserve` binary appears to use the obsolete `gethostbyname(3)`
+ # function, which always returns an IPv4 address, even on hosts that
+ # support and expect IPv6. As a workaround, resolve the hostname
+ # within the test harness with `getaddrinfo(3)` to ensure that the
+ # client and server both use the same IPv4 or IPv6 address.
+ addrinfo = socket.getaddrinfo(self.host, self.port)
+ # On macOS svn seems to have issues with IPv6 at least some of
+ # the time, so try and bias towards IPv4. This works because
+ # AF_INET is less than AF_INET6 on all platforms I've
+ # checked. Hopefully any platform where that's not true will
+ # be fine with IPv6 all the time. :)
+ selected = sorted(addrinfo)[0]
+ self.host = selected[4][0]
+
+ # If we're connecting via IPv6 the need to put brackets around the
+ # hostname in the URL.
+ ipv6 = selected[0] == socket.AF_INET6
+
+ # Ditch any interface information since that's not helpful in
+ # a URL
+ if ipv6 and ':' in self.host and '%' in self.host:
+ self.host = self.host.rsplit('%', 1)[0]
+
+ urlfmt = 'svn://[%s]:%d/%s' if ipv6 else 'svn://%s:%d/%s'
+
args = ['svnserve', '--daemon', '--foreground',
'--listen-port=%d' % self.port,
'--listen-host=%s' % self.host,
@@ -152,7 +178,7 @@ class PushTests(test_util.TestBase):
import shutil
shutil.rmtree(self.wc_path)
commands.clone(self.ui(),
- 'svn://%s:%d/%s' % (self.host, self.port, subdir),
+ urlfmt % (self.host, self.port, subdir),
self.wc_path, noupdate=True)
repo = self.repo
diff --git a/tests/test_revmap_migrate.py b/tests/test_revmap_migrate.py
new file mode 100644
index 0000000..4d92b22
--- /dev/null
+++ b/tests/test_revmap_migrate.py
@@ -0,0 +1,72 @@
+import test_util
+
+from mercurial import util as hgutil
+from hgsubversion import svnmeta, maps
+from mercurial.node import hex
+
+class TestRevMapMigrate(test_util.TestBase):
+
+ def tearDown(self):
+ # revert changes to defaultrevmapclass
+ svnmeta.SVNMeta._defaultrevmapclass = maps.RevMap
+
+ def _test_revmap_migrate(self, fromclass, toclass):
+ # revmap interfaces to test
+ getters = [
+ lambda x: x.branchedits('the_branch', 3),
+ lambda x: x.branchedits('the_branch', 4),
+ lambda x: x.branchedits('the_branch', 5),
+ lambda x: x.branchedits('the_branch', 6),
+ lambda x: x.branchedits(None, 5),
+ lambda x: x.branchedits('non_existed', 10),
+ lambda x: x.branchmaxrevnum('the_branch', 3),
+ lambda x: x.branchmaxrevnum('the_branch', 4),
+ lambda x: x.branchmaxrevnum('the_branch', 5),
+ lambda x: x.branchmaxrevnum('the_branch', 6),
+ lambda x: x.branchmaxrevnum(None, 5),
+ lambda x: x.branchmaxrevnum('non_existed', 10),
+ lambda x: list(x.revhashes(3)),
+ lambda x: list(x.revhashes(4)),
+ lambda x: list(x.revhashes(42)),
+ lambda x: list(x.revhashes(105)),
+ lambda x: x.firstpulled,
+ lambda x: x.lastpulled,
+ lambda x: x.lasthash,
+ ]
+
+ svnmeta.SVNMeta._defaultrevmapclass = fromclass
+ repo = self._load_fixture_and_fetch('two_heads.svndump')
+ meta = svnmeta.SVNMeta(repo)
+ self.assertEqual(meta.revmap.__class__, fromclass)
+ origrevmap = meta.revmap
+
+ # insert fake special (duplicated, with '\0') data
+ origrevmap[103, None] = b'\0' * 20
+ origrevmap[104, None] = b'\0' * 18 + b'cd'
+ origrevmap[105, None] = b'ab\0cdefghijklmnopqrs'
+ origrevmap[104, None] = b'\0' * 18 + b'\xff\0'
+ origrevmap[105, 'ab'] = origrevmap[105, None]
+
+ origvalues = [f(meta.revmap) for f in getters]
+
+ # migrate to another format (transparently)
+ svnmeta.SVNMeta._defaultrevmapclass = toclass
+ meta = svnmeta.SVNMeta(repo)
+ self.assertEqual(meta.revmap.__class__, toclass)
+
+ # enable iteration otherwise we cannot use iteritems
+ origrevmap._allowiter = True
+ for k, v in origrevmap.iteritems():
+ newv = meta.revmap[k]
+ self.assertEqual(newv, v)
+ self.assertEqual(len(newv), 20)
+ self.assertEqual(meta.revmap[meta.revmap.hashes()[v]], v)
+
+ newvalues = [f(meta.revmap) for f in getters]
+ self.assertEqual(origvalues, newvalues)
+
+ def test_revmap_migrate_up(self):
+ self._test_revmap_migrate(maps.RevMap, maps.SqliteRevMap)
+
+ def test_revmap_migrate_down(self):
+ self._test_revmap_migrate(maps.SqliteRevMap, maps.RevMap)
diff --git a/tests/test_svnwrap.py b/tests/test_svnwrap.py
index d71c9e4..4db069f 100644
--- a/tests/test_svnwrap.py
+++ b/tests/test_svnwrap.py
@@ -11,15 +11,11 @@ class TestBasicRepoLayout(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp('svnwrap_test')
self.repo_path = '%s/testrepo' % self.tmpdir
- subprocess.call(['svnadmin', 'create', self.repo_path, ])
- inp = open(os.path.join(os.path.dirname(__file__), 'fixtures',
- 'project_root_at_repo_root.svndump'))
- proc = subprocess.call(['svnadmin', 'load', self.repo_path, ],
- stdin=inp,
- close_fds=test_util.canCloseFds,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
- assert proc == 0
+
+ with open(os.path.join(test_util.FIXTURES,
+ 'project_root_at_repo_root.svndump')) as fp:
+ svnwrap.create_and_load(self.repo_path, fp)
+
self.repo = svnwrap.SubversionRepo(test_util.fileurl(self.repo_path))
def tearDown(self):
diff --git a/tests/test_tags.py b/tests/test_tags.py
index cfd2659..2a0f06b 100644
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -4,6 +4,7 @@ import os, sys, cStringIO, difflib
import unittest
from mercurial import commands
+from mercurial import error
from mercurial import hg
from mercurial import node
from mercurial import ui
@@ -128,10 +129,8 @@ rename a tag
'branch': 'magic',
'convert_revision': 'svn:af82cc90-c2d2-43cd-b1aa-c8a78449440a/tags/will-edit@19'})
self.assertEqual(willedit, repo.tags()['will-edit'])
- self.assertEqual(repo['will-edit'].manifest().keys(), ['alpha',
- 'beta',
- 'gamma',
- ])
+ self.assertEqual(sorted(repo['will-edit'].manifest().keys()),
+ ['alpha', 'beta', 'gamma'])
self.assertEqual(
repo[alsoedit].extra(),
{'close': '1',
@@ -164,14 +163,12 @@ rename a tag
'magic2': '\xa3\xa2D\x86aM\xc0v\xb9\xb0\x18\x14\xad\xacwBUi}\xe2',
})
- def test_old_tag_map_rebuilds(self):
+ def test_old_tag_map_aborts(self):
repo = self._load_fixture_and_fetch('tag_name_same_as_branch.svndump')
tm = os.path.join(repo.path, 'svn', 'tagmap')
open(tm, 'w').write('1\n')
# force tags to load since it is lazily loaded when needed
- repo.svnmeta().tags
- commands.pull(repo.ui, repo)
- self.assertEqual(open(tm).read().splitlines()[0], '2')
+ self.assertRaises(error.Abort, lambda: repo.svnmeta().tags)
def _debug_print_tags(self, repo, ctx, fp):
def formatnode(ctx):
diff --git a/tests/test_urls.py b/tests/test_urls.py
index 591ac22..a790f16 100644
--- a/tests/test_urls.py
+++ b/tests/test_urls.py
@@ -8,58 +8,61 @@ from hgsubversion import svnrepo
class TestSubversionUrls(test_util.TestBase):
def test_standard_url(self):
- self.assertEqual((None, None, 'file:///var/svn/repo'),
- parse_url('file:///var/svn/repo'))
+ self.check_parse_url((None, None, 'file:///var/svn/repo'),
+ ('file:///var/svn/repo', ))
def test_user_url(self):
- self.assertEqual(
+ self.check_parse_url(
('joe', None, 'https://svn.testurl.com/repo'),
- parse_url('https://joe@svn.testurl.com/repo'))
- self.assertEqual(
+ ('https://joe@svn.testurl.com/repo', ))
+ self.check_parse_url(
('bob', None, 'https://svn.testurl.com/repo'),
- parse_url('https://joe@svn.testurl.com/repo', 'bob'))
+ ('https://joe@svn.testurl.com/repo', 'bob', ))
def test_password_url(self):
- self.assertEqual(
+ self.check_parse_url(
(None, 't3stpw', 'svn+ssh://svn.testurl.com/repo'),
- parse_url('svn+ssh://:t3stpw@svn.testurl.com/repo'))
- self.assertEqual(
+ ('svn+ssh://:t3stpw@svn.testurl.com/repo', ))
+ self.check_parse_url(
(None, '123abc', 'svn+ssh://svn.testurl.com/repo'),
- parse_url('svn+ssh://:t3stpw@svn.testurl.com/repo', None, '123abc'))
+ ('svn+ssh://:t3stpw@svn.testurl.com/repo', None, '123abc', ))
def test_svnssh_preserve_user(self):
- self.assertEqual(
+ self.check_parse_url(
('user', 't3stpw', 'svn+ssh://user@svn.testurl.com/repo',),
- parse_url('svn+ssh://user:t3stpw@svn.testurl.com/repo'))
- self.assertEqual(
+ ('svn+ssh://user:t3stpw@svn.testurl.com/repo', ))
+ self.check_parse_url(
('bob', '123abc', 'svn+ssh://bob@svn.testurl.com/repo',),
- parse_url('svn+ssh://user:t3stpw@svn.testurl.com/repo', 'bob', '123abc'))
- self.assertEqual(
+ ('svn+ssh://user:t3stpw@svn.testurl.com/repo', 'bob', '123abc', ))
+ self.check_parse_url(
('user2', None, 'svn+ssh://user2@svn.testurl.com/repo',),
- parse_url('svn+ssh://user2@svn.testurl.com/repo'))
- self.assertEqual(
+ ('svn+ssh://user2@svn.testurl.com/repo', ))
+ self.check_parse_url(
('bob', None, 'svn+ssh://bob@svn.testurl.com/repo',),
- parse_url('svn+ssh://user2@svn.testurl.com/repo', 'bob'))
+ ('svn+ssh://user2@svn.testurl.com/repo', 'bob', ))
def test_user_password_url(self):
- self.assertEqual(
+ self.check_parse_url(
('joe', 't3stpw', 'https://svn.testurl.com/repo'),
- parse_url('https://joe:t3stpw@svn.testurl.com/repo'))
- self.assertEqual(
+ ('https://joe:t3stpw@svn.testurl.com/repo', ))
+ self.check_parse_url(
('bob', '123abc', 'https://svn.testurl.com/repo'),
- parse_url('https://joe:t3stpw@svn.testurl.com/repo', 'bob', '123abc'))
+ ('https://joe:t3stpw@svn.testurl.com/repo', 'bob', '123abc', ))
def test_url_rewriting(self):
ui = test_util.ui.ui()
ui.setconfig('hgsubversion', 'username', 'bob')
repo = svnrepo.svnremoterepo(ui, 'svn+ssh://joe@foo/bar')
self.assertEqual('svn+ssh://bob@foo/bar', repo.svnauth[0])
+ self.assertEqual('svn+ssh://bob@foo/bar', repo.svnurl)
repo = svnrepo.svnremoterepo(ui, 'svn+http://joe@foo/bar')
self.assertEqual(('http://foo/bar', 'bob', None), repo.svnauth)
+ self.assertEqual('http://foo/bar', repo.svnurl)
repo = svnrepo.svnremoterepo(ui, 'svn+https://joe@foo/bar')
self.assertEqual(('https://foo/bar', 'bob', None), repo.svnauth)
+ self.assertEqual('https://foo/bar', repo.svnurl)
def test_quoting(self):
ui = self.ui()
@@ -72,3 +75,11 @@ class TestSubversionUrls(test_util.TestBase):
repo1 = svnrepo.svnremoterepo(ui, repo_url + subdir)
repo2 = svnrepo.svnremoterepo(ui, repo_url + quoted_subdir)
self.assertEqual(repo1.svnurl, repo2.svnurl)
+
+ def check_parse_url(self, expected, args):
+ self.assertEqual(expected, parse_url(*args))
+ if len(args) == 1:
+ repo = svnrepo.svnremoterepo(self.ui(), path=args[0])
+ self.assertEqual(expected[2], repo.svnauth[0])
+ self.assertEqual(expected[2], repo.svnurl)
+
diff --git a/tests/test_util.py b/tests/test_util.py
index b24c35a..7c78c51 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -28,6 +28,7 @@ from mercurial import util as hgutil
from mercurial import extensions
from hgsubversion import compathacks
+from hgsubversion import svnrepo
from hgsubversion import svnwrap
try:
@@ -39,14 +40,12 @@ except ImportError:
try:
SkipTest = unittest.SkipTest
except AttributeError:
- try:
- from unittest2 import SkipTest
- except ImportError:
- try:
- from nose import SkipTest
- except ImportError:
- SkipTest = None
+ if 'nose' in sys.modules:
+ SkipTest = sys.modules['nose'].SkipTest
+ else:
+ SkipTest = None
+from hgsubversion import svnwrap
from hgsubversion import util
from hgsubversion import svnwrap
@@ -210,15 +209,25 @@ def getlocalpeer(repo):
localrepo = repo
return localrepo
-def repolen(repo):
+def repolen(repo, svnonly=False):
"""Naively calculate the amount of available revisions in a repository.
this is usually equal to len(repo) -- except in the face of
obsolete revisions.
+
+ if svnonly is true, only count revisions converted from Subversion.
"""
# kind of nasty way of calculating the length, but fortunately,
# our test repositories tend to be rather small
- return len([r for r in repo])
+ revs = set(repo)
+
+ if obsolete:
+ revs -= obsolete.getrevs(repo, 'obsolete')
+
+ if svnonly:
+ revs = set(r for r in revs if util.getsvnrev(repo[r]))
+
+ return len(revs)
def _makeskip(name, message):
if SkipTest:
@@ -284,10 +293,12 @@ def testui(stupid=False, layout='auto', startrev=0):
u = ui.ui()
bools = {True: 'true', False: 'false'}
u.setconfig('ui', 'quiet', bools[True])
+ u.setconfig('ui', 'username', 'automated tests')
u.setconfig('extensions', 'hgsubversion', '')
u.setconfig('hgsubversion', 'stupid', bools[stupid])
u.setconfig('hgsubversion', 'layout', layout)
u.setconfig('hgsubversion', 'startrev', startrev)
+ u.setconfig('devel', 'all-warnings', True)
return u
def dispatch(cmd):
@@ -534,12 +545,8 @@ class TestBase(unittest.TestCase):
'''
path = self._makerepopath()
assert not os.path.exists(path)
- subprocess.call(['svnadmin', 'create', path,],
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- inp = open(os.path.join(FIXTURES, fixture_name))
- proc = subprocess.Popen(['svnadmin', 'load', path,], stdin=inp,
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- proc.communicate()
+ with open(os.path.join(FIXTURES, fixture_name)) as inp:
+ svnwrap.create_and_load(path, inp)
return path
def load_repo_tarball(self, fixture_name):
@@ -596,7 +603,7 @@ class TestBase(unittest.TestCase):
return hg.repository(testui(), self.wc_path)
- def load_and_fetch(self, fixture_name, *args, **opts):
+ def load(self, fixture_name):
if fixture_name.endswith('.svndump'):
repo_path = self.load_svndump(fixture_name)
elif fixture_name.endswith('tar.gz'):
@@ -604,6 +611,10 @@ class TestBase(unittest.TestCase):
else:
assert False, 'Unknown fixture type'
+ return repo_path
+
+ def load_and_fetch(self, fixture_name, *args, **opts):
+ repo_path = self.load(fixture_name)
return self.fetch(repo_path, *args, **opts), repo_path
def _load_fixture_and_fetch(self, *args, **kwargs):
@@ -704,7 +715,8 @@ class TestBase(unittest.TestCase):
changed + removed,
filectxfn,
'an_author',
- '2008-10-07 20:59:48 -0500')
+ '2008-10-07 20:59:48 -0500',
+ {'branch': parentctx.branch()})
nodeid = repo.commitctx(ctx)
repo = self.repo
hg.clean(repo, nodeid)
@@ -773,5 +785,21 @@ files: {files}
commands.log(_ui, repo, rev=None, template=templ, graph=True)
return _ui.popbuffer()
+ def svnlog(self, repo=None):
+ '''log of the remote Subversion repository corresponding to repo
+
+ In order to make the format suitable for direct comparison in
+ tests, we exclude dates and convert the path operations into
+ a tuple.
+ '''
+
+ if repo is None:
+ repo = self.repo
+
+ return [(r.revnum, r.message,
+ dict((p, (op.action, op.copyfrom_path, int(op.copyfrom_rev)))
+ for (p, op) in r.paths.items()))
+ for r in svnrepo.svnremoterepo(repo.ui).svn.revisions()]
+
def draw(self, repo):
sys.stdout.write(self.getgraph(repo))
diff --git a/tests/test_utility_commands.py b/tests/test_utility_commands.py
index 5b03901..481c64b 100644
--- a/tests/test_utility_commands.py
+++ b/tests/test_utility_commands.py
@@ -128,10 +128,10 @@ class UtilityTests(test_util.TestBase):
def test_missing_metadata(self):
self._load_fixture_and_fetch('two_heads.svndump')
- os.remove(self.repo.join('svn/branch_info'))
+ os.remove(self.repo.vfs.join('svn/branch_info'))
svncommands.updatemeta(self.ui(), self.repo, [])
- test_util.rmtree(self.repo.join('svn'))
+ test_util.rmtree(self.repo.vfs.join('svn'))
self.assertRaises(hgutil.Abort,
self.repo.svnmeta)
self.assertRaises(hgutil.Abort,
@@ -141,7 +141,7 @@ class UtilityTests(test_util.TestBase):
svncommands.genignore,
self.ui(), repo=self.repo, args=[])
- os.remove(self.repo.join('hgrc'))
+ os.remove(self.repo.vfs.join('hgrc'))
self.assertRaises(hgutil.Abort,
self.repo.svnmeta)
self.assertRaises(hgutil.Abort,
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..aafef92
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,18 @@
+[tox]
+envlist = hg32,hg33,hg34,hg35,hg36,hg37,hg38,hg39,hg40,hg41
+
+[testenv]
+deps=
+ nose
+ hg32: Mercurial==3.2.4
+ hg33: Mercurial==3.3.3
+ hg34: Mercurial==3.4.2
+ hg35: Mercurial==3.5.2
+ hg36: Mercurial==3.6.3
+ hg37: Mercurial==3.7.3
+ hg38: Mercurial==3.8.3
+ hg39: Mercurial==3.8.3
+ hg40: Mercurial==4.0.2
+ hg41: Mercurial==4.1
+ subvertpy
+commands=nosetests {posargs}