diff options
Diffstat (limited to 'hgsubversion')
-rw-r--r-- | hgsubversion/__init__.py | 49 | ||||
-rw-r--r-- | hgsubversion/editor.py | 9 | ||||
-rw-r--r-- | hgsubversion/help/subversion.rst | 19 | ||||
-rw-r--r-- | hgsubversion/layouts/custom.py | 4 | ||||
-rw-r--r-- | hgsubversion/maps.py | 872 | ||||
-rw-r--r-- | hgsubversion/replay.py | 17 | ||||
-rw-r--r-- | hgsubversion/stupid.py | 8 | ||||
-rw-r--r-- | hgsubversion/svncommands.py | 46 | ||||
-rw-r--r-- | hgsubversion/svnexternals.py | 111 | ||||
-rw-r--r-- | hgsubversion/svnmeta.py | 97 | ||||
-rw-r--r-- | hgsubversion/svnrepo.py | 2 | ||||
-rw-r--r-- | hgsubversion/svnwrap/common.py | 3 | ||||
-rw-r--r-- | hgsubversion/svnwrap/subvertpy_wrapper.py | 30 | ||||
-rw-r--r-- | hgsubversion/svnwrap/svn_swig_wrapper.py | 31 | ||||
-rw-r--r-- | hgsubversion/util.py | 72 | ||||
-rw-r--r-- | hgsubversion/wrappers.py | 67 |
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: |