# HG changeset patch # User Augie Fackler # Date 1396661322 14400 # Node ID c55b94dc3a4831208b7b1888d1d6f2f1496480e6 # Parent 8be51900386839f205726bd9edefa5dc1c2fe610# Parent 61d4fb78370bc72160966fb4dc0685b5b90ae5e9 Merge with stable. diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -9,3 +9,4 @@ 0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 07234759a3f750029ccaa001837d42fa12dd33ee 1.4 77b22e5b4ea6c248e079afd0f1e544cb5690ce20 1.5 d0f3a5c2cb56ce65d9ef1c611c8bfbebdc3bef34 1.5.1 +7d47a0f731354505ed9ae8d60d2a6996e8c3294f 1.6 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,11 +1,17 @@ -"""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. - - Works around branchtags() vanishing between 2.8 and 2.9. - """ - try: - return set(repo.branchmap()) - except AttributeError: - return set(repo.branchtags()) + """Return the set of branches present in a repo. + + Works around branchtags() vanishing between 2.8 and 2.9. + """ + try: + return set(repo.branchmap()) + except AttributeError: + return set(repo.branchtags()) + +def pickle_load(f): + import cPickle as pickle + f.seek(0) + return pickle.load(f) 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 @@ -203,8 +203,7 @@ class RevMap(dict): # 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._youngest = util.load(self.ypath, 0) self.oldest = 0 if os.path.isfile(self.path): self._load() @@ -213,7 +212,7 @@ class RevMap(dict): def _set_youngest(self, rev): self._youngest = max(self._youngest, rev) - util.save_string(self.ypath, str(self._youngest) + '\n') + util.dump(self._youngest, self.ypath) def _get_youngest(self): return self._youngest 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: 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,36 +54,29 @@ 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 sofar = [] branchinfo = {} + youngestpath = os.path.join(meta.metapath, 'lastpulled') if partial: try: - youngestpath = os.path.join(svnmetadir, 'lastpulled') foundpartialinfo = False if os.path.exists(youngestpath): - youngest = int(util.load_string(youngestpath).strip()) + youngest = util.load(youngestpath) sofar = list(maps.RevMap.readmapfile(repo)) 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 +88,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 +137,7 @@ def _buildmeta(ui, repo, args, partial=F else: closed.add(parentctx.rev()) - lastpulled.write(str(youngest) + '\n') + util.dump(youngest, youngestpath) ui.progress('prepare', None, total=numrevs) for rev in xrange(startrev, len(repo)): @@ -197,11 +170,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 +187,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 +205,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 +216,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 +279,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 @@ -16,7 +15,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*. @@ -25,12 +24,13 @@ 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) + self._revmap = None author_host = self.ui.config('hgsubversion', 'defaulthost', uuid) authors = util.configpath(self.ui, 'authormap') @@ -40,14 +40,10 @@ class SVNMeta(object): 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.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 @@ -55,11 +51,11 @@ class SVNMeta(object): defaulthost=author_host) if authors: self.authors.load(authors) - self.branchmap = maps.BranchMap(self.ui, self.branchmapfile) + self.branchmap = maps.BranchMap(self.ui, self.branchmap_file) if branchmap: self.branchmap.load(branchmap) - self.tagmap = maps.TagMap(self.ui, self.tagmapfile) + self.tagmap = maps.TagMap(self.ui, self.tagmap_file) if tagmap: self.tagmap.load(tagmap) @@ -78,7 +74,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 @@ -100,24 +96,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'") @@ -129,19 +124,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'") @@ -149,29 +143,50 @@ 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 filemap_file(self): - return os.path.join(self.meta_data_dir, 'filemap') + return os.path.join(self.metapath, 'filemap') + + @property + def branchmap_file(self): + return os.path.join(self.metapath, 'branchmap') + + @property + def tagfile(self): + # called tagmap for backwards compatibility + return os.path.join(self.metapath, 'tagmap') @property - def branchmapfile(self): - return os.path.join(self.meta_data_dir, 'branchmap') + def tags(self): + if self._tags is None: + self._tags = maps.Tags(self.repo) + return self._tags @property - def tagmapfile(self): + 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 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.repo) + return self._revmap def fixdate(self, date): if date is not None: @@ -186,7 +201,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. @@ -246,7 +261,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 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. -def load_string(file_path, default=None, limit=1024): + 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(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 diff --git a/hgsubversion/wrappers.py b/hgsubversion/wrappers.py --- a/hgsubversion/wrappers.py +++ b/hgsubversion/wrappers.py @@ -563,11 +563,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), @@ -602,14 +598,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_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')