summaryrefslogtreecommitdiff
path: root/hgsubversion
diff options
context:
space:
mode:
Diffstat (limited to 'hgsubversion')
-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
16 files changed, 1058 insertions, 379 deletions
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: