# HG changeset patch # User Augie Fackler # Date 1243394520 18000 # Node ID 13998e698d3e1433f6e13a790e94317372a5c894 # Parent 47c0110046dcc48c107e156c8d1e0e9a2f4f7299# Parent 4f4db3d2fdbb66b282109ba694c8577353cd4f34 Merge with crew. diff --git a/__init__.py b/hgsubversion/__init__.py rename from __init__.py rename to hgsubversion/__init__.py --- a/__init__.py +++ b/hgsubversion/__init__.py @@ -1,16 +1,24 @@ '''integration with Subversion repositories -This extension allows Mercurial to act as a Subversion client, for -fast incremental, bidirectional updates. +hgsubversion is an extension for Mercurial that allows it to act as a Subversion +client, offering fast, incremental and bidirectional synchronisation. -It is *not* ready yet for production use. You should only be using -this if you're ready to hack on it, and go diving into the internals -of Mercurial and/or Subversion. +Please note that hgsubversion should not be considered stable software. It is +not feature complete, and neither guarantees of functionality nor future +compatability can be offered. It is, however, quite useful for the cases where +it works, and a good platform for further improvements. -Before using hgsubversion, it is *strongly* encouraged to run the +Before using hgsubversion, we *strongly* encourage running the automated tests. See `README' in the hgsubversion directory for details. + +The operation of hgsubversion can be customised with the following variables: + + + ''' +# TODO: The docstring should be slightly more helpful, and at least mention all +# configuration settings we support import os import sys @@ -18,21 +26,42 @@ import traceback from mercurial import commands from mercurial import extensions +from mercurial import hg from mercurial import util as hgutil from svn import core import svncommands -import tag_repo +import svnrepo import util import wrappers import svnexternals -def reposetup(ui, repo): - if not util.is_svn_repo(repo): - return +schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+file') + +optionmap = { + 'tagpaths': ('hgsubversion', 'tagpaths'), + 'authors': ('hgsubversion', 'authormap'), + 'filemap': ('hgsubversion', 'filemap'), + 'stupid': ('hgsubversion', 'stupid'), + 'defaulthost': ('hgsubversion', 'defaulthost'), + 'defaultauthors': ('hgsubversion', 'defaultauthors'), + 'usebranchnames': ('hgsubversion', 'usebranchnames'), +} + +def wrapper(orig, ui, repo, *args, **opts): + """ + Subversion repositories are also supported for this command. See + `hg help %(extension)s` for details. + """ + for opt, (section, name) in optionmap.iteritems(): + if opt in opts: + if isinstance(repo, str): + ui.setconfig(section, name, opts.pop(opt)) + else: + repo.ui.setconfig(section, name, opts.pop(opt)) - repo.__class__ = tag_repo.generate_repo_class(ui, repo) + return orig(ui, repo, *args, **opts) def uisetup(ui): """Do our UI setup. @@ -51,25 +80,20 @@ def uisetup(ui): wrappers.diff) entry[1].append(('', 'svn', None, "show svn-style diffs, default against svn parent")) - entry = extensions.wrapcommand(commands.table, 'push', - wrappers.push) - entry[1].append(('', 'svn', None, "push to subversion")) - entry[1].append(('', 'svn-stupid', None, "use stupid replay during push to svn")) - entry = extensions.wrapcommand(commands.table, 'pull', - wrappers.pull) - entry[1].append(('', 'svn', None, "pull from subversion")) - entry[1].append(('', 'svn-stupid', None, "use stupid replay during pull from svn")) - - entry = extensions.wrapcommand(commands.table, 'clone', - wrappers.clone) - entry[1].extend([#('', 'skipto-rev', '0', 'skip commits before this revision.'), - ('', 'svn-stupid', False, 'be stupid and use diffy replay.'), - ('', 'svn-tag-locations', 'tags', 'Relative path to Subversion tags.'), - ('', 'svn-authors', '', 'username mapping filename'), - ('', 'svn-filemap', '', - 'remap file to exclude paths or include only certain paths'), - ('', 'svn-no-branchnames', False, "don't record branch names in hg"), - ]) + + newflags = (('A', 'authors', '', 'path to file containing username ' + 'mappings for Subversion sources'), + ('', 'filemap', '', 'path to file containing rules for file ' + 'name mapping used for sources)'), + ('T', 'tagpaths', ['tags'], 'list of paths to search for tags ' + 'in Subversion repositories.')) + extname = 'hgsubversion' + + for command in ['clone']: + doc = wrapper.__doc__.strip() % { 'extension': extname } + getattr(commands, command).__doc__ += doc + entry = extensions.wrapcommand(commands.table, command, wrapper) + entry[1].extend(newflags) try: rebase = extensions.find('rebase') @@ -116,7 +140,12 @@ def svn(ui, repo, subcommand, *args, **o else: raise +def reposetup(ui, repo): + if repo.local(): + svnrepo.generate_repo_class(ui, repo) +for scheme in schemes: + hg.schemes[scheme] = svnrepo cmdtable = { "svn": diff --git a/cmdutil.py b/hgsubversion/cmdutil.py rename from cmdutil.py rename to hgsubversion/cmdutil.py diff --git a/hg_delta_editor.py b/hgsubversion/hg_delta_editor.py rename from hg_delta_editor.py rename to hgsubversion/hg_delta_editor.py --- a/hg_delta_editor.py +++ b/hgsubversion/hg_delta_editor.py @@ -11,6 +11,7 @@ from mercurial import ui from mercurial import util as hgutil from mercurial import revlog from mercurial import node +from mercurial import error from svn import delta from svn import core @@ -74,9 +75,8 @@ class HgChangeReceiver(delta.Editor): def __init__(self, path=None, repo=None, ui_=None, subdir='', author_host='', - tag_locations=['tags'], - authors=None, - filemap=None): + tag_locations=[], + authors=None, filemap=None, uuid=None): """path is the path to the target hg repo. subdir is the subdirectory of the edits *on the svn server*. @@ -87,21 +87,22 @@ class HgChangeReceiver(delta.Editor): if not ui_: ui_ = ui.ui() self.ui = ui_ - if repo: - self.repo = repo - self.path = os.path.normpath(os.path.join(self.repo.path, '..')) - elif path: - self.path = path - self.__setup_repo(path) - else: #pragma: no cover - raise TypeError("Expected either path or repo argument") + self.__setup_repo(repo or path, uuid) + + if not author_host: + author_host = self.ui.config('hgsubversion', 'defaulthost', uuid) + if not authors: + authors = self.ui.config('hgsubversion', 'authormap') + if not filemap: + filemap = self.ui.config('hgsubversion', 'filemap') + if not tag_locations: + tag_locations = self.ui.config('hgsubversion', 'tagpaths', ['tags']) + self.usebranchnames = self.ui.configbool('hgsubversion', + 'usebranchnames', True) self.subdir = subdir if self.subdir and self.subdir[0] == '/': self.subdir = self.subdir[1:] - self.revmap = {} - if os.path.exists(self.revmap_file): - self.revmap = util.parse_revmap(self.revmap_file) self.branches = {} if os.path.exists(self.branch_info_file): f = open(self.branch_info_file) @@ -144,20 +145,31 @@ class HgChangeReceiver(delta.Editor): date = self.lastdate return date - def __setup_repo(self, repo_path): + def __setup_repo(self, arg, uuid): """Verify the repo is going to work out for us. This method will fail an assertion if the repo exists but doesn't have the Subversion metadata. """ - if os.path.isdir(repo_path) and len(os.listdir(repo_path)): - self.repo = hg.repository(self.ui, repo_path) - assert os.path.isfile(self.revmap_file) - assert os.path.isfile(self.svn_url_file) - assert os.path.isfile(self.uuid_file) + if isinstance(arg, basestring): + self.repo = hg.repository(self.ui, arg, + create=(not os.path.exists(arg))) + self.path = os.path.normpath(os.path.join(arg, '..')) + elif arg: + self.repo = arg + self.path = os.path.normpath(os.path.join(self.repo.path, '..')) + else: #pragma: no cover + raise TypeError("editor requires either a path or a repository " + "specified") + + if not os.path.isdir(self.meta_data_dir): + os.makedirs(self.meta_data_dir) + self._set_uuid(uuid) + + if os.path.isfile(self.revmap_file): + self.revmap = util.parse_revmap(self.revmap_file) else: - self.repo = hg.repository(self.ui, repo_path, create=True) - os.makedirs(os.path.dirname(self.uuid_file)) + self.revmap = {} f = open(self.revmap_file, 'w') f.write('%s\n' % util.REVMAP_FILE_VERSION) f.flush() @@ -638,7 +650,7 @@ class HgChangeReceiver(delta.Editor): raise IOError files = parentctx.manifest().keys() extra = {} - if not self.opts.get('svn_no_branchnames', False): + if self.usebranchnames: extra['branch'] = 'closed-branches' current_ctx = context.memctx(self.repo, parents, @@ -660,9 +672,7 @@ class HgChangeReceiver(delta.Editor): revlog.nullid) if parents[0] in closed_revs and branch in self.branches_to_delete: continue - extra = util.build_extra(rev.revnum, branch, - open(self.uuid_file).read(), - self.subdir) + extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir) if branch is not None: if (branch not in self.branches and branch not in self.repo.branchtags()): @@ -684,16 +694,18 @@ class HgChangeReceiver(delta.Editor): is_link = self.current_files_symlink.get(current_file, 'l' in flags) if current_file in self.current_files: data = self.current_files[current_file] - if is_link: - assert data.startswith('link ') + if is_link and data.startswith('link '): data = data[len('link '):] + elif is_link: + raise ValueError('file erronously marked as a link: ' + '%s (%r)' % (current_file, flags)) else: data = parent_ctx.filectx(path).data() return context.memfilectx(path=path, data=data, islink=is_link, isexec=is_exec, copied=copied) - if self.opts.get('svn_no_branchnames', False): + if not self.usebranchnames: extra.pop('branch', None) current_ctx = context.memctx(self.repo, parents, @@ -719,10 +731,8 @@ class HgChangeReceiver(delta.Editor): if self.commit_branches_empty[branch]: #pragma: no cover raise hgutil.Abort('Empty commit to an open branch attempted. ' 'Please report this issue.') - extra = util.build_extra(rev.revnum, branch, - open(self.uuid_file).read(), - self.subdir) - if self.opts.get('svn_no_branchnames', False): + extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir) + if not self.usebranchnames: extra.pop('branch', None) current_ctx = context.memctx(self.repo, (ha, node.nullid), @@ -785,13 +795,31 @@ class HgChangeReceiver(delta.Editor): return self.meta_file_named('rev_map') revmap_file = property(revmap_file) - def svn_url_file(self): - return self.meta_file_named('url') - svn_url_file = property(svn_url_file) + def _get_uuid(self): + return open(self.meta_file_named('uuid')).read() + + def _set_uuid(self, uuid): + if not uuid: + return self._get_uuid() + elif os.path.isfile(self.meta_file_named('uuid')): + stored_uuid = self._get_uuid() + assert stored_uuid + if uuid != stored_uuid: + raise hgutil.Abort('unable to operate on unrelated repository') + else: + return stored_uuid + else: + if uuid: + f = open(self.meta_file_named('uuid'), 'w') + f.write(uuid) + f.flush() + f.close() + return self._get_uuid() + else: + raise hgutil.Abort('unable to operate on unrelated repository') - def uuid_file(self): - return self.meta_file_named('uuid') - uuid_file = property(uuid_file) + uuid = property(_get_uuid, _set_uuid, None, + 'Error-checked UUID of source Subversion repository.') def branch_info_file(self): return self.meta_file_named('branch_info') @@ -805,10 +833,6 @@ class HgChangeReceiver(delta.Editor): return self.meta_file_named('tag_locations') tag_locations_file = property(tag_locations_file) - def url(self): - return open(self.svn_url_file).read() - url = property(url) - def authors_file(self): return self.meta_file_named('authors') authors_file = property(authors_file) diff --git a/maps.py b/hgsubversion/maps.py rename from maps.py rename to hgsubversion/maps.py diff --git a/stupid.py b/hgsubversion/stupid.py rename from stupid.py rename to hgsubversion/stupid.py --- a/stupid.py +++ b/hgsubversion/stupid.py @@ -527,7 +527,7 @@ def svn_server_pull_rev(ui, svn, hg_edit date, extra) branch = extra.get('branch', None) - if hg_editor.opts.get('svn_no_branchnames', False): + if not hg_editor.usebranchnames: extra.pop('branch', None) ha = hg_editor.repo.commitctx(current_ctx) if not branch in hg_editor.branches: @@ -561,7 +561,7 @@ def svn_server_pull_rev(ui, svn, hg_edit closed = hg_editor.repo['closed-branches'].node() parents = (parent, closed) extra = {} - if not hg_editor.opts.get('svn_no_branchnames', False): + if hg_editor.usebranchnames: extra['branch'] = 'closed-branches' current_ctx = context.memctx(hg_editor.repo, parents, diff --git a/svncommands.py b/hgsubversion/svncommands.py rename from svncommands.py rename to hgsubversion/svncommands.py --- a/svncommands.py +++ b/hgsubversion/svncommands.py @@ -30,16 +30,10 @@ def incoming(ui, svn_url, hg_repo_path, author_host=author_host, tag_locations=tag_locations, authors=authors, - filemap=filemap) - if os.path.exists(hg_editor.uuid_file): - uuid = open(hg_editor.uuid_file).read() - assert uuid == svn.uuid - start = hg_editor.last_known_revision() - else: - open(hg_editor.uuid_file, 'w').write(svn.uuid) - open(hg_editor.svn_url_file, 'w').write(svn_url) - initializing_repo = True - start = skipto_rev + filemap=filemap, + uuid=svn.uuid) + start = max(hg_editor.last_known_revision(), skipto_rev) + initializing_repo = (hg_editor.last_known_revision() <= 0) if initializing_repo and start > 0: raise hgutil.Abort('Revision skipping at repository initialization ' @@ -65,9 +59,13 @@ def rebuildmeta(ui, repo, hg_repo_path, """rebuild hgsubversion metadata using values stored in revisions """ if len(args) != 1: - raise hgutil.Abort('You must pass the svn URI used to create this repo.') + url = repo.ui.expandpath(dest or 'default-push', dest or 'default') + else: + url = args[0] + if not (url.startswith('svn+') or url.startswith('svn:')): + raise hgutil.Abort('No valid Subversion URI found; please specify one.') uuid = None - url = args[0].rstrip('/') + url = util.normalize_url(url.rstrip('/')) user, passwd = util.getuserpass(opts) svn = svnwrap.SubversionRepo(url, user, passwd) subdir = svn.subdir @@ -95,9 +93,6 @@ def rebuildmeta(ui, repo, hg_repo_path, if uuid is None: uuid = convinfo[4:40] assert uuid == svn.uuid, 'UUIDs did not match!' - urlfile = open(os.path.join(svnmetadir, 'url'), 'w') - urlfile.write(url) - urlfile.close() uuidfile = open(os.path.join(svnmetadir, 'uuid'), 'w') uuidfile.write(uuid) uuidfile.close() diff --git a/svnexternals.py b/hgsubversion/svnexternals.py rename from svnexternals.py rename to hgsubversion/svnexternals.py --- a/svnexternals.py +++ b/hgsubversion/svnexternals.py @@ -3,6 +3,7 @@ import cStringIO import os, re, shutil, stat, subprocess from mercurial import util as hgutil from mercurial.i18n import _ +from hgsubversion import util class externalsfile(dict): """Map svn directories to lists of externals entries. @@ -160,7 +161,8 @@ def getsvninfo(svnurl): # Yes, this is ugly, but good enough for now args = ['svn', 'info', '--xml', svnurl] shell = os.name == 'nt' - p = subprocess.Popen(args, stdout=subprocess.PIPE, shell=shell) + p = subprocess.Popen(args, shell=shell, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout = p.communicate()[0] if p.returncode: raise hgutil.Abort(_('cannot get information about %s') @@ -246,16 +248,17 @@ class externalsupdater: def updateexternals(ui, args, repo, **opts): """update repository externals """ - if len(args) > 1: + if len(args) > 2: raise hgutil.Abort(_('updateexternals expects at most one changeset')) node = None + if len(args) == 2: + svnurl = util.normalize_url(repo.ui.expandpath(args[0])) + args = args[1:] + else: + svnurl = util.normalize_url(repo.ui.expandpath('default')) if args: node = args[0] - try: - svnurl = file(repo.join('svn/url'), 'rb').read() - except: - raise hgutil.Abort(_('failed to retrieve original svn URL')) svnroot = getsvninfo(svnurl)[1] # Retrieve current externals status diff --git a/tag_repo.py b/hgsubversion/svnrepo.py rename from tag_repo.py rename to hgsubversion/svnrepo.py --- a/tag_repo.py +++ b/hgsubversion/svnrepo.py @@ -1,23 +1,112 @@ +""" +repository class-based interface for hgsubversion + + Copyright (C) 2009, Dan Villiom Podlaski Christiansen + See parent package for licensing. + +Internally, Mercurial assumes that every single repository is a localrepository +subclass: pull() is called on the instance pull *to*, but not the one pulled +*from*. To work around this, we create two classes: + +- svnremoterepo for Subversion repositories, but it doesn't really do anything. +- svnlocalrepo for local repositories which handles both operations on itself -- + the local, hgsubversion-enabled clone -- and the remote repository. Decorators + are used to distinguish and filter these operations from others. +""" + from mercurial import node +from mercurial import util as hgutil +import mercurial.repo import hg_delta_editor +import util +import wrappers +def generate_repo_class(ui, repo): + """ This function generates the local repository wrapper. """ -def tags_from_tag_info(repo): - hg_editor = hg_delta_editor.HgChangeReceiver(repo=repo) - for tag, source in hg_editor.tags.iteritems(): - source_ha = hg_editor.get_parent_revision(source[1]+1, source[0]) - yield 'tag/%s'%tag, node.hex(source_ha) + def localsvn(fn): + """ + Filter for instance methods which only apply to local Subversion + repositories. + """ + if util.is_svn_repo(repo): + return fn + else: + return getattr(repo, fn.__name__) + def remotesvn(fn): + """ + Filter for instance methods which require the first argument + to be a remote Subversion repository instance. + """ + original = getattr(repo.__class__, fn.__name__) + def wrapper(self, *args, **opts): + if 'subversion' in getattr(args[0], 'capabilities', []): + return fn(self, *args, **opts) + else: + return original(self, *args, **opts) + wrapper.__name__ = fn.__name__ + '_wrapper' + wrapper.__doc__ = fn.__doc__ + return wrapper -def generate_repo_class(ui, repo): + class svnlocalrepo(repo.__class__): + @remotesvn + def push(self, remote, force=False, revs=None): + # TODO: pass on revs + wrappers.push(self, dest=remote.svnurl, force=force, revs=None) - class svntagrepo(repo.__class__): + @remotesvn + def pull(self, remote, heads=None, force=False): + try: + lock = self.wlock() + wrappers.pull(self, source=remote.svnurl, rev=heads, force=force) + except KeyboardInterrupt: + pass + finally: + lock.release() + + @localsvn def tags(self): - tags = dict((k, node.bin(v)) - for k,v in tags_from_tag_info(self)) - hg_tags = super(svntagrepo, self).tags() - tags.update(hg_tags) + tags = super(svnlocalrepo, self).tags() + hg_editor = hg_delta_editor.HgChangeReceiver(repo=self) + for tag, source in hg_editor.tags.iteritems(): + target = hg_editor.get_parent_revision(source[1]+1, source[0]) + tags['tag/%s' % tag] = target return tags - return svntagrepo + repo.__class__ = svnlocalrepo + +class svnremoterepo(mercurial.repo.repository): + """ the dumb wrapper for actual Subversion repositories """ + + def __init__(self, ui, path): + self.ui = ui + self.path = path + self.capabilities = set(['lookup', 'subversion']) + + @property + def svnurl(self): + return util.normalize_url(self.path) + + def url(self): + return self.path + + def lookup(self, key): + return key + + def cancopy(self): + return False + + def heads(self, *args, **opts): + """ + Whenever this function is hit, we abort. The traceback is useful for + figuring out where to intercept the functionality. + """ + raise hgutil.Abort('command unavailable for Subversion repositories') + +def instance(ui, url, create): + if create: + raise hgutil.Abort('cannot create new remote Subversion repository') + + return svnremoterepo(ui, url) diff --git a/svnwrap/__init__.py b/hgsubversion/svnwrap/__init__.py rename from svnwrap/__init__.py rename to hgsubversion/svnwrap/__init__.py diff --git a/svnwrap/svn_ctypes_wrapper.py b/hgsubversion/svnwrap/svn_ctypes_wrapper.py rename from svnwrap/svn_ctypes_wrapper.py rename to hgsubversion/svnwrap/svn_ctypes_wrapper.py diff --git a/svnwrap/svn_swig_wrapper.py b/hgsubversion/svnwrap/svn_swig_wrapper.py rename from svnwrap/svn_swig_wrapper.py rename to hgsubversion/svnwrap/svn_swig_wrapper.py diff --git a/svnwrap/tests/__init__.py b/hgsubversion/svnwrap/tests/__init__.py rename from svnwrap/tests/__init__.py rename to hgsubversion/svnwrap/tests/__init__.py diff --git a/svnwrap/tests/fixtures/project_root_at_repo_root.svndump b/hgsubversion/svnwrap/tests/fixtures/project_root_at_repo_root.svndump rename from svnwrap/tests/fixtures/project_root_at_repo_root.svndump rename to hgsubversion/svnwrap/tests/fixtures/project_root_at_repo_root.svndump diff --git a/svnwrap/tests/fixtures/project_root_not_repo_root.svndump b/hgsubversion/svnwrap/tests/fixtures/project_root_not_repo_root.svndump rename from svnwrap/tests/fixtures/project_root_not_repo_root.svndump rename to hgsubversion/svnwrap/tests/fixtures/project_root_not_repo_root.svndump diff --git a/svnwrap/tests/test_svnwrap.py b/hgsubversion/svnwrap/tests/test_svnwrap.py rename from svnwrap/tests/test_svnwrap.py rename to hgsubversion/svnwrap/tests/test_svnwrap.py diff --git a/util.py b/hgsubversion/util.py rename from util.py rename to hgsubversion/util.py --- a/util.py +++ b/hgsubversion/util.py @@ -24,7 +24,7 @@ def version(ui): def normalize_url(svnurl): - if svnurl.startswith('svn+http'): + if svnurl.startswith('svn+') and not svnurl.startswith('svn+ssh'): svnurl = svnurl[4:] url, revs, checkout = hg.parseurl(svnurl) url = url.rstrip('/') diff --git a/utility_commands.py b/hgsubversion/utility_commands.py rename from utility_commands.py rename to hgsubversion/utility_commands.py --- a/utility_commands.py +++ b/hgsubversion/utility_commands.py @@ -7,14 +7,6 @@ import cmdutil import util import hg_delta_editor -def url(ui, repo, hg_repo_path, **opts): - """show the location (URL) of the Subversion repository - """ - hge = hg_delta_editor.HgChangeReceiver(hg_repo_path, - ui_=ui) - ui.status(hge.url, '\n') - - def genignore(ui, repo, hg_repo_path, force=False, **opts): """generate .hgignore from svn:ignore properties. """ @@ -23,8 +15,11 @@ def genignore(ui, repo, hg_repo_path, fo raise hgutil.Abort('not overwriting existing .hgignore, try --force?') ignorefile = open(ignpath, 'w') ignorefile.write('.hgignore\nsyntax:glob\n') - hge = hg_delta_editor.HgChangeReceiver(hg_repo_path, - ui_=ui) + url = util.normalize_url(repo.ui.config('paths', 'default')) + user, passwd = util.getuserpass(opts) + svn = svnwrap.SubversionRepo(url, user, passwd) + hge = hg_delta_editor.HgChangeReceiver(path=hg_repo_path, repo=repo, + ui_=ui, uuid=svn.uuid) svn_commit_hashes = dict(zip(hge.revmap.itervalues(), hge.revmap.iterkeys())) parent = cmdutil.parentrev(ui, repo, hge, svn_commit_hashes) @@ -33,11 +28,8 @@ def genignore(ui, repo, hg_repo_path, fo branchpath = 'trunk' else: branchpath = 'branches/%s' % br - url = hge.url if url[-1] == '/': url = url[:-1] - user, passwd = util.getuserpass(opts) - svn = svnwrap.SubversionRepo(url, user, passwd) dirs = [''] + [d[0] for d in svn.list_files(branchpath, r) if d[1] == 'd'] for dir in dirs: props = svn.list_props('%s/%s/' % (branchpath,dir), r) @@ -53,8 +45,11 @@ def genignore(ui, repo, hg_repo_path, fo def info(ui, repo, hg_repo_path, **opts): """show Subversion details similar to `svn info' """ - hge = hg_delta_editor.HgChangeReceiver(hg_repo_path, - ui_=ui) + url = util.normalize_url(repo.ui.config('paths', 'default')) + user, passwd = util.getuserpass(opts) + svn = svnwrap.SubversionRepo(url, user, passwd) + hge = hg_delta_editor.HgChangeReceiver(path=hg_repo_path, repo=repo, + ui_=ui, uuid=svn.uuid) svn_commit_hashes = dict(zip(hge.revmap.itervalues(), hge.revmap.iterkeys())) parent = cmdutil.parentrev(ui, repo, hge, svn_commit_hashes) @@ -71,7 +66,7 @@ def info(ui, repo, hg_repo_path, **opts) subdir = subdir.replace('branches/../', '') else: branchpath = '/branches/%s' % br - url = hge.url + url = util.normalize_url(repo.ui.config('paths', 'default')) if url[-1] == '/': url = url[:-1] url = '%s%s' % (url, branchpath) @@ -87,7 +82,7 @@ Last Changed Author: %(author)s Last Changed Rev: %(revision)s Last Changed Date: %(date)s\n''' % {'reporoot': reporoot, - 'uuid': open(hge.uuid_file).read(), + 'uuid': hge.uuid, 'url': url, 'author': author, 'revision': r, @@ -125,7 +120,6 @@ def version(ui, **opts): nourl = ['version', 'listauthors'] table = { - 'url': url, 'genignore': genignore, 'info': info, 'listauthors': listauthors, diff --git a/wrappers.py b/hgsubversion/wrappers.py rename from wrappers.py rename to hgsubversion/wrappers.py --- a/wrappers.py +++ b/hgsubversion/wrappers.py @@ -87,24 +87,36 @@ def diff(orig, ui, repo, *args, **opts): })) ui.write(cmdutil.filterdiff(''.join(it), baserev, newrev)) - -def push(orig, ui, repo, dest=None, *args, **opts): +def push(repo, dest="default", force=False, revs=None): """push revisions starting at a specified head back to Subversion. """ - opts.pop('svn', None) # unused in this case - svnurl = repo.ui.expandpath(dest or 'default-push', dest or 'default') - if not cmdutil.issvnurl(svnurl): - return orig(ui, repo, dest=dest, *args, **opts) + assert not revs, 'designated revisions for push remains unimplemented.' + print dest + ui = repo.ui + svnurl = util.normalize_url(repo.ui.expandpath(dest)) old_encoding = util.swap_out_encoding() - hge = hg_delta_editor.HgChangeReceiver(repo=repo) - svnurl = util.normalize_url(svnurl) # split of #rev; TODO: implement --rev/#rev support - svnurl, revs, checkout = hg.parseurl(svnurl, opts.get('rev')) - if svnurl != hge.url: - raise hgutil.Abort('wrong subversion url!') - svn_commit_hashes = dict(zip(hge.revmap.itervalues(), - hge.revmap.iterkeys())) - user, passwd = util.getuserpass(opts) + svnurl, revs, checkout = hg.parseurl(svnurl, revs) + # TODO: do credentials specified in the URL still work? + user = repo.ui.config('hgsubversion', 'username') + passwd = repo.ui.config('hgsubversion', 'password') + svn = svnwrap.SubversionRepo(svnurl, user, passwd) + hge = hg_delta_editor.HgChangeReceiver(repo=repo, uuid=svn.uuid) + + # Check if we are up-to-date with the Subversion repository. + if hge.last_known_revision() != svn.last_changed_rev: + # Based on localrepository.push() in localrepo.py:1559. + # TODO: Ideally, we would behave exactly like other repositories: + # - push everything by default + # - handle additional heads in the same way + # - allow pushing single revisions, branches, tags or heads using + # the -r/--rev flag. + if force: + ui.warn("note: unsynced remote changes!\n") + else: + ui.warn("abort: unsynced remote changes!\n") + return None, 0 + # Strategy: # 1. Find all outgoing commits from this head if len(repo.parents()) != 1: @@ -112,6 +124,8 @@ def push(orig, ui, repo, dest=None, *arg return 1 workingrev = repo.parents()[0] ui.status('searching for changes\n') + svn_commit_hashes = dict(zip(hge.revmap.itervalues(), + hge.revmap.iterkeys())) outgoing = util.outgoing_revisions(ui, repo, hge, svn_commit_hashes, workingrev.node()) if not (outgoing and len(outgoing)): ui.status('no changes found\n') @@ -143,12 +157,10 @@ def push(orig, ui, repo, dest=None, *arg old_ctx) return 1 # 3. Fetch revisions from svn - # TODO this probably should pass in the source explicitly - r = pull(None, ui, repo, svn=True, stupid=opts.get('svn_stupid', False), - username=user, password=passwd) + # TODO: this probably should pass in the source explicitly - rev too? + r = pull(repo, source=dest, force=force) assert not r or r == 0 # 4. Find the new head of the target branch - repo = hg.repository(ui, hge.path) oldtipctx = repo[oldtip] replacement = [c for c in oldtipctx.children() if c not in old_children and c.branch() == oldtipctx.branch()] @@ -161,8 +173,9 @@ def push(orig, ui, repo, dest=None, *arg if ctx.node() == oldest: return extra['branch'] = ctx.branch() + # TODO: can we avoid calling our own rebase wrapper here? rebase(hgrebase.rebase, ui, repo, svn=True, svnextrafn=extrafn, - svnsourcerev=needs_transplant, **opts) + svnsourcerev=needs_transplant) repo = hg.repository(ui, hge.path) for child in repo[replacement.node()].children(): rebasesrc = node.bin(child.extra().get('rebase_source', node.hex(node.nullid))) @@ -175,7 +188,8 @@ def push(orig, ui, repo, dest=None, *arg if children: child = children[0] rebasesrc = node.bin(child.extra().get('rebase_source', node.hex(node.nullid))) - hge = hg_delta_editor.HgChangeReceiver(hge.path, ui_=ui) + # TODO: stop constantly creating the HgChangeReceiver instances. + hge = hg_delta_editor.HgChangeReceiver(hge.repo, ui_=ui, uuid=svn.uuid) svn_commit_hashes = dict(zip(hge.revmap.itervalues(), hge.revmap.iterkeys())) util.swap_out_encoding(old_encoding) return 0 @@ -234,25 +248,20 @@ def clone(orig, ui, source, dest=None, * return res -def pull(orig, ui, repo, source="default", *args, **opts): +def pull(repo, source="default", rev=None, force=False): """pull new revisions from Subversion Also takes svn, svn_stupid, and create_new_dest kwargs. """ - svn = opts.pop('svn', None) - svn_stupid = opts.pop('svn_stupid', False) - create_new_dest = opts.pop('create_new_dest', False) - url = ((repo and repo.ui) or ui).expandpath(source) - if not (cmdutil.issvnurl(url) or svn or create_new_dest): - return orig(ui, repo, source=source, *args, **opts) - svn_url = url - svn_url = util.normalize_url(svn_url) + url = repo.ui.expandpath(source) + svn_url = util.normalize_url(url) + # Split off #rev; TODO: implement --rev/#rev support limiting the pulled/cloned revisions - svn_url, revs, checkout = hg.parseurl(svn_url, opts.get('rev')) + svn_url, revs, checkout = hg.parseurl(svn_url, rev) old_encoding = util.swap_out_encoding() # TODO implement skipto support skipto_rev = 0 - have_replay = not svn_stupid + have_replay = not repo.ui.configbool('hgsubversion', 'stupid') if have_replay and not callable( delta.svn_txdelta_apply(None, None, None)[0]): #pragma: no cover ui.status('You are using old Subversion SWIG bindings. Replay will not' @@ -261,46 +270,23 @@ def pull(orig, ui, repo, source="default ' contribute a patch to use the ctypes bindings instead' ' of SWIG.\n') have_replay = False - initializing_repo = False - user, passwd = util.getuserpass(opts) + + # FIXME: enable this + user = repo.ui.config('hgsubversion', 'username') + passwd = repo.ui.config('hgsubversion', 'password') svn = svnwrap.SubversionRepo(svn_url, user, passwd) - author_host = ui.config('hgsubversion', 'defaulthost', svn.uuid) - tag_locations = ['tags', ] - authors = opts.pop('svn_authors', None) - filemap = opts.pop('svn_filemap', None) - if repo: - hg_editor = hg_delta_editor.HgChangeReceiver(repo=repo, - subdir=svn.subdir, - author_host=author_host, - tag_locations=tag_locations, - authors=authors, - filemap=filemap) - else: - hg_editor = hg_delta_editor.HgChangeReceiver(ui_=ui, - path=create_new_dest, - subdir=svn.subdir, - author_host=author_host, - tag_locations=tag_locations, - authors=authors, - filemap=filemap) - hg_editor.opts = opts - if os.path.exists(hg_editor.uuid_file): - uuid = open(hg_editor.uuid_file).read() - assert uuid == svn.uuid - start = hg_editor.last_known_revision() - else: - open(hg_editor.uuid_file, 'w').write(svn.uuid) - open(hg_editor.svn_url_file, 'w').write(svn_url) - initializing_repo = True - start = skipto_rev + hg_editor = hg_delta_editor.HgChangeReceiver(repo=repo, subdir=svn.subdir, + uuid=svn.uuid) + + start = max(hg_editor.last_known_revision(), skipto_rev) + initializing_repo = (hg_editor.last_known_revision() <= 0) + ui = repo.ui if initializing_repo and start > 0: raise hgutil.Abort('Revision skipping at repository initialization ' 'remains unimplemented.') revisions = 0 - if not initializing_repo: - oldheads = len(repo.changelog.heads()) # start converting revisions for r in svn.revisions(start=start): @@ -344,14 +330,6 @@ def pull(orig, ui, repo, source="default return else: ui.status("added %d svn revisions\n" % revisions) - if not initializing_repo: - newheads = len(repo.changelog.heads()) - # postincoming needs to know if heads were added or removed - # calculation based on mercurial.localrepo.addchangegroup - # 0 means no changes, 1 no new heads, > 1 new heads, < 0 heads removed - modheads = newheads - oldheads + (newheads < oldheads and -1 or 1) - commands.postincoming(ui, repo, modheads, opts.get('update'), checkout) - def rebase(orig, ui, repo, **opts): """rebase current unpushed revisions onto the Subversion head diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ setup( long_description = open(os.path.join(os.path.dirname(__file__), 'README')).read(), keywords = 'mercurial', - packages = ['hgext.hgsubversion', 'hgext.hgsubversion.svnwrap'], - package_dir = {'hgext.hgsubversion': ''}, + packages = ['hgsubversion', 'hgsubversion.svnwrap'], platforms = 'any', classifiers = [ 'License :: OSI Approved :: GNU General Public License (GPL)', diff --git a/tests/run.py b/tests/run.py --- a/tests/run.py +++ b/tests/run.py @@ -2,7 +2,7 @@ import os import sys import unittest -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import test_binaryfiles import test_diff @@ -54,5 +54,8 @@ if __name__ == '__main__': kwargs['descriptions'] = 3 kwargs['verbosity'] = 2 + # silence output when running outside nose + sys.stdout = os.tmpfile() + runner = unittest.TextTestRunner(**kwargs) runner.run(suite()) diff --git a/tests/test_diff.py b/tests/test_diff.py --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -2,7 +2,7 @@ import unittest from mercurial import ui -import wrappers +from hgsubversion import wrappers import test_util @@ -32,9 +32,9 @@ class DiffTests(test_util.TestBase): ('alpha', 'alpha', 'alpha\n\nadded line\n'), ]) u = ui.ui() - wrappers.diff(lambda x,y,z: None, - u, self.repo, svn=True) - self.assertEqual(u.stream.getvalue(), expected_diff_output) + u.pushbuffer() + wrappers.diff(lambda x,y,z: None, u, self.repo, svn=True) + self.assertEqual(u.popbuffer(), expected_diff_output) def suite(): diff --git a/tests/test_externals.py b/tests/test_externals.py --- a/tests/test_externals.py +++ b/tests/test_externals.py @@ -2,7 +2,7 @@ import os, unittest from mercurial import commands -import svnexternals +from hgsubversion import svnexternals import test_util class TestFetchExternals(test_util.TestBase): @@ -47,7 +47,7 @@ class TestFetchExternals(test_util.TestB ('http://svn.example.com/skin-maker@21 third-party/skins/toolkit', ('third-party/skins/toolkit', None, 'http://svn.example.com/skin-maker', '21')), ] - + for line, expected in samples: self.assertEqual(expected, svnexternals.parsedefinition(line)) @@ -106,7 +106,7 @@ class TestFetchExternals(test_util.TestB svnexternals.updateexternals(ui, [rev], repo) for d in deps: p = os.path.join(repo.root, d) - self.assertTrue(os.path.isdir(p), + self.assertTrue(os.path.isdir(p), 'missing: %s@%r' % (d, rev)) for d in nodeps: p = os.path.join(repo.root, d) @@ -118,10 +118,10 @@ class TestFetchExternals(test_util.TestB commands.update(ui, repo) checkdeps(['deps/project1'], [], repo, 0) checkdeps(['deps/project1', 'deps/project2'], [], repo, 1) - checkdeps(['subdir/deps/project1', 'subdir2/deps/project1', - 'deps/project2'], + checkdeps(['subdir/deps/project1', 'subdir2/deps/project1', + 'deps/project2'], ['deps/project1'], repo, 2) - checkdeps(['subdir/deps/project1', 'deps/project2'], + checkdeps(['subdir/deps/project1', 'deps/project2'], ['subdir2/deps/project1'], repo, 3) checkdeps(['subdir/deps/project1'], ['deps/project2'], repo, 4) diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py --- a/tests/test_fetch_branches.py +++ b/tests/test_fetch_branches.py @@ -5,7 +5,6 @@ from mercurial import node from mercurial import ui import test_util -import wrappers class TestFetchBranches(test_util.TestBase): @@ -18,7 +17,7 @@ class TestFetchBranches(test_util.TestBa def _load_fixture_and_fetch_with_anchor(self, fixture_name, anchor): test_util.load_svndump_fixture(self.repo_path, fixture_name) source = '%s#%s' % (test_util.fileurl(self.repo_path), anchor) - wrappers.clone(None, ui.ui(), source=source, dest=self.wc_path) + repo = hg.clone(ui.ui(), source=source, dest=self.wc_path) return hg.repository(ui.ui(), self.wc_path) def test_unrelatedbranch(self, stupid=False): @@ -75,10 +74,10 @@ class TestFetchBranches(test_util.TestBa stupid, noupdate=False) self.assertEqual(repo[None].branch(), 'default') self.assertTrue('tip' not in repo[None].tags()) - + def test_branch_tip_update_to_default_stupid(self): self.test_branch_tip_update_to_default(True) - + def test_branch_tip_update_to_branch_anchor(self): repo = self._load_fixture_and_fetch_with_anchor( 'unorderedbranch.svndump', 'branch') diff --git a/tests/test_fetch_command_regexes.py b/tests/test_fetch_command_regexes.py --- a/tests/test_fetch_command_regexes.py +++ b/tests/test_fetch_command_regexes.py @@ -1,6 +1,8 @@ -import stupid import unittest +from hgsubversion import stupid + + two_empties = """Index: __init__.py =================================================================== Index: bar/__init__.py 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 @@ -3,11 +3,11 @@ import os import unittest +from mercurial import commands from mercurial import ui from mercurial import node import test_util -import wrappers class MapTests(test_util.TestBase): @property @@ -23,8 +23,11 @@ class MapTests(test_util.TestBase): authormap = open(self.authors, 'w') authormap.write("Augie=Augie Fackler \n") authormap.close() - wrappers.clone(None, ui.ui(), source=test_util.fileurl(self.repo_path), - dest=self.wc_path, stupid=stupid, svn_authors=self.authors) + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + _ui.setconfig('hgsubversion', 'authormap', self.authors) + commands.clone(_ui, test_util.fileurl(self.repo_path), + self.wc_path, authors=self.authors) self.assertEqual(self.repo[0].user(), 'Augie Fackler ') self.assertEqual(self.repo['tip'].user(), @@ -38,9 +41,11 @@ class MapTests(test_util.TestBase): authormap = open(self.authors, 'w') authormap.write("evil=Testy ") authormap.close() - wrappers.clone(None, ui.ui(), source=test_util.fileurl(self.repo_path), - dest=self.wc_path, stupid=stupid, - svn_authors=self.authors) + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + _ui.setconfig('hgsubversion', 'authormap', self.authors) + commands.clone(_ui, test_util.fileurl(self.repo_path), + self.wc_path, authors=self.authors) self.assertEqual(self.repo[0].user(), 'Augie@5b65bade-98f3-4993-a01f-b7a6710da339') self.assertEqual(self.repo['tip'].user(), @@ -54,9 +59,11 @@ class MapTests(test_util.TestBase): filemap = open(self.filemap, 'w') filemap.write("include alpha\n") filemap.close() - wrappers.clone(None, ui.ui(), source=test_util.fileurl(self.repo_path), - dest=self.wc_path, stupid=stupid, - svn_filemap=self.filemap) + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + _ui.setconfig('hgsubversion', 'filemap', self.filemap) + commands.clone(_ui, test_util.fileurl(self.repo_path), + self.wc_path, filemap=self.filemap) self.assertEqual(node.hex(self.repo[0].node()), '88e2c7492d83e4bf30fbb2dcbf6aa24d60ac688d') self.assertEqual(node.hex(self.repo['default'].node()), 'e524296152246b3837fe9503c83b727075835155') @@ -68,9 +75,11 @@ class MapTests(test_util.TestBase): filemap = open(self.filemap, 'w') filemap.write("exclude alpha\n") filemap.close() - wrappers.clone(None, ui.ui(), source=test_util.fileurl(self.repo_path), - dest=self.wc_path, stupid=stupid, - svn_filemap=self.filemap) + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + _ui.setconfig('hgsubversion', 'filemap', self.filemap) + commands.clone(_ui, test_util.fileurl(self.repo_path), + self.wc_path, filemap=self.filemap) self.assertEqual(node.hex(self.repo[0].node()), '2c48f3525926ab6c8b8424bcf5eb34b149b61841') self.assertEqual(node.hex(self.repo['default'].node()), 'b37a3c0297b71f989064d9b545b5a478bbed7cc1') diff --git a/tests/test_fetch_truncated.py b/tests/test_fetch_truncated.py --- a/tests/test_fetch_truncated.py +++ b/tests/test_fetch_truncated.py @@ -1,39 +1,39 @@ -import unittest - -from mercurial import hg -from mercurial import ui - -import wrappers -import test_util - -class TestFetchTruncatedHistory(test_util.TestBase): - def test_truncated_history(self, stupid=False): - # Test repository does not follow the usual layout - test_util.load_svndump_fixture(self.repo_path, 'truncatedhistory.svndump') - svn_url = test_util.fileurl(self.repo_path + '/project2') - wrappers.clone(None, ui.ui(), source=svn_url, - dest=self.wc_path, stupid=stupid, - noupdate=True) - repo = hg.repository(ui.ui(), self.wc_path) - - # We are converting /project2/trunk coming from: - # - # Changed paths: - # D /project1 - # A /project2/trunk (from /project1:2) - # - # Here a full fetch should be performed since we are starting - # the conversion on an already filled branch. - tip = repo['tip'] - files = tip.manifest().keys() - files.sort() - self.assertEqual(files, ['a', 'b']) - self.assertEqual(repo['tip']['a'].data(), 'a\n') - - def test_truncated_history_stupid(self): - self.test_truncated_history(True) - -def suite(): - all = [unittest.TestLoader().loadTestsFromTestCase(TestFetchTruncatedHistory), - ] - return unittest.TestSuite(all) +import unittest + +from mercurial import commands +from mercurial import hg +from mercurial import ui + +import test_util + +class TestFetchTruncatedHistory(test_util.TestBase): + def test_truncated_history(self, stupid=False): + # Test repository does not follow the usual layout + test_util.load_svndump_fixture(self.repo_path, 'truncatedhistory.svndump') + svn_url = test_util.fileurl(self.repo_path + '/project2') + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + commands.clone(_ui, svn_url, self.wc_path, noupdate=True) + repo = hg.repository(_ui, self.wc_path) + + # We are converting /project2/trunk coming from: + # + # Changed paths: + # D /project1 + # A /project2/trunk (from /project1:2) + # + # Here a full fetch should be performed since we are starting + # the conversion on an already filled branch. + tip = repo['tip'] + files = tip.manifest().keys() + files.sort() + self.assertEqual(files, ['a', 'b']) + self.assertEqual(repo['tip']['a'].data(), 'a\n') + + def test_truncated_history_stupid(self): + self.test_truncated_history(True) + +def suite(): + all = [unittest.TestLoader().loadTestsFromTestCase(TestFetchTruncatedHistory), + ] + return unittest.TestSuite(all) diff --git a/tests/test_pull.py b/tests/test_pull.py --- a/tests/test_pull.py +++ b/tests/test_pull.py @@ -3,9 +3,8 @@ import test_util import os.path import subprocess from mercurial import ui - -import wrappers - +from mercurial import util as hgutil +from mercurial import commands class TestPull(test_util.TestBase): def setUp(self): @@ -22,30 +21,34 @@ class TestPull(test_util.TestBase): if self.svn_wc is None: self.svn_wc = os.path.join(self.tmpdir, 'testsvn_wc') subprocess.call([ - 'svn', 'co', '-q', test_util.fileurl(self.repo_path), + 'svn', 'co', '-q', test_util.fileurl(self.repo_path)[4:], self.svn_wc - ]) + ], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) for filename, contents in changes.iteritems(): # filenames are / separated filename = filename.replace('/', os.path.sep) filename = os.path.join(self.svn_wc, filename) open(filename, 'w').write(contents) - subprocess.call(['svn', 'add', '-q', filename]) # may be redundant + # may be redundant + subprocess.call(['svn', 'add', '-q', filename], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) subprocess.call([ - 'svn', 'commit', '-q', self.svn_wc, '-m', 'test changes']) + 'svn', 'commit', '-q', self.svn_wc, '-m', 'test changes'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) def test_nochanges(self): - repo = self._load_fixture_and_fetch('single_rev.svndump') - state = repo.parents() - wrappers.pull(None, ui.ui(), repo) - self.assertEqual(state, repo.parents()) + self._load_fixture_and_fetch('single_rev.svndump') + state = self.repo.parents() + commands.pull(self.repo.ui, self.repo) + self.assertEqual(state, self.repo.parents()) def test_onerevision_noupdate(self): repo = self._load_fixture_and_fetch('single_rev.svndump') state = repo.parents() self._add_svn_rev({'trunk/alpha': 'Changed'}) - wrappers.pull(None, ui.ui(), repo) + commands.pull(self.repo.ui, repo) self.assertEqual(state, repo.parents()) self.assertTrue('tip' not in repo[None].tags()) @@ -53,7 +56,7 @@ class TestPull(test_util.TestBase): repo = self._load_fixture_and_fetch('single_rev.svndump') state = repo.parents() self._add_svn_rev({'trunk/alpha': 'Changed'}) - wrappers.pull(None, ui.ui(), repo, update=True) + commands.pull(self.repo.ui, repo, update=True) self.failIfEqual(state, repo.parents()) self.assertTrue('tip' in repo[None].tags()) @@ -62,7 +65,8 @@ class TestPull(test_util.TestBase): self.commitchanges((('alpha', 'alpha', 'Changed another way'),)) state = repo.parents() self._add_svn_rev({'trunk/alpha': 'Changed one way'}) - wrappers.pull(None, ui.ui(), repo, update=True) + self.assertRaises(hgutil.Abort, commands.pull, + self.repo.ui, repo, update=True) self.assertEqual(state, repo.parents()) self.assertTrue('tip' not in repo[None].tags()) self.assertEqual(len(repo.heads()), 2) 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 @@ -1,84 +1,22 @@ +import atexit import os +import random import socket import subprocess import unittest from mercurial import context +from mercurial import commands from mercurial import hg from mercurial import node from mercurial import ui from mercurial import revlog from mercurial import util as hgutil -import wrappers import test_util import time -class PushOverSvnserveTests(test_util.TestBase): - def setUp(self): - test_util.TestBase.setUp(self) - test_util.load_svndump_fixture(self.repo_path, 'simple_branch.svndump') - open(os.path.join(self.repo_path, 'conf', 'svnserve.conf'), - 'w').write('[general]\nanon-access=write\n[sasl]\n') - # Paranoia: we try and connect to localhost on 3689 before we start - # svnserve. If it is running, we force the test to fail early. - user_has_own_svnserve = False - try: - s = socket.socket() - s.settimeout(0.3) - s.connect(('localhost', 3690)) - s.close() - user_has_own_svnserve = True - except: - pass - if user_has_own_svnserve: - assert False, ('You appear to be running your own svnserve!' - ' You can probably ignore this test failure.') - args = ['svnserve', '-d', '--foreground', '-r', self.repo_path] - self.svnserve_pid = subprocess.Popen(args).pid - time.sleep(2) - wrappers.clone(None, ui.ui(), source='svn://localhost/', - dest=self.wc_path, noupdate=True) - - def tearDown(self): - os.system('kill -9 %d' % self.svnserve_pid) - test_util.TestBase.tearDown(self) - - def test_push_to_default(self, commit=True): - repo = self.repo - old_tip = repo['tip'].node() - expected_parent = repo['default'].node() - def file_callback(repo, memctx, path): - if path == 'adding_file': - return context.memfilectx(path=path, - data='foo', - islink=False, - isexec=False, - copied=False) - raise IOError() - ctx = context.memctx(repo, - (repo['default'].node(), node.nullid), - 'automated test', - ['adding_file'], - file_callback, - 'an_author', - '2008-10-07 20:59:48 -0500', - {'branch': 'default',}) - new_hash = repo.commitctx(ctx) - if not commit: - return # some tests use this test as an extended setup. - hg.update(repo, repo['tip'].node()) - oldauthor = repo['tip'].user() - wrappers.push(None, ui.ui(), repo=self.repo) - tip = self.repo['tip'] - self.assertNotEqual(oldauthor, tip.user()) - self.assertNotEqual(tip.node(), old_tip) - self.assertEqual(tip.parents()[0].node(), expected_parent) - self.assertEqual(tip['adding_file'].data(), 'foo') - self.assertEqual(tip.branch(), 'default') - - class PushTests(test_util.TestBase): def setUp(self): test_util.TestBase.setUp(self) @@ -111,6 +49,58 @@ class PushTests(test_util.TestBase): tip = self.repo['tip'] self.assertEqual(tip.node(), old_tip) + def test_push_over_svnserve(self, commit=True): + test_util.load_svndump_fixture(self.repo_path, 'simple_branch.svndump') + open(os.path.join(self.repo_path, 'conf', 'svnserve.conf'), + 'w').write('[general]\nanon-access=write\n[sasl]\n') + self.port = random.randint(socket.IPPORT_USERRESERVED, 65535) + self.host = 'localhost' + args = ['svnserve', '--daemon', '--foreground', + '--listen-port=%d' % self.port, + '--listen-host=%s' % self.host, + '--root=%s' % self.repo_path] + + self.svnserve_pid = subprocess.Popen(args).pid + try: + time.sleep(2) + import shutil + shutil.rmtree(self.wc_path) + commands.clone(ui.ui(), 'svn://%s:%d/' % (self.host, self.port), + self.wc_path, noupdate=True) + + repo = self.repo + old_tip = repo['tip'].node() + expected_parent = repo['default'].node() + def file_callback(repo, memctx, path): + if path == 'adding_file': + return context.memfilectx(path=path, + data='foo', + islink=False, + isexec=False, + copied=False) + raise IOError() + ctx = context.memctx(repo, + (repo['default'].node(), node.nullid), + 'automated test', + ['adding_file'], + file_callback, + 'an_author', + '2008-10-07 20:59:48 -0500', + {'branch': 'default',}) + new_hash = repo.commitctx(ctx) + if not commit: + return # some tests use this test as an extended setup. + hg.update(repo, repo['tip'].node()) + oldauthor = repo['tip'].user() + commands.push(repo.ui, repo) + tip = self.repo['tip'] + self.assertNotEqual(oldauthor, tip.user()) + self.assertNotEqual(tip.node(), old_tip) + self.assertEqual(tip.parents()[0].node(), expected_parent) + self.assertEqual(tip['adding_file'].data(), 'foo') + self.assertEqual(tip.branch(), 'default') + finally: + os.kill(self.svnserve_pid, 9) def test_push_to_default(self, commit=True): repo = self.repo @@ -172,7 +162,7 @@ class PushTests(test_util.TestBase): newhash = self.repo.commitctx(ctx) repo = self.repo hg.update(repo, newhash) - wrappers.push(None, ui.ui(), repo=repo) + commands.push(repo.ui, repo) self.assertEqual(self.repo['tip'].parents()[0].parents()[0].node(), oldtiphash) self.assertEqual(self.repo['tip'].files(), ['delta', ]) self.assertEqual(self.repo['tip'].manifest().keys(), @@ -424,7 +414,7 @@ class PushTests(test_util.TestBase): def suite(): - test_classes = [PushTests, PushOverSvnserveTests] + test_classes = [PushTests, ] tests = [] # This is the quickest hack I could come up with to load all the tests from # both classes. Would love a patch that simplifies this without adding diff --git a/tests/test_rebuildmeta.py b/tests/test_rebuildmeta.py --- a/tests/test_rebuildmeta.py +++ b/tests/test_rebuildmeta.py @@ -5,8 +5,8 @@ import unittest from mercurial import hg from mercurial import ui -import svncommands -import hg_delta_editor +from hgsubversion import svncommands +from hgsubversion import hg_delta_editor import test_util @@ -22,10 +22,18 @@ def _do_case(self, name, stupid): os.path.dirname(dest.path), args=[test_util.fileurl(self.repo_path + subdir), ]) + self.assertTrue(os.path.isdir(os.path.join(src.path, 'svn')), + 'no .hg/svn directory in the source!') + self.assertTrue(os.path.isdir(os.path.join(src.path, 'svn')), + 'no .hg/svn directory in the destination!') dest = hg.repository(u, os.path.dirname(dest.path)) - for tf in ('rev_map', 'uuid', 'url'): - self.assertEqual(open(os.path.join(src.path, 'svn', tf)).read(), - open(os.path.join(dest.path, 'svn', tf)).read()) + for tf in ('rev_map', 'uuid'): + stf = os.path.join(src.path, 'svn', tf) + 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) + self.assertEqual(open(stf).read(), + open(dtf).read()) self.assertEqual(pickle.load(open(os.path.join(src.path, 'svn', 'tag_info'))), pickle.load(open(os.path.join(dest.path, 'svn', diff --git a/tests/test_tags.py b/tests/test_tags.py --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -6,20 +6,15 @@ from mercurial import ui import test_util -import tag_repo +from hgsubversion import svnrepo class TestTags(test_util.TestBase): def _load_fixture_and_fetch(self, fixture_name, stupid=False): return test_util.load_fixture_and_fetch(fixture_name, self.repo_path, self.wc_path, stupid=stupid) - def getrepo(self): - ui_ = ui.ui() - repo = hg.repository(ui_, self.wc_path) - repo.__class__ = tag_repo.generate_repo_class(ui_, repo) - return repo - def _test_tag_revision_info(self, repo): + print repo.tags() self.assertEqual(node.hex(repo[0].node()), '434ed487136c1b47c1e8f952edb4dc5a8e6328df') self.assertEqual(node.hex(repo['tip'].node()), @@ -30,7 +25,7 @@ class TestTags(test_util.TestBase): repo = self._load_fixture_and_fetch('basic_tag_tests.svndump', stupid=stupid) self._test_tag_revision_info(repo) - repo = self.getrepo() + repo = self.repo self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node()) self.assertEqual(repo['tip'].node(), repo['tag/copied_tag'].node()) @@ -41,7 +36,7 @@ class TestTags(test_util.TestBase): repo = self._load_fixture_and_fetch('remove_tag_test.svndump', stupid=stupid) self._test_tag_revision_info(repo) - repo = self.getrepo() + repo = self.repo self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node()) self.assert_('tag/copied_tag' not in repo.tags()) @@ -52,7 +47,7 @@ class TestTags(test_util.TestBase): repo = self._load_fixture_and_fetch('rename_tag_test.svndump', stupid=stupid) self._test_tag_revision_info(repo) - repo = self.getrepo() + repo = self.repo self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node()) self.assertEqual(repo['tip'].node(), repo['tag/other_tag_r3'].node()) self.assert_('tag/copied_tag' not in repo.tags()) @@ -63,7 +58,7 @@ class TestTags(test_util.TestBase): def test_branch_from_tag(self, stupid=False): repo = self._load_fixture_and_fetch('branch_from_tag.svndump', stupid=stupid) - repo = self.getrepo() + repo = self.repo self.assertEqual(repo['tip'].node(), repo['branch_from_tag'].node()) self.assertEqual(repo[1].node(), repo['tag/tag_r3'].node()) self.assertEqual(repo['branch_from_tag'].parents()[0].node(), @@ -75,7 +70,7 @@ class TestTags(test_util.TestBase): def test_tag_by_renaming_branch(self, stupid=False): repo = self._load_fixture_and_fetch('tag_by_rename_branch.svndump', stupid=stupid) - repo = self.getrepo() + repo = self.repo self.assertEqual(repo['tip'], repo['closed-branches']) self.assertEqual(node.hex(repo['tip'].node()), '2f0a3abe2004c0fa01f5f6074a8b5441e9c80c2a') diff --git a/tests/test_urls.py b/tests/test_urls.py --- a/tests/test_urls.py +++ b/tests/test_urls.py @@ -1,6 +1,6 @@ import test_util import unittest -from svnwrap.svn_swig_wrapper import parse_url +from hgsubversion.svnwrap.svn_swig_wrapper import parse_url class TestSubversionUrls(test_util.TestBase): def test_standard_url(self): diff --git a/tests/test_util.py b/tests/test_util.py --- a/tests/test_util.py +++ b/tests/test_util.py @@ -9,11 +9,12 @@ import unittest import urllib from mercurial import context +from mercurial import commands from mercurial import hg from mercurial import node from mercurial import ui -import wrappers +from hgsubversion import util # Fixtures that need to be pulled at a subdirectory of the repo path subdir = {'truncatedhistory.svndump': '/project2', @@ -30,29 +31,32 @@ def fileurl(path): path = urllib.pathname2url(path) if drive: drive = '/' + drive - url = 'file://%s%s' % (drive, path) + url = 'svn+file://%s%s' % (drive, path) return url def load_svndump_fixture(path, fixture_name): '''Loads an svnadmin dump into a fresh repo at path, which should not already exist. ''' - subprocess.call(['svnadmin', 'create', path,]) - proc = subprocess.Popen(['svnadmin', 'load', path,], stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if os.path.exists(path): rmtree(path) + subprocess.call(['svnadmin', 'create', path,], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) inp = open(os.path.join(FIXTURES, fixture_name)) - proc.stdin.write(inp.read()) - proc.stdin.flush() + proc = subprocess.Popen(['svnadmin', 'load', path,], stdin=inp, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) proc.communicate() def load_fixture_and_fetch(fixture_name, repo_path, wc_path, stupid=False, subdir='', noupdate=True): load_svndump_fixture(repo_path, fixture_name) if subdir: repo_path += '/' + subdir - wrappers.clone(None, ui.ui(), source=fileurl(repo_path), - dest=wc_path, stupid=stupid, noupdate=noupdate) - repo = hg.repository(ui.ui(), wc_path) - return repo + + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + commands.clone(_ui, fileurl(repo_path), wc_path, noupdate=noupdate) + _ui = ui.ui() + _ui.setconfig('hgsubversion', 'stupid', str(stupid)) + return hg.repository(_ui, wc_path) def rmtree(path): # Read-only files cannot be removed under Windows @@ -69,30 +73,6 @@ def rmtree(path): os.chmod(f, s.st_mode | stat.S_IWRITE) shutil.rmtree(path) - -class MockUI(object): - real_ui = ui.ui - _isatty = False - def __init__(self, src=None): - self.stream = StringIO.StringIO() - self.inner_ui = self.real_ui(src) - - def status(self, *args): - self.stream.write(''.join(args)) - - def warn(self, *args): - self.stream.write(*args) - - def write(self, *args): - self.stream.write(*args) - - def copy(self): - return self.__class__(self.inner_ui) - - def __getattr__(self, attr): - return getattr(self.inner_ui, attr) - - class TestBase(unittest.TestCase): def setUp(self): self.oldwd = os.getcwd() @@ -101,17 +81,24 @@ class TestBase(unittest.TestCase): self.hgrc = os.path.join(self.tmpdir, '.hgrc') os.environ['HGRCPATH'] = self.hgrc rc = open(self.hgrc, 'w') - rc.write('[extensions]\nhgsubversion=') + for l in '[extensions]', 'hgsubversion=': + print >> rc, l self.repo_path = '%s/testrepo' % self.tmpdir self.wc_path = '%s/testrepo_wc' % self.tmpdir - self._real_ui = ui.ui - ui.ui = MockUI + + # Previously, we had a MockUI class that wrapped ui, and giving access + # to the stream. The ui.pushbuffer() and ui.popbuffer() can be used + # instead. Using the regular UI class, with all stderr redirected to + # stdout ensures that the test setup is much more similar to usage + # setups. + self.patch = (ui.ui.write_err, ui.ui.write) + setattr(ui.ui, self.patch[0].func_name, self.patch[1]) def tearDown(self): rmtree(self.tmpdir) os.chdir(self.oldwd) - ui.ui = self._real_ui + setattr(ui.ui, self.patch[0].func_name, self.patch[0]) def _load_fixture_and_fetch(self, fixture_name, subdir='', stupid=False): return load_fixture_and_fetch(fixture_name, self.repo_path, @@ -125,17 +112,18 @@ class TestBase(unittest.TestCase): def pushrevisions(self, stupid=False): before = len(self.repo) - wrappers.push(None, ui.ui(), repo=self.repo, stupid=stupid) + self.repo.ui.setconfig('hgsubversion', 'stupid', str(stupid)) + commands.push(self.repo.ui, self.repo) after = len(self.repo) self.assertEqual(0, after - before) def svnls(self, path, rev='HEAD'): path = self.repo_path + '/' + path - path = fileurl(path) + path = util.normalize_url(fileurl(path)) args = ['svn', 'ls', '-r', rev, '-R', path] p = subprocess.Popen(args, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.STDOUT) stdout, stderr = p.communicate() if p.returncode: raise Exception('svn ls failed on %s: %r' % (path, stderr)) diff --git a/tests/test_utility_commands.py b/tests/test_utility_commands.py --- a/tests/test_utility_commands.py +++ b/tests/test_utility_commands.py @@ -8,9 +8,10 @@ from mercurial import revlog from mercurial import context from mercurial import node -import utility_commands +from hgsubversion import util +from hgsubversion import utility_commands import test_util -import wrappers +from hgsubversion import wrappers expected_info_output = '''URL: %(repourl)s/%(branch)s Repository Root: %(repourl)s @@ -23,32 +24,40 @@ Last Changed Date: %(date)s ''' class UtilityTests(test_util.TestBase): + @property + def repourl(self): + return util.normalize_url(test_util.fileurl(self.repo_path)) + def test_info_output(self): self._load_fixture_and_fetch('two_heads.svndump') hg.update(self.repo, 'the_branch') u = ui.ui() + u.pushbuffer() utility_commands.info(u, self.repo, self.wc_path) + actual = u.popbuffer() expected = (expected_info_output % {'date': '2008-10-08 01:39:05 +0000 (Wed, 08 Oct 2008)', - 'repourl': test_util.fileurl(self.repo_path), + 'repourl': self.repourl, 'branch': 'branches/the_branch', 'rev': 5, }) - self.assertEqual(u.stream.getvalue(), expected) + self.assertEqual(actual, expected) hg.update(self.repo, 'default') - u = ui.ui() + u.pushbuffer() utility_commands.info(u, self.repo, self.wc_path) + actual = u.popbuffer() expected = (expected_info_output % {'date': '2008-10-08 01:39:29 +0000 (Wed, 08 Oct 2008)', - 'repourl': test_util.fileurl(self.repo_path), + 'repourl': self.repourl, 'branch': 'trunk', 'rev': 6, }) - self.assertEqual(u.stream.getvalue(), expected) + self.assertEqual(actual, expected) def test_parent_output(self): self._load_fixture_and_fetch('two_heads.svndump') u = ui.ui() + u.pushbuffer() parents = (self.repo['the_branch'].node(), revlog.nullid, ) def filectxfn(repo, memctx, path): return context.memfilectx(path=path, @@ -67,7 +76,8 @@ class UtilityTests(test_util.TestBase): new = self.repo.commitctx(ctx) hg.update(self.repo, new) wrappers.parent(lambda x, y: None, u, self.repo, svn=True) - self.assertEqual(u.stream.getvalue(), + actual = u.popbuffer() + self.assertEqual(actual, 'changeset: 3:4e256962fc5d\n' 'branch: the_branch\n' 'user: durin@df2126f7-00ab-4d49-b42c-7e981dde0bcf\n' @@ -76,19 +86,22 @@ class UtilityTests(test_util.TestBase): hg.update(self.repo, 'default') # Make sure styles work - u = ui.ui() + u.pushbuffer() wrappers.parent(lambda x, y: None, u, self.repo, svn=True, style='compact') - self.assertEqual(u.stream.getvalue(), + actual = u.popbuffer() + self.assertEqual(actual, '4:1 1083037b18d8 2008-10-08 01:39 +0000 durin\n' ' Add gamma on trunk.\n\n') # custom templates too - u = ui.ui() + u.pushbuffer() wrappers.parent(lambda x, y: None, u, self.repo, svn=True, template='{node}\n') - self.assertEqual(u.stream.getvalue(), '1083037b18d85cd84fa211c5adbaeff0fea2cd9f\n') + actual = u.popbuffer() + self.assertEqual(actual, '1083037b18d85cd84fa211c5adbaeff0fea2cd9f\n') - u = ui.ui() + u.pushbuffer() wrappers.parent(lambda x, y: None, u, self.repo, svn=True) - self.assertEqual(u.stream.getvalue(), + actual = u.popbuffer() + self.assertEqual(actual, 'changeset: 4:1083037b18d8\n' 'parent: 1:c95251e0dd04\n' 'user: durin@df2126f7-00ab-4d49-b42c-7e981dde0bcf\n' @@ -98,6 +111,7 @@ class UtilityTests(test_util.TestBase): def test_outgoing_output(self): self._load_fixture_and_fetch('two_heads.svndump') u = ui.ui() + u.pushbuffer() parents = (self.repo['the_branch'].node(), revlog.nullid, ) def filectxfn(repo, memctx, path): return context.memfilectx(path=path, @@ -116,9 +130,9 @@ class UtilityTests(test_util.TestBase): new = self.repo.commitctx(ctx) hg.update(self.repo, new) wrappers.outgoing(lambda x,y,z: None, u, self.repo, svn=True) - self.assert_(node.hex(self.repo['localbranch'].node())[:8] in - u.stream.getvalue()) - self.assertEqual(u.stream.getvalue(), ('changeset: 5:6de15430fa20\n' + actual = u.popbuffer() + self.assert_(node.hex(self.repo['localbranch'].node())[:8] in actual) + self.assertEqual(actual, ('changeset: 5:6de15430fa20\n' 'branch: localbranch\n' 'tag: tip\n' 'parent: 3:4e256962fc5d\n' @@ -127,17 +141,10 @@ class UtilityTests(test_util.TestBase): 'summary: automated test\n' '\n')) hg.update(self.repo, 'default') - u = ui.ui() + u.pushbuffer() wrappers.outgoing(lambda x,y,z: None, u, self.repo, svn=True) - self.assertEqual(u.stream.getvalue(), 'no changes found\n') - - def test_url_output(self): - self._load_fixture_and_fetch('two_revs.svndump') - hg.update(self.repo, 'tip') - u = ui.ui() - utility_commands.url(u, self.repo, self.wc_path) - expected = test_util.fileurl(self.repo_path) + '\n' - self.assertEqual(u.stream.getvalue(), expected) + actual = u.popbuffer() + self.assertEqual(actual, 'no changes found\n') def test_rebase(self): self._load_fixture_and_fetch('two_revs.svndump') @@ -165,28 +172,12 @@ class UtilityTests(test_util.TestBase): self.assertEqual(self.repo['tip'].parents()[0].parents()[0], self.repo[0]) self.assertNotEqual(beforerebasehash, self.repo['tip'].node()) - def test_url_is_normalized(self): - """Verify url gets normalized on initial clone. - """ - test_util.load_svndump_fixture(self.repo_path, 'two_revs.svndump') - wrappers.clone(None, ui.ui(), - source=test_util.fileurl(self.repo_path) + '/', - dest=self.wc_path, stupid=False) - hg.update(self.repo, 'tip') - u = ui.ui() - utility_commands.url(u, self.repo, self.wc_path) - expected = test_util.fileurl(self.repo_path) + '\n' - self.assertEqual(u.stream.getvalue(), expected) - def test_genignore(self): - """Verify url gets normalized on initial clone. - """ - test_util.load_svndump_fixture(self.repo_path, 'ignores.svndump') - wrappers.clone(None, ui.ui(), - source=test_util.fileurl(self.repo_path) + '/', - dest=self.wc_path, stupid=False) - hg.update(self.repo, 'tip') + """ Test generation of .hgignore file. """ + test_util.load_fixture_and_fetch('ignores.svndump', self.repo_path, + self.wc_path, noupdate=False) u = ui.ui() + u.pushbuffer() utility_commands.genignore(u, self.repo, self.wc_path) self.assertEqual(open(os.path.join(self.wc_path, '.hgignore')).read(), '.hgignore\nsyntax:glob\nblah\notherblah\nbaz/magic\n') @@ -195,10 +186,12 @@ class UtilityTests(test_util.TestBase): test_util.load_svndump_fixture(self.repo_path, 'replace_trunk_with_branch.svndump') u = ui.ui() + u.pushbuffer() utility_commands.listauthors(u, args=[test_util.fileurl(self.repo_path)], authors=None) - self.assertEqual(u.stream.getvalue(), 'Augie\nevil\n') + actual = u.popbuffer() + self.assertEqual(actual, 'Augie\nevil\n') def test_list_authors_map(self):