# HG changeset patch # User Augie Fackler # Date 1407856121 14400 # Node ID 5c29173759610c399c6982b7fb6645931aa50b5d # Parent 9490a30529358456e47cf7c3e1f0322ecf93724e# Parent 807c443928d4b033eb7254100de1f48a47e93e4a Merge with stable. diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -9,6 +9,7 @@ 0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 07234759a3f750029ccaa001837d42fa12dd33ee 1.4 77b22e5b4ea6c248e079afd0f1e544cb5690ce20 1.5 d0f3a5c2cb56ce65d9ef1c611c8bfbebdc3bef34 1.5.1 +7d47a0f731354505ed9ae8d60d2a6996e8c3294f 1.6 8caf1226adecb322e90ddb3817c604fa2fe8a66d 1.6.1 36f6d51b4edc31f1f9ce2d0d02965a85dd26a455 1.6.2 46523cdfd3b0cee0bf1366ab587686bb65211747 1.6.3 diff --git a/hgsubversion/__init__.py b/hgsubversion/__init__.py --- a/hgsubversion/__init__.py +++ b/hgsubversion/__init__.py @@ -33,27 +33,9 @@ demandimport.ignore.extend([ 'svn.ra', ]) -try: - from mercurial import templatekw - # force demandimport to load templatekw - templatekw.keywords -except ImportError: - templatekw = None - -try: - from mercurial import revset - # force demandimport to load revset - revset.methods -except ImportError: - revset = None - -try: - from mercurial import subrepo - # require svnsubrepo and hg >= 1.7.1 - subrepo.svnsubrepo - hgutil.checknlink -except (ImportError, AttributeError), e: - subrepo = None +from mercurial import templatekw +from mercurial import revset +from mercurial import subrepo import svncommands import util @@ -124,9 +106,8 @@ except AttributeError: except ImportError: pass -def extsetup(): +def extsetup(ui): """insert command wrappers for a bunch of commands""" - # add the ui argument to this function once we drop support for 1.3 docvals = {'extension': 'hgsubversion'} for cmd, (generic, target, fixdoc, ppopts, opts) in wrapcmds.iteritems(): @@ -163,20 +144,13 @@ def extsetup(): lambda: open(os.path.join(helpdir, 'subversion.rst')).read()), ) - # in 1.6 and earler the help table is a tuple - if getattr(help.helptable, 'extend', None): - help.helptable.extend(entries) - else: - help.helptable = help.helptable + entries + help.helptable.extend(entries) - if templatekw: - templatekw.keywords.update(util.templatekeywords) + templatekw.keywords.update(util.templatekeywords) - if revset: - revset.symbols.update(util.revsets) + revset.symbols.update(util.revsets) - if subrepo: - subrepo.types['hgsubversion'] = svnexternals.svnsubrepo + subrepo.types['hgsubversion'] = svnexternals.svnsubrepo def reposetup(ui, repo): if repo.local(): @@ -184,7 +158,7 @@ def reposetup(ui, repo): for tunnel in ui.configlist('hgsubversion', 'tunnels'): hg.schemes['svn+' + tunnel] = svnrepo - if revset and ui.configbool('hgsubversion', 'nativerevs'): + if ui.configbool('hgsubversion', 'nativerevs'): extensions.wrapfunction(revset, 'stringset', util.revset_stringset) _old_local = hg.schemes['file'] diff --git a/hgsubversion/compathacks.py b/hgsubversion/compathacks.py --- a/hgsubversion/compathacks.py +++ b/hgsubversion/compathacks.py @@ -1,4 +1,5 @@ -"""Functions to work around API changes inside Mercurial.""" +"""Functions to work around API changes.""" + def branchset(repo): """Return the set of branches present in a repo. diff --git a/hgsubversion/help/subversion.rst b/hgsubversion/help/subversion.rst --- a/hgsubversion/help/subversion.rst +++ b/hgsubversion/help/subversion.rst @@ -45,8 +45,8 @@ issue ``hg clone http://python-nose.goog works with any directory with a Subversion repository, and is known as a single directory clone. Normally, converted changesets will be marked as belonging to the ``default`` branch, but this can be changed by using the ``-b/--branch`` -option when using Mercurial 1.5 or later. To force single directory clone, use -hgsubversion.layout option (see below for detailed help) :: +option. To force single directory clone, use hgsubversion.layout option (see +below for detailed help) :: $ hg clone --layout single svn+http://python-nose.googlecode.com/svn nose-hg @@ -85,8 +85,6 @@ An example:: $ hg log --template='{rev}:{node|short} {author|user}\nsvn: {svnrev}\n' -The template keywords are available when using Mercurial 1.5 or later. - For finding changesets from Subversion, hgsubversion extends revsets to provide two new selectors: @@ -100,9 +98,7 @@ For example:: $ hg log -r 'fromsvn()' $ hg log -r 'svnrev(500)' -Revsets are available when using Mercurial 1.6 or later and are -accepted by several Mercurial commands for specifying revisions. See -``hg help revsets`` for details. +See ``hg help revsets`` for details. Support for externals --------------------- @@ -146,7 +142,7 @@ related Subversion repository. Alternatively, one can use the ``hgsubversion.externals`` in hgrc to specify ``subrepos`` as the externals mode. In this mode, ``.hgsub`` and ``.hgsubstate`` files will be used instead of -``.hgsvnexternals``. This feature requires Mercurial 1.7.1 or later. +``.hgsvnexternals``. Using Subrepositories @@ -183,8 +179,6 @@ with the revision identifier replaced wi This mode has the following limitations: -* Require Mercurial >= 1.7.1 to work correctly on all platforms. - * "hgsubversion" subrepositories require hgsubversion extension to be available. To operate transparently on ``svn:externals`` we have to stay as close as possible to their original property @@ -365,10 +359,10 @@ settings: when necessary. ``hgsubversion.externals`` - Set to ``subrepos`` to switch to subrepos-based externals support - (requires Mercurial 1.7.1 or later.) Default is ``svnexternals``, - which uses a custom hgsubversion-specific format and works on - older versions of Mercurial. Use ``ignore`` to avoid converting externals. + Set to ``subrepos`` to switch to subrepos-based externals support. Default + is ``svnexternals``, which uses a custom hgsubversion-specific format and + works on older versions of Mercurial. Use ``ignore`` to avoid converting + externals. The following options only have an effect on the initial clone of a repository: diff --git a/hgsubversion/layouts/base.py b/hgsubversion/layouts/base.py --- a/hgsubversion/layouts/base.py +++ b/hgsubversion/layouts/base.py @@ -48,7 +48,7 @@ class BaseLayout(object): """ self.__unimplemented('remotepath') - def taglocations(self, meta_data_dir): + def taglocations(self, metapath): """Return a list of locations within svn to search for tags Should be returned in reverse-sorted order. diff --git a/hgsubversion/layouts/custom.py b/hgsubversion/layouts/custom.py --- a/hgsubversion/layouts/custom.py +++ b/hgsubversion/layouts/custom.py @@ -64,7 +64,7 @@ class CustomLayout(base.BaseLayout): subdir += '/' return subdir + self.remotename(branch) - def taglocations(self, meta_data_dir): + def taglocations(self, metapath): return [] def get_path_tag(self, path, taglocations): diff --git a/hgsubversion/layouts/detect.py b/hgsubversion/layouts/detect.py --- a/hgsubversion/layouts/detect.py +++ b/hgsubversion/layouts/detect.py @@ -58,7 +58,7 @@ def layout_from_config(ui, allow_auto=Fa raise hgutil.Abort("unknown layout '%s'" % layout) return layout -def layout_from_file(meta_data_dir, ui=None): +def layout_from_file(metapath, ui=None): """ Load the layout in use from the metadata file. If you pass the ui arg, we will also write the layout to the @@ -67,7 +67,7 @@ def layout_from_file(meta_data_dir, ui=N """ layout = None - layoutfile = os.path.join(meta_data_dir, 'layout') + layoutfile = os.path.join(metapath, 'layout') if os.path.exists(layoutfile): f = open(layoutfile) layout = f.read().strip() diff --git a/hgsubversion/layouts/persist.py b/hgsubversion/layouts/persist.py --- a/hgsubversion/layouts/persist.py +++ b/hgsubversion/layouts/persist.py @@ -7,10 +7,10 @@ to do it. import os.path -def layout_to_file(meta_data_dir, layout): - """Save the given layout to a file under the given meta_data_dir""" +def layout_to_file(metapath, layout): + """Save the given layout to a file under the given metapath""" - layoutfile = os.path.join(meta_data_dir, 'layout') + layoutfile = os.path.join(metapath, 'layout') f = open(layoutfile, 'w') f.write(layout) f.close() diff --git a/hgsubversion/layouts/single.py b/hgsubversion/layouts/single.py --- a/hgsubversion/layouts/single.py +++ b/hgsubversion/layouts/single.py @@ -14,7 +14,7 @@ class SingleLayout(base.BaseLayout): def remotepath(self, branch, subdir='/'): return subdir or '/' - def taglocations(self, meta_data_dir): + def taglocations(self, metapath): return [] def get_path_tag(self, path, taglocations): diff --git a/hgsubversion/layouts/standard.py b/hgsubversion/layouts/standard.py --- a/hgsubversion/layouts/standard.py +++ b/hgsubversion/layouts/standard.py @@ -1,5 +1,4 @@ import os.path -import pickle import base @@ -55,7 +54,7 @@ class StandardLayout(base.BaseLayout): return '%s/%s' % (subdir or '', branchpath) - def taglocations(self, meta_data_dir): + def taglocations(self, metapath): # import late to avoid trouble when running the test suite try: # newer versions of mercurial >= 2.8 will import this because the @@ -66,17 +65,14 @@ class StandardLayout(base.BaseLayout): if self._tag_locations is None: - tag_locations_file = os.path.join(meta_data_dir, 'tag_locations') + tag_locations_file = os.path.join(metapath, 'tag_locations') + self._tag_locations = util.load(tag_locations_file) - if os.path.exists(tag_locations_file): - f = open(tag_locations_file) - self._tag_locations = pickle.load(f) - f.close() - else: + if not self._tag_locations: self._tag_locations = self.ui.configlist('hgsubversion', 'tagpaths', ['tags']) - util.pickle_atomic(self._tag_locations, tag_locations_file) + util.dump(self._tag_locations, tag_locations_file) # ensure nested paths are handled properly self._tag_locations.sort() diff --git a/hgsubversion/maps.py b/hgsubversion/maps.py --- a/hgsubversion/maps.py +++ b/hgsubversion/maps.py @@ -14,12 +14,12 @@ class AuthorMap(dict): 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, path, defaulthost=None): + def __init__(self, meta): '''Initialise a new AuthorMap. The ui argument is used to print diagnostic messages. @@ -27,17 +27,19 @@ class AuthorMap(dict): The path argument is the location of the backing store, typically .hg/svn/authors. ''' - self.ui = ui - self.path = path - self.use_defaultauthors = self.ui.configbool('hgsubversion', 'defaultauthors', True) - self.caseignoreauthors = self.ui.configbool('hgsubversion', 'caseignoreauthors', False) - if defaulthost: - self.defaulthost = '@%s' % defaulthost.lstrip('@') - else: - self.defaulthost = '' + self.meta = meta + self.defaulthost = '' + if meta.defaulthost: + self.defaulthost = '@%s' % meta.defaulthost.lstrip('@') + self.super = super(AuthorMap, self) self.super.__init__() - self.load(path) + self.load(self.meta.authors_file) + + # append authors specified from the commandline + clmap = util.configpath(self.meta.ui, 'authormap') + if clmap: + self.load(clmap) def load(self, path): ''' Load mappings from a file at the specified path. ''' @@ -47,10 +49,10 @@ class AuthorMap(dict): return writing = False - if path != self.path: - writing = open(self.path, 'a') + if path != self.meta.authors_file: + writing = open(self.meta.authors_file, 'a') - self.ui.debug('reading authormap from %s\n' % path) + self.meta.ui.debug('reading authormap from %s\n' % path) f = open(path, 'r') for number, line_org in enumerate(f): @@ -62,21 +64,21 @@ class AuthorMap(dict): src, dst = line.split('=', 1) except (IndexError, ValueError): msg = 'ignoring line %i in author map %s: %s\n' - self.ui.status(msg % (number, path, line.rstrip())) + self.meta.ui.status(msg % (number, path, line.rstrip())) continue src = src.strip() dst = dst.strip() - if self.caseignoreauthors: + if self.meta.caseignoreauthors: src = src.lower() if writing: if not src in self: - self.ui.debug('adding author %s to author map\n' % src) + 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.ui.status(msg % (self[src], dst, src)) + self.meta.ui.status(msg % (self[src], dst, src)) writing.write(line_org) self[src] = dst @@ -92,21 +94,20 @@ class AuthorMap(dict): if author is None: author = '(no author)' - if self.caseignoreauthors: + search_author = author + if self.meta.caseignoreauthors: search_author = author.lower() - else: - search_author = author if search_author in self: result = self.super.__getitem__(search_author) - elif self.use_defaultauthors: + elif self.meta.defaultauthors: self[author] = result = '%s%s' % (author, self.defaulthost) msg = 'substituting author "%s" for default "%s"\n' - self.ui.debug(msg % (author, result)) + self.meta.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)) + self.meta.ui.debug('mapping author "%s" to "%s"\n' % (author, result)) return result def reverselookup(self, author): @@ -126,27 +127,23 @@ class Tags(dict): """ VERSION = 2 - @classmethod - def filepath(cls, repo): - return os.path.join(repo.path, 'svn', 'tagmap') - - def __init__(self, repo, endrev=None): + def __init__(self, meta, endrev=None): dict.__init__(self) - self.path = self.filepath(repo) + self.meta = meta self.endrev = endrev - if os.path.isfile(self.path): - self._load(repo) + if os.path.isfile(self.meta.tagfile): + self._load() else: self._write() - def _load(self, repo): - f = open(self.path) + def _load(self): + f = open(self.meta.tagfile) ver = int(f.readline()) if ver < self.VERSION: - repo.ui.status('tag map outdated, running rebuildmeta...\n') + self.meta.ui.status('tag map outdated, running rebuildmeta...\n') f.close() - os.unlink(self.path) - svncommands.rebuildmeta(repo.ui, repo, ()) + os.unlink(self.meta.tagfile) + svncommands.rebuildmeta(self.meta.ui, self.meta.repo, ()) return elif ver != self.VERSION: raise hgutil.Abort('tagmap too new -- please upgrade') @@ -163,7 +160,7 @@ class Tags(dict): def _write(self): assert self.endrev is None - f = open(self.path, 'w') + f = open(self.meta.tagfile, 'w') f.write('%s\n' % self.VERSION) f.close() @@ -184,7 +181,7 @@ class Tags(dict): if not tag: raise hgutil.Abort('tag cannot be empty') ha, revision = info - f = open(self.path, 'a') + f = open(self.meta.tagfile, 'a') f.write('%s %s %s\n' % (node.hex(ha), revision, tag)) f.close() dict.__setitem__(self, tag, ha) @@ -194,32 +191,15 @@ class RevMap(dict): VERSION = 1 - def __init__(self, repo): + def __init__(self, meta): dict.__init__(self) - self.path = self.mappath(repo) - self.repo = repo - self.ypath = os.path.join(repo.path, 'svn', 'lastpulled') - # TODO(durin42): Consider moving management of the youngest - # file to svnmeta itself rather than leaving it here. - # must load youngest file first, or else self._load() can - # clobber the info - _yonngest_str = util.load_string(self.ypath, '0') - self._youngest = int(_yonngest_str.strip()) - self.oldest = 0 - if os.path.isfile(self.path): + self.meta = meta + + if os.path.isfile(self.meta.revmap_file): self._load() else: self._write() - def _set_youngest(self, rev): - self._youngest = max(self._youngest, rev) - util.save_string(self.ypath, str(self._youngest) + '\n') - - def _get_youngest(self): - return self._youngest - - youngest = property(_get_youngest, _set_youngest) - def hashes(self): return dict((v, k) for (k, v) in self.iteritems()) @@ -227,14 +207,10 @@ class RevMap(dict): check = lambda x: x[0][1] == branch and x[0][0] < rev.revnum return sorted(filter(check, self.iteritems()), reverse=True) - @staticmethod - def mappath(repo): - return os.path.join(repo.path, 'svn', 'rev_map') - @classmethod - def readmapfile(cls, repo, missingok=True): + def readmapfile(cls, path, missingok=True): try: - f = open(cls.mappath(repo)) + f = open(path) except IOError, err: if not missingok or err.errno != errno.ENOENT: raise @@ -245,34 +221,34 @@ class RevMap(dict): return f def _load(self): - for l in self.readmapfile(self.repo): + for l in self.readmapfile(self.meta.revmap_file): revnum, ha, branch = l.split(' ', 2) if branch == '\n': branch = None else: branch = branch[:-1] revnum = int(revnum) - if revnum > self.youngest or not self.youngest: - self.youngest = revnum - if revnum < self.oldest or not self.oldest: - self.oldest = revnum + 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 dict.__setitem__(self, (revnum, branch), node.bin(ha)) def _write(self): - f = open(self.path, 'w') + f = open(self.meta.revmap_file, 'w') f.write('%s\n' % self.VERSION) f.close() def __setitem__(self, key, ha): revnum, branch = key - f = open(self.path, 'a') + f = open(self.meta.revmap_file, 'a') b = branch or '' f.write(str(revnum) + ' ' + node.hex(ha) + ' ' + b + '\n') f.close() - if revnum > self.youngest or not self.youngest: - self.youngest = revnum - if revnum < self.oldest or not self.oldest: - self.oldest = revnum + 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 dict.__setitem__(self, (revnum, branch), ha) @@ -280,7 +256,7 @@ class FileMap(object): VERSION = 1 - def __init__(self, ui, path): + def __init__(self, meta): '''Initialise a new FileMap. The ui argument is used to print diagnostic messages. @@ -288,15 +264,19 @@ class FileMap(object): The path argument is the location of the backing store, typically .hg/svn/filemap. ''' - self.ui = ui - self.path = path + self.meta = meta self.include = {} self.exclude = {} - if os.path.isfile(self.path): + if os.path.isfile(self.meta.filemap_file): self._load() else: self._write() + # append file mapping specified from the commandline + clmap = util.configpath(self.meta.ui, 'filemap') + if clmap: + self.load(clmap) + def _rpairs(self, name): e = len(name) while e != -1: @@ -335,19 +315,19 @@ class FileMap(object): mapping = getattr(self, m) if path in mapping: msg = 'duplicate %s entry in %s: "%s"\n' - self.ui.status(msg % (m, fn, path)) + self.meta.ui.status(msg % (m, fn, path)) return bits = m.rstrip('e'), path - self.ui.debug('%sing %s\n' % bits) + self.meta.ui.debug('%sing %s\n' % bits) # respect rule order mapping[path] = len(self) - if fn != self.path: - f = open(self.path, 'a') + if fn != self.meta.filemap_file: + f = open(self.meta.filemap_file, 'a') f.write(m + ' ' + path + '\n') f.close() def load(self, fn): - self.ui.debug('reading file map from %s\n' % fn) + self.meta.ui.debug('reading file map from %s\n' % fn) f = open(fn, 'r') self.load_fd(f, fn) f.close() @@ -363,22 +343,22 @@ class FileMap(object): if cmd in ('include', 'exclude'): self.add(fn, cmd, path) continue - self.ui.warn('unknown filemap command %s\n' % cmd) + self.meta.ui.warn('unknown filemap command %s\n' % cmd) except IndexError: msg = 'ignoring bad line in filemap %s: %s\n' - self.ui.warn(msg % (fn, line.rstrip())) + self.meta.ui.warn(msg % (fn, line.rstrip())) def _load(self): - self.ui.debug('reading in-repo file map from %s\n' % self.path) - f = open(self.path) + 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.path) + self.load_fd(f, self.meta.filemap_file) f.close() def _write(self): - f = open(self.path, 'w') + f = open(self.meta.filemap_file, 'w') f.write('%s\n' % self.VERSION) f.close() @@ -392,12 +372,16 @@ class BranchMap(dict): changes on other will now be on default (have no branch name set). ''' - def __init__(self, ui, path): - self.ui = ui - self.path = path + def __init__(self, meta): + self.meta = meta self.super = super(BranchMap, self) self.super.__init__() - self.load(path) + 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.''' @@ -405,10 +389,10 @@ class BranchMap(dict): return writing = False - if path != self.path: - writing = open(self.path, 'a') + if path != self.meta.branchmap_file: + writing = open(self.meta.branchmap_file, 'a') - self.ui.debug('reading branchmap from %s\n' % path) + self.meta.ui.debug('reading branchmap from %s\n' % path) f = open(path, 'r') for number, line in enumerate(f): @@ -423,12 +407,12 @@ class BranchMap(dict): src, dst = line.split('=', 1) except (IndexError, ValueError): msg = 'ignoring line %i in branch map %s: %s\n' - self.ui.status(msg % (number, path, line.rstrip())) + self.meta.ui.status(msg % (number, path, line.rstrip())) continue src = src.strip() dst = dst.strip() - self.ui.debug('adding branch %s to branch map\n' % src) + self.meta.ui.debug('adding branch %s to branch map\n' % src) if not dst: # prevent people from assuming such lines are valid @@ -437,7 +421,7 @@ class BranchMap(dict): % (number, path)) elif src in self and dst != self[src]: msg = 'overriding branch: "%s" to "%s" (%s)\n' - self.ui.status(msg % (self[src], dst, src)) + self.meta.ui.status(msg % (self[src], dst, src)) self[src] = dst f.close() @@ -454,12 +438,16 @@ class TagMap(dict): the other tag will not be reflected in the hg repository. ''' - def __init__(self, ui, path): - self.ui = ui - self.path = path + def __init__(self, meta): + self.meta = meta self.super = super(TagMap, self) self.super.__init__() - self.load(path) + 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.''' @@ -467,10 +455,10 @@ class TagMap(dict): return writing = False - if path != self.path: - writing = open(self.path, 'a') + if path != self.meta.tagmap_file: + writing = open(self.meta.tagmap_file, 'a') - self.ui.debug('reading tag renames from %s\n' % path) + self.meta.ui.debug('reading tag renames from %s\n' % path) f = open(path, 'r') for number, line in enumerate(f): @@ -485,16 +473,16 @@ class TagMap(dict): src, dst = line.split('=', 1) except (IndexError, ValueError): msg = 'ignoring line %i in tag renames %s: %s\n' - self.ui.status(msg % (number, path, line.rstrip())) + self.meta.ui.status(msg % (number, path, line.rstrip())) continue src = src.strip() dst = dst.strip() - self.ui.debug('adding tag %s to tag renames\n' % src) + 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.ui.status(msg % (self[src], dst, src)) + self.meta.ui.status(msg % (self[src], dst, src)) self[src] = dst f.close() diff --git a/hgsubversion/replay.py b/hgsubversion/replay.py --- a/hgsubversion/replay.py +++ b/hgsubversion/replay.py @@ -65,13 +65,13 @@ def _convert_rev(ui, meta, svn, r, tbdel editor.current.rev = r editor.setsvn(svn) - if firstrun and meta.revmap.oldest <= 0: + if firstrun and meta.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.revmap.oldest) + svn.get_replay(r.revnum, editor, meta.firstpulled) editor.close() current = editor.current diff --git a/hgsubversion/stupid.py b/hgsubversion/stupid.py --- a/hgsubversion/stupid.py +++ b/hgsubversion/stupid.py @@ -158,27 +158,8 @@ def filteriterhunks(meta): applycurrent = False # Passing False instead of textmode because we should never # be ignoring EOL type. - if iterhunks.func_code.co_argcount == 1: - # Since 1.9 (28762bb767dc) - fp = args[0] - gen = iterhunks(fp) - else: - ui, fp = args[:2] - if len(args) > 2: - sourcefile = args[2] - else: - sourcefile = kwargs.get('sourcefile', None) - if len(args) > 3: - textmode = args[3] - else: - textmode = kwargs.get('textmode', False) - if not iterhunks.func_defaults: - # Since 1.7 (cfedc529e4a1) - gen = iterhunks(ui, fp) - elif len(iterhunks.func_defaults) == 1: - gen = iterhunks(ui, fp, sourcefile) - else: - gen = iterhunks(ui, fp, sourcefile, textmode) + fp = args[0] + gen = iterhunks(fp) for data in gen: if data[0] == 'file': if data[1][1] in meta.filemap: @@ -736,7 +717,7 @@ def convert_rev(ui, meta, svn, r, tbdelt # 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.revmap.oldest > 0 and + incremental = (meta.firstpulled > 0 and parentctx.rev() != node.nullrev and not firstrun) diff --git a/hgsubversion/svncommands.py b/hgsubversion/svncommands.py --- a/hgsubversion/svncommands.py +++ b/hgsubversion/svncommands.py @@ -1,6 +1,5 @@ import os import posixpath -import cPickle as pickle import sys import traceback import urlparse @@ -19,6 +18,7 @@ import svnrepo import util import svnexternals import verify +import svnmeta def updatemeta(ui, repo, args, **opts): @@ -39,22 +39,6 @@ def rebuildmeta(ui, repo, args, unsafe_s return _buildmeta(ui, repo, args, partial=False, skipuuid=unsafe_skip_uuid_check) -def read_if_exists(path): - try: - fp = open(path, 'rb') - d = fp.read() - fp.close() - return d - except IOError, err: - if err.errno != errno.ENOENT: - raise - -def write_if_needed(path, content): - if read_if_exists(path) != content: - fp = open(path, 'wb') - fp.write(content) - fp.close() - def _buildmeta(ui, repo, args, partial=False, skipuuid=False): if repo is None: @@ -70,19 +54,13 @@ def _buildmeta(ui, repo, args, partial=F raise hgutil.Abort('rebuildmeta takes 1 or no arguments') url = repo.ui.expandpath(dest or repo.ui.config('paths', 'default-push') or repo.ui.config('paths', 'default') or '') - svnmetadir = os.path.join(repo.path, 'svn') - if not os.path.exists(svnmetadir): - os.makedirs(svnmetadir) - uuidpath = os.path.join(svnmetadir, 'uuid') - uuid = read_if_exists(uuidpath) - - subdirpath = os.path.join(svnmetadir, 'subdir') - subdir = read_if_exists(subdirpath) + + meta = svnmeta.SVNMeta(repo, skiperrorcheck=True) + svn = None - if subdir is None: + if meta.subdir is None: svn = svnrepo.svnremoterepo(ui, url).svn - subdir = svn.subdir - open(subdirpath, 'wb').write(subdir.strip('/')) + meta.subdir = svn.subdir youngest = 0 startrev = 0 @@ -90,16 +68,18 @@ def _buildmeta(ui, repo, args, partial=F branchinfo = {} if partial: try: - youngestpath = os.path.join(svnmetadir, 'lastpulled') + # we can't use meta.lastpulled here because we are bootstraping the + # lastpulled and want to keep the cached value on disk during a + # partial rebuild foundpartialinfo = False + youngestpath = os.path.join(meta.metapath, 'lastpulled') if os.path.exists(youngestpath): - youngest = int(util.load_string(youngestpath).strip()) - sofar = list(maps.RevMap.readmapfile(repo)) + 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] startrev = repo[lasthash].rev() + 1 - branchinfo = pickle.load(open(os.path.join(svnmetadir, - 'branch_info'))) + branchinfo = util.load(meta.branch_info_file) foundpartialinfo = True if not foundpartialinfo: ui.status('missing some metadata -- doing a full rebuild\n') @@ -111,16 +91,12 @@ def _buildmeta(ui, repo, args, partial=F except AttributeError: ui.status('no metadata available -- doing a full rebuild\n') - - lastpulled = open(os.path.join(svnmetadir, 'lastpulled'), 'wb') - revmap = open(os.path.join(svnmetadir, 'rev_map'), 'w') - revmap.write('1\n') + revmap = open(meta.revmap_file, 'w') + revmap.write('%d\n' % maps.RevMap.VERSION) revmap.writelines(sofar) last_rev = -1 - tagfile = os.path.join(svnmetadir, 'tagmap') - if not partial and os.path.exists(maps.Tags.filepath(repo)) : - os.unlink(maps.Tags.filepath(repo)) - tags = maps.Tags(repo) + if not partial and os.path.exists(meta.tagfile): + os.unlink(meta.tagfile) layout = None layoutobj = None @@ -164,7 +140,7 @@ def _buildmeta(ui, repo, args, partial=F else: closed.add(parentctx.rev()) - lastpulled.write(str(youngest) + '\n') + meta.lastpulled = youngest ui.progress('prepare', None, total=numrevs) for rev in xrange(startrev, len(repo)): @@ -197,11 +173,13 @@ def _buildmeta(ui, repo, args, partial=F # number. tagging = int(convinfo[40:].split('@')[1]) tagrev = max(tagged, tagging) - tags[tag] = node.bin(ha), tagrev + meta.tags[tag] = node.bin(ha), tagrev # check that the conversion metadata matches expectations assert convinfo.startswith('svn:') revpath, revision = convinfo[40:].split('@') + # use tmp variable for testing + subdir = meta.subdir if subdir and subdir[0] != '/': subdir = '/' + subdir if subdir and subdir[-1] == '/': @@ -212,16 +190,16 @@ def _buildmeta(ui, repo, args, partial=F if layout is None: layout = layouts.detect.layout_from_commit(subdir, revpath, ctx.branch(), ui) - existing_layout = layouts.detect.layout_from_file(svnmetadir) + existing_layout = layouts.detect.layout_from_file(meta.metapath) if layout != existing_layout: - layouts.persist.layout_to_file(svnmetadir, layout) + layouts.persist.layout_to_file(meta.metapath, layout) layoutobj = layouts.layout_from_name(layout, ui) elif layout == 'single': assert (subdir or '/') == revpath, ('Possible layout detection' ' defect in replay') # write repository uuid if required - if uuid is None or validateuuid: + if meta.uuid is None or validateuuid: validateuuid = False uuid = convinfo[4:40] if not skipuuid: @@ -230,7 +208,7 @@ def _buildmeta(ui, repo, args, partial=F if uuid != svn.uuid: raise hgutil.Abort('remote svn repository identifier ' 'does not match') - write_if_needed(uuidpath, uuid) + meta.uuid = uuid # don't reflect closed branches if (ctx.extra().get('close') and not ctx.files() or @@ -241,7 +219,7 @@ def _buildmeta(ui, repo, args, partial=F # find commitpath, write to revmap commitpath = revpath[len(subdir)+1:] - tag_locations = layoutobj.taglocations(svnmetadir) + tag_locations = layoutobj.taglocations(meta.metapath) found_tag = False for location in tag_locations: if commitpath.startswith(location + '/'): @@ -304,9 +282,7 @@ def _buildmeta(ui, repo, args, partial=F ui.progress('rebuild', None, total=numrevs) # save off branch info - branchinfofile = open(os.path.join(svnmetadir, 'branch_info'), 'w') - pickle.dump(branchinfo, branchinfofile) - branchinfofile.close() + util.dump(branchinfo, meta.branch_info_file) def help_(ui, args=None, **opts): diff --git a/hgsubversion/svnexternals.py b/hgsubversion/svnexternals.py --- a/hgsubversion/svnexternals.py +++ b/hgsubversion/svnexternals.py @@ -3,26 +3,14 @@ import cStringIO import os, re, shutil, stat, subprocess from mercurial import util as hgutil from mercurial.i18n import _ +from mercurial import subrepo try: - from mercurial import subrepo - # require svnsubrepo and hg >= 1.7.1 - subrepo.svnsubrepo - hgutil.checknlink -except (ImportError, AttributeError), e: - subrepo = None - -passpegrev = True # see svnsubrepo below -try: - canonpath = hgutil.canonpath + from mercurial import scmutil + canonpath = scmutil.canonpath except (ImportError, AttributeError): - passpegrev = False - try: - from mercurial import scmutil - canonpath = scmutil.canonpath - except (ImportError, AttributeError): - from mercurial import pathutil - canonpath = pathutil.canonpath + from mercurial import pathutil + canonpath = pathutil.canonpath import util @@ -398,58 +386,53 @@ def parse(ui, ctx): raise hgutil.Abort(_('unknown externals modes: %s') % mode) return external -if subrepo: - class svnsubrepo(subrepo.svnsubrepo): - def __init__(self, ctx, path, state): - state = (state[0].split(':', 1)[1], state[1]) - super(svnsubrepo, self).__init__(ctx, path, state) - - def get(self, state, *args, **kwargs): - # Resolve source first - line = state[0].split(':', 1)[1] - source, pegrev = parsedefinition(line)[2:4] - try: - # Getting the root SVN repository URL is expensive. - # Assume the externals is absolute. - source = resolvesource(self._ui, None, source) - except RelativeSourceError: - svnurl = self._ctx._repo.ui.expandpath('default') - svnroot = getsvninfo(util.normalize_url(svnurl))[1] - source = resolvesource(self._ui, svnroot, source) - # hg < 1.9 svnsubrepo calls "svn checkout" with --rev - # only, so peg revisions are correctly used. 1.9 and - # higher, append the rev as a peg revision to the source - # URL, so we cannot add our own. We assume that "-r10 - # url@2" will be similar to "url@10" most of the time. - if pegrev is not None and passpegrev: - source = source + '@' + pegrev - state = (source, state[1]) - # hg-1.7.4-c19b9282d3a7 introduced the overwrite argument - return super(svnsubrepo, self).get(state, *args, **kwargs) - - def dirty(self, ignoreupdate=False): - # You cannot compare anything with HEAD. Just accept it - # can be anything. - if hasattr(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 - return True - - def commit(self, text, user, date): - rev = super(svnsubrepo, self).commit(text, user, date) - # Keep unversioned externals unversioned - if self._state[1] == 'HEAD': - rev = 'HEAD' - return rev - - def basestate(self): - # basestate() was introduced by bcb973abcc0b in 2.2 - if self._state[1] == 'HEAD': - return 'HEAD' - return super(svnsubrepo, self).basestate() +class svnsubrepo(subrepo.svnsubrepo): + def __init__(self, ctx, path, state): + state = (state[0].split(':', 1)[1], state[1]) + super(svnsubrepo, self).__init__(ctx, path, state) + + def get(self, state, *args, **kwargs): + # Resolve source first + line = state[0].split(':', 1)[1] + source, pegrev = parsedefinition(line)[2:4] + try: + # Getting the root SVN repository URL is expensive. + # Assume the externals is absolute. + source = resolvesource(self._ui, None, source) + except RelativeSourceError: + svnurl = self._ctx._repo.ui.expandpath('default') + svnroot = getsvninfo(util.normalize_url(svnurl))[1] + source = resolvesource(self._ui, svnroot, source) + # hg 1.9 and higher, append the rev as a peg revision to + # the source URL, so we cannot add our own. We assume + # that "-r10 url@2" will be similar to "url@10" most of + # the time. + state = (source, state[1]) + return super(svnsubrepo, self).get(state, *args, **kwargs) + + def dirty(self, ignoreupdate=False): + # You cannot compare anything with HEAD. Just accept it + # can be anything. + if hasattr(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 + return True + + def commit(self, text, user, date): + rev = super(svnsubrepo, self).commit(text, user, date) + # Keep unversioned externals unversioned + if self._state[1] == 'HEAD': + rev = 'HEAD' + return rev + + def basestate(self): + # basestate() was introduced by bcb973abcc0b in 2.2 + if self._state[1] == 'HEAD': + return 'HEAD' + return super(svnsubrepo, self).basestate() diff --git a/hgsubversion/svnmeta.py b/hgsubversion/svnmeta.py --- a/hgsubversion/svnmeta.py +++ b/hgsubversion/svnmeta.py @@ -1,4 +1,3 @@ -import cPickle as pickle import posixpath import os import tempfile @@ -17,7 +16,7 @@ import editor class SVNMeta(object): - def __init__(self, repo, uuid=None, subdir=None): + def __init__(self, repo, uuid=None, subdir=None, skiperrorcheck=False): """path is the path to the target hg repo. subdir is the subdirectory of the edits *on the svn server*. @@ -26,52 +25,115 @@ class SVNMeta(object): self.ui = repo.ui self.repo = repo self.path = os.path.normpath(repo.join('..')) + self._skiperror = skiperrorcheck - if not os.path.isdir(self.meta_data_dir): - os.makedirs(self.meta_data_dir) + if not os.path.isdir(self.metapath): + os.makedirs(self.metapath) self.uuid = uuid self.subdir = subdir - self.revmap = maps.RevMap(repo) - - author_host = self.ui.config('hgsubversion', 'defaulthost', uuid) - authors = util.configpath(self.ui, 'authormap') - self.usebranchnames = self.ui.configbool('hgsubversion', - 'usebranchnames', True) - branchmap = util.configpath(self.ui, 'branchmap') - tagmap = util.configpath(self.ui, 'tagmap') - filemap = util.configpath(self.ui, 'filemap') - - self.branches = {} - if os.path.exists(self.branch_info_file): - f = open(self.branch_info_file) - self.branches = pickle.load(f) - f.close() + self._revmap = None + self.firstpulled = 0 + + self._gen_cachedconfig('lastpulled', 0, configname=False) + self._gen_cachedconfig('defaultauthors', True) + self._gen_cachedconfig('caseignoreauthors', False) + self._gen_cachedconfig('defaulthost', self.uuid) + self._gen_cachedconfig('usebranchnames', True) + + self.branches = util.load(self.branch_info_file) or {} self.prevbranches = dict(self.branches) - self.tags = maps.Tags(repo) - self._layout = layouts.detect.layout_from_file(self.meta_data_dir, + self._tags = None + self._layout = layouts.detect.layout_from_file(self.metapath, ui=self.repo.ui) self._layoutobj = None - self.authors = maps.AuthorMap(self.ui, self.authors_file, - defaulthost=author_host) - if authors: self.authors.load(authors) + self._authors = None - self.branchmap = maps.BranchMap(self.ui, self.branchmapfile) - if branchmap: - self.branchmap.load(branchmap) + self._branchmap = None - self.tagmap = maps.TagMap(self.ui, self.tagmapfile) - if tagmap: - self.tagmap.load(tagmap) + self._tagmap = None - self.filemap = maps.FileMap(self.ui, self.filemap_file) - if filemap: - self.filemap.load(filemap) + self._filemap = None self.lastdate = '1970-01-01 00:00:00 -0000' self.addedtags = {} self.deletedtags = {} + def _get_cachedconfig(self, name, filename, configname, default): + """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' + filename: name of file in .hg/svn + configname: commandline option name + default: default value + """ + varname = '_' + name + if getattr(self, varname) is None: + # construct the file path from metapath (e.g. .hg/svn) plus the + # filename + f = os.path.join(self.metapath, filename) + + # load the config property (i.e. command-line or .hgrc) + c = None + if configname: + # a little awkward but we need to convert the option from a + # string to whatever type the default value is, so we use the + # type of `default` to determine with ui.config method to call + c = None + if isinstance(default, bool): + c = self.ui.configbool('hgsubversion', configname, default) + elif isinstance(default, int): + c = self.ui.configint('hgsubversion', configname, default) + elif isinstance(default, list): + c = self.ui.configlist('hgsubversion', configname, default) + else: + c = self.ui.config('hgsubversion', configname, default) + + # load the value from disk + val = util.load(f, default=default) + + # prefer the non-default, and the one sent from command-line + if c is not None and c != val and c != default: + val = c + + # set the value as the one from disk (or default if not found) + setattr(self, varname, val) + + # save the value to disk by using the setter property + setattr(self, name, val) + + return getattr(self, varname) + + def _set_cachedconfig(self, value, name, filename): + varname = '_' + name + f = os.path.join(self.metapath, filename) + setattr(self, varname, value) + util.dump(value, f) + + def _gen_cachedconfig(self, name, default=None, filename=None, + configname=None): + """Generate an attribute for reading (and caching) config data. + + This method constructs a new attribute on self with the given name. + The actual value from the config file will be read lazily, and then + cached once that read has occurred. No cache invalidation will happen, + so within a session these values shouldn't be required to mutate. + """ + setattr(SVNMeta, '_' + name, None) + if filename is None: + filename = name + if configname is None: + configname = name + prop = property(lambda x: x._get_cachedconfig(name, + filename, + configname, + default), + lambda x, y: x._set_cachedconfig(y, + name, + filename)) + setattr(SVNMeta, name, prop) + @property def layout(self): # this method can't determine the layout, but it needs to be @@ -79,7 +141,7 @@ class SVNMeta(object): # gets called if not self._layout or self._layout == 'auto': self._layout = layouts.detect.layout_from_config(self.repo.ui) - layouts.persist.layout_to_file(self.meta_data_dir, self._layout) + layouts.persist.layout_to_file(self.metapath, self._layout) return self._layout @property @@ -101,24 +163,23 @@ class SVNMeta(object): if subdir: subdir = '/'.join(p for p in subdir.split('/') if p) - subdirfile = os.path.join(self.meta_data_dir, 'subdir') + self.__subdir = None + subdirfile = os.path.join(self.metapath, 'subdir') if os.path.isfile(subdirfile): - stored_subdir = open(subdirfile).read() + stored_subdir = util.load(subdirfile) assert stored_subdir is not None if subdir is None: self.__subdir = stored_subdir - elif subdir != stored_subdir: + elif subdir and subdir != stored_subdir: raise hgutil.Abort('unable to work on a different path in the ' 'repository') else: self.__subdir = subdir elif subdir is not None: - f = open(subdirfile, 'w') - f.write(subdir) - f.close() + util.dump(subdir, subdirfile) self.__subdir = subdir - else: + elif not self._skiperror: raise hgutil.Abort("hgsubversion metadata unavailable; " "please run 'hg svn rebuildmeta'") @@ -130,19 +191,18 @@ class SVNMeta(object): return self.__uuid def _set_uuid(self, uuid): - uuidfile = os.path.join(self.meta_data_dir, 'uuid') + self.__uuid = None + uuidfile = os.path.join(self.metapath, 'uuid') if os.path.isfile(uuidfile): - stored_uuid = open(uuidfile).read() + stored_uuid = util.load(uuidfile) assert stored_uuid if uuid and uuid != stored_uuid: raise hgutil.Abort('unable to operate on unrelated repository') self.__uuid = uuid or stored_uuid elif uuid: - f = open(uuidfile, 'w') - f.write(uuid) - f.close() + util.dump(uuid, uuidfile) self.__uuid = uuid - else: + elif not self._skiperror: raise hgutil.Abort("hgsubversion metadata unavailable; " "please run 'hg svn rebuildmeta'") @@ -150,29 +210,74 @@ class SVNMeta(object): 'Error-checked UUID of source Subversion repository.') @property - def meta_data_dir(self): + def metapath(self): return os.path.join(self.path, '.hg', 'svn') @property def branch_info_file(self): - return os.path.join(self.meta_data_dir, 'branch_info') + return os.path.join(self.metapath, 'branch_info') @property def authors_file(self): - return os.path.join(self.meta_data_dir, 'authors') + return os.path.join(self.metapath, 'authors') + + @property + def authors(self): + if self._authors is None: + self._authors = maps.AuthorMap(self) + return self._authors @property def filemap_file(self): - return os.path.join(self.meta_data_dir, 'filemap') + return os.path.join(self.metapath, 'filemap') + + @property + def filemap(self): + if self._filemap is None: + self._filemap = maps.FileMap(self) + return self._filemap + + @property + def branchmap_file(self): + return os.path.join(self.metapath, 'branchmap') + + @property + def branchmap(self): + if self._branchmap is None: + self._branchmap = maps.BranchMap(self) + return self._branchmap @property - def branchmapfile(self): - return os.path.join(self.meta_data_dir, 'branchmap') + def tagfile(self): + # called tagmap for backwards compatibility + return os.path.join(self.metapath, 'tagmap') @property - def tagmapfile(self): + def tags(self): + if self._tags is None: + self._tags = maps.Tags(self) + return self._tags + + @property + def tagmap_file(self): # called tag-renames for backwards compatibility - return os.path.join(self.meta_data_dir, 'tag-renames') + return os.path.join(self.metapath, 'tag-renames') + + @property + def tagmap(self): + if self._tagmap is None: + self._tagmap = maps.TagMap(self) + return self._tagmap + + @property + def revmap_file(self): + return os.path.join(self.metapath, 'rev_map') + + @property + def revmap(self): + if self._revmap is None: + self._revmap = maps.RevMap(self) + return self._revmap def fixdate(self, date): if date is not None: @@ -187,7 +292,7 @@ class SVNMeta(object): '''Save the Subversion metadata. This should really be called after every revision is created. ''' - util.pickle_atomic(self.branches, self.branch_info_file) + util.dump(self.branches, self.branch_info_file) def localname(self, path): """Compute the local name for a branch located at path. @@ -247,7 +352,7 @@ class SVNMeta(object): @property def taglocations(self): - return self.layoutobj.taglocations(self.meta_data_dir) + return self.layoutobj.taglocations(self.metapath) def get_path_tag(self, path): """If path could represent the path to a tag, returns the @@ -395,7 +500,7 @@ class SVNMeta(object): return node.hex(self.revmap[tagged]) tag = fromtag # Reference an existing tag - limitedtags = maps.Tags(self.repo, endrev=number - 1) + limitedtags = maps.Tags(self, 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 --- a/hgsubversion/svnrepo.py +++ b/hgsubversion/svnrepo.py @@ -71,10 +71,6 @@ def generate_repo_class(ui, repo): """ original = getattr(repo, fn.__name__, None) - # remove when dropping support for hg < 1.6. - if original is None and fn.__name__ == 'findoutgoing': - return - def wrapper(self, *args, **opts): capable = getattr(args[0], 'capable', lambda x: False) if capable('subversion'): @@ -112,8 +108,8 @@ def generate_repo_class(ui, repo): def findoutgoing(self, remote, base=None, heads=None, force=False): return wrappers.findoutgoing(repo, remote, heads, force) - def svnmeta(self, uuid=None, subdir=None): - return svnmeta.SVNMeta(self, uuid, subdir) + def svnmeta(self, uuid=None, subdir=None, skiperrorcheck=False): + return svnmeta.SVNMeta(self, uuid, subdir, skiperrorcheck) repo.__class__ = svnlocalrepo diff --git a/hgsubversion/util.py b/hgsubversion/util.py --- a/hgsubversion/util.py +++ b/hgsubversion/util.py @@ -1,8 +1,9 @@ -import cPickle as pickle +import compathacks import errno import re import os import urllib +import json from mercurial import cmdutil from mercurial import error @@ -120,57 +121,74 @@ def normalize_url(url): url = '%s#%s' % (url, checkout) return url +def _scrub(data): + if not data and not isinstance(data, list): + return '' + return data + +def _descrub(data): + if isinstance(data, list): + return tuple(data) + if data == '': + return None + return data + +def _convert(input, visitor): + if isinstance(input, dict): + scrubbed = {} + d = dict([(_convert(key, visitor), _convert(value, visitor)) + for key, value in input.iteritems()]) + for key, val in d.iteritems(): + scrubbed[visitor(key)] = visitor(val) + return scrubbed + elif isinstance(input, list): + return [_convert(element, visitor) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + return input + +def dump(data, file_path): + """Serialize some data to a path atomically. + + This is present because I kept corrupting my revmap by managing to hit ^C + during the serialization of that file. + """ + f = hgutil.atomictempfile(file_path, 'w+b', 0644) + json.dump(_convert(data, _scrub), f) + f.close() -def load_string(file_path, default=None, limit=1024): +def load(file_path, default=None, resave=True): + """Deserialize some data from a path. + """ + data = default if not os.path.exists(file_path): - return default + return data + + f = open(file_path) try: - f = open(file_path, 'r') - ret = f.read(limit) + data = _convert(json.load(f), _descrub) f.close() - except: - return default - if ret == '': - return default - return ret - - -def save_string(file_path, string): - if string is None: - string = "" - f = open(file_path, 'wb') - f.write(str(string)) - f.close() - -def pickle_atomic(data, file_path): - """pickle some data to a path atomically. + except ValueError: + try: + # Ok, JSON couldn't be loaded, so we'll try the old way of using pickle + data = compathacks.pickle_load(f) + except: + # well, pickle didn't work either, so we reset the file pointer and + # read the string + f.seek(0) + data = f.read() - This is present because I kept corrupting my revmap by managing to hit ^C - during the pickle of that file. - """ - f = hgutil.atomictempfile(file_path, 'w+b', 0644) - pickle.dump(data, f) - # Older versions of hg have .rename() instead of .close on - # atomictempfile. - if getattr(hgutil.atomictempfile, 'rename', False): - f.rename() - else: + # convert the file to json immediately f.close() + if resave: + dump(data, file_path) + return data def parseurl(url, heads=[]): - parsed = hg.parseurl(url, heads) - if len(parsed) == 3: - # old hg, remove when we can be 1.5-only - svn_url, heads, checkout = parsed - else: - svn_url, heads = parsed - if isinstance(heads, tuple) and len(heads) == 2: - # hg 1.6 or later - _junk, heads = heads - if heads: - checkout = heads[0] - else: - checkout = None + checkout = None + svn_url, (_junk, heads) = hg.parseurl(url, heads) + if heads: + checkout = heads[0] return svn_url, heads, checkout @@ -325,9 +343,11 @@ 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(repo, missingok=False)) + for l in maps.RevMap.readmapfile(meta.revmap_file, + missingok=False)) return filter(svnrevs.__contains__, subset) except IOError, err: if err.errno != errno.ENOENT: @@ -350,8 +370,9 @@ def revset_svnrev(repo, subset, x): rev = rev + ' ' revs = [] + meta = repo.svnmeta(skiperrorcheck=True) try: - for l in maps.RevMap.readmapfile(repo, missingok=False): + 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() diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -97,7 +97,7 @@ def incoming(orig, ui, repo, origsource= meta = repo.svnmeta(svn.uuid, svn.subdir) ui.status('incoming changes from %s\n' % other.svnurl) - svnrevisions = list(svn.revisions(start=meta.revmap.youngest)) + svnrevisions = list(svn.revisions(start=meta.lastpulled)) if opts.get('newest_first'): svnrevisions.reverse() # Returns 0 if there are incoming changes, 1 otherwise. @@ -181,7 +181,8 @@ def push(repo, dest, force, revs): checkpush = getattr(repo, 'checkpush', None) if checkpush: try: - # The checkpush function changed as of e10000369b47 in mercurial + # The checkpush function changed as of e10000369b47 (first + # in 3.0) in mercurial from mercurial.exchange import pushoperation pushop = pushoperation(repo, dest, force, revs, False) checkpush(pushop) @@ -390,7 +391,7 @@ def pull(repo, source, heads=[], force=F meta.branchmap['default'] = branch ui = repo.ui - start = meta.revmap.youngest + start = meta.lastpulled origrevcount = len(meta.revmap) if start <= 0: @@ -484,7 +485,7 @@ def pull(repo, source, heads=[], force=F util.swap_out_encoding(old_encoding) if lastpulled is not None: - meta.revmap.youngest = lastpulled + meta.lastpulled = lastpulled revisions = len(meta.revmap) - oldrevisions if revisions == 0: @@ -573,11 +574,7 @@ def clone(orig, ui, source, dest=None, * data = {} def hgclonewrapper(orig, ui, *args, **opts): - if getattr(hg, 'peer', None): - # Since 1.9 (d976542986d2) - origsource = args[1] - else: - origsource = args[0] + origsource = args[1] if isinstance(origsource, str): source, branch, checkout = util.parseurl(ui.expandpath(origsource), @@ -612,14 +609,11 @@ def clone(orig, ui, source, dest=None, * dstrepo = data.get('dstrepo') srcrepo = data.get('srcrepo') + dst = dstrepo.local() if dstrepo.local() and srcrepo.capable('subversion'): dst = dstrepo.local() - if isinstance(dst, bool): - # Apparently <= hg@1.9 - fd = dstrepo.opener("hgrc", "a", text=True) - else: - fd = dst.opener("hgrc", "a", text=True) + fd = dst.opener("hgrc", "a", text=True) preservesections = set(s for s, v in optionmap.itervalues()) preservesections |= extrasections for section in preservesections: diff --git a/tests/comprehensive/test_custom_layout.py b/tests/comprehensive/test_custom_layout.py --- a/tests/comprehensive/test_custom_layout.py +++ b/tests/comprehensive/test_custom_layout.py @@ -1,5 +1,4 @@ import os -import pickle import sys import unittest diff --git a/tests/comprehensive/test_rebuildmeta.py b/tests/comprehensive/test_rebuildmeta.py --- a/tests/comprehensive/test_rebuildmeta.py +++ b/tests/comprehensive/test_rebuildmeta.py @@ -1,5 +1,4 @@ import os -import pickle import unittest import sys @@ -19,6 +18,7 @@ from mercurial import ui from hgsubversion import compathacks from hgsubversion import svncommands from hgsubversion import svnmeta +from hgsubversion import util # These test repositories have harmless skew in rebuildmeta for the # last-pulled-rev because the last rev in svn causes absolutely no @@ -110,23 +110,29 @@ def _run_assertions(self, name, single, for tf in ('lastpulled', 'rev_map', 'uuid', 'tagmap', 'layout', 'subdir',): stf = os.path.join(src.path, 'svn', tf) - self.assertTrue(os.path.isfile(stf), '%r is missing!' % stf) + # the generation of tagmap is lazy so it doesn't strictly need to exist + # if it's not being used + if not stf.endswith('tagmap'): + self.assertTrue(os.path.isfile(stf), '%r is missing!' % stf) dtf = os.path.join(dest.path, 'svn', tf) - self.assertTrue(os.path.isfile(dtf), '%r is missing!' % tf) - old, new = open(stf).read(), open(dtf).read() + old, new = None, None + if not dtf.endswith('tagmap'): + self.assertTrue(os.path.isfile(dtf), '%r is missing!' % tf) + if os.path.isfile(stf) and os.path.isfile(dtf): + old, new = util.load(stf, resave=False), util.load(dtf, resave=False) if tf == 'lastpulled' and (name, self.stupid, single) in expect_youngest_skew: self.assertNotEqual(old, new, 'rebuildmeta unexpected match on youngest rev!') continue - self.assertMultiLineEqual(old, new, tf + ' differs') + self.assertEqual(old, new, tf + ' differs') try: self.assertEqual(src.branchmap(), dest.branchmap()) except AttributeError: # hg 2.8 and earlier self.assertEqual(src.branchtags(), dest.branchtags()) - srcbi = pickle.load(open(os.path.join(src.path, 'svn', 'branch_info'))) - destbi = pickle.load(open(os.path.join(dest.path, 'svn', 'branch_info'))) + srcbi = util.load(os.path.join(src.path, 'svn', 'branch_info')) + destbi = util.load(os.path.join(dest.path, 'svn', 'branch_info')) self.assertEqual(sorted(srcbi.keys()), sorted(destbi.keys())) revkeys = svnmeta.SVNMeta(dest).revmap.keys() for branch in destbi: diff --git a/tests/comprehensive/test_stupid_pull.py b/tests/comprehensive/test_stupid_pull.py --- a/tests/comprehensive/test_stupid_pull.py +++ b/tests/comprehensive/test_stupid_pull.py @@ -1,5 +1,4 @@ import os -import pickle import sys import unittest diff --git a/tests/comprehensive/test_verify_and_startrev.py b/tests/comprehensive/test_verify_and_startrev.py --- a/tests/comprehensive/test_verify_and_startrev.py +++ b/tests/comprehensive/test_verify_and_startrev.py @@ -1,5 +1,4 @@ import os -import pickle import sys import unittest diff --git a/tests/test_fetch_mappings.py b/tests/test_fetch_mappings.py --- a/tests/test_fetch_mappings.py +++ b/tests/test_fetch_mappings.py @@ -84,10 +84,15 @@ class MapTests(test_util.TestBase): def test_author_map_no_overwrite(self): cwd = os.path.dirname(__file__) orig = os.path.join(cwd, 'fixtures', 'author-map-test.txt') - new = open(self.authors, 'w') + # create a fake hgsubversion repo + repopath = os.path.join(self.wc_path, '.hg') + repopath = os.path.join(repopath, 'svn') + if not os.path.isdir(repopath): + os.makedirs(repopath) + new = open(os.path.join(repopath, 'authors'), 'w') new.write(open(orig).read()) new.close() - test = maps.AuthorMap(self.ui(), self.authors) + test = maps.AuthorMap(self.repo.svnmeta(skiperrorcheck=True)) fromself = set(test) test.load(orig) all_tests = set(test) @@ -285,21 +290,6 @@ class MapTests(test_util.TestBase): for r in repo: self.assertEquals(verify.verify(ui, repo, rev=r), 0) - def test_branchmap_no_replacement(self): - ''' - test that empty mappings are rejected - - Empty mappings are lines like 'this ='. The most sensible thing to do - is to not convert the 'this' branches. Until we can do that, we settle - with aborting. - ''' - repo_path = self.load_svndump('propset-branch.svndump') - branchmap = open(self.branchmap, 'w') - branchmap.write("closeme =\n") - branchmap.close() - self.assertRaises(hgutil.Abort, - maps.BranchMap, self.ui(), self.branchmap) - def test_tagmap(self): repo_path = self.load_svndump('basic_tag_tests.svndump') tagmap = open(self.tagmap, 'w') diff --git a/tests/test_push_command.py b/tests/test_push_command.py --- a/tests/test_push_command.py +++ b/tests/test_push_command.py @@ -552,7 +552,7 @@ class PushTests(test_util.TestBase): copied=False) ctx = context.memctx(repo, (repo['default'].node(), node.nullid), - 'message', + 'mutate already-special file alpha', ['alpha', ], file_callback2, 'author', @@ -577,7 +577,7 @@ class PushTests(test_util.TestBase): copied=False) ctx = context.memctx(repo, (repo['default'].node(), node.nullid), - 'message', + 'convert alpha back to regular file', ['alpha', ], file_callback3, 'author', diff --git a/tests/test_tags.py b/tests/test_tags.py --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -175,6 +175,8 @@ rename a tag repo = self._load_fixture_and_fetch('tag_name_same_as_branch.svndump') tm = os.path.join(repo.path, 'svn', 'tagmap') open(tm, 'w').write('1\n') + # force tags to load since it is lazily loaded when needed + repo.svnmeta().tags commands.pull(repo.ui, repo) self.assertEqual(open(tm).read().splitlines()[0], '2')