summaryrefslogtreecommitdiff
path: root/hgext/convert/subversion.py
diff options
context:
space:
mode:
Diffstat (limited to 'hgext/convert/subversion.py')
-rw-r--r--hgext/convert/subversion.py262
1 files changed, 93 insertions, 169 deletions
diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py
index 094988b..3e64ce6 100644
--- a/hgext/convert/subversion.py
+++ b/hgext/convert/subversion.py
@@ -2,14 +2,17 @@
#
# Copyright(C) 2007 Daniel Holth et al
-import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
+import os
+import re
+import sys
import cPickle as pickle
+import tempfile
+import urllib
+import urllib2
from mercurial import strutil, scmutil, util, encoding
from mercurial.i18n import _
-propertycache = util.propertycache
-
# Subversion stuff. Works best with very recent Python SVN bindings
# e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
# these bindings.
@@ -47,21 +50,10 @@ def revsplit(rev):
mod = '/' + parts[1]
return parts[0][4:], mod, int(revnum)
-def quote(s):
- # As of svn 1.7, many svn calls expect "canonical" paths. In
- # theory, we should call svn.core.*canonicalize() on all paths
- # before passing them to the API. Instead, we assume the base url
- # is canonical and copy the behaviour of svn URL encoding function
- # so we can extend it safely with new components. The "safe"
- # characters were taken from the "svn_uri__char_validity" table in
- # libsvn_subr/path.c.
- return urllib.quote(s, "!$&'()*+,-./:=@_~")
-
def geturl(path):
try:
return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
except SubversionException:
- # svn.client.url_from_path() fails with local repositories
pass
if os.path.isdir(path):
path = os.path.normpath(os.path.abspath(path))
@@ -70,8 +62,8 @@ def geturl(path):
# Module URL is later compared with the repository URL returned
# by svn API, which is UTF-8.
path = encoding.tolocal(path)
- path = 'file://%s' % quote(path)
- return svn.core.svn_path_canonicalize(path)
+ return 'file://%s' % urllib.quote(path)
+ return path
def optrev(number):
optrev = svn.core.svn_opt_revision_t()
@@ -85,8 +77,8 @@ class changedpath(object):
self.copyfrom_rev = p.copyfrom_rev
self.action = p.action
-def get_log_child(fp, url, paths, start, end, limit=0,
- discover_changed_paths=True, strict_node_history=False):
+def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
+ strict_node_history=False):
protocol = -1
def receiver(orig_paths, revnum, author, date, message, pool):
if orig_paths is not None:
@@ -103,11 +95,11 @@ def get_log_child(fp, url, paths, start, end, limit=0,
discover_changed_paths,
strict_node_history,
receiver)
+ except SubversionException, (inst, num):
+ pickle.dump(num, fp, protocol)
except IOError:
# Caller may interrupt the iteration
pickle.dump(None, fp, protocol)
- except Exception, inst:
- pickle.dump(str(inst), fp, protocol)
else:
pickle.dump(None, fp, protocol)
fp.close()
@@ -120,10 +112,6 @@ def debugsvnlog(ui, **opts):
"""Fetch SVN log in a subprocess and channel them back to parent to
avoid memory collection issues.
"""
- if svn is None:
- raise util.Abort(_('debugsvnlog could not load Subversion python '
- 'bindings'))
-
util.setbinary(sys.stdin)
util.setbinary(sys.stdout)
args = decodeargs(sys.stdin.read())
@@ -143,10 +131,10 @@ class logstream(object):
' hg executable is in PATH'))
try:
orig_paths, revnum, author, date, message = entry
- except (TypeError, ValueError):
+ except:
if entry is None:
break
- raise util.Abort(_("log stream exception '%s'") % entry)
+ raise SubversionException("child raised exception", entry)
yield entry
def close(self):
@@ -180,7 +168,7 @@ def httpcheck(ui, path, proto):
'know better.\n'))
return True
data = inst.fp.read()
- except Exception:
+ except:
# Could be urllib2.URLError if the URL is invalid or anything else.
return False
return '<m:human-readable errcode="160013">' in data
@@ -193,15 +181,12 @@ def issvnurl(ui, url):
try:
proto, path = url.split('://', 1)
if proto == 'file':
- if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
- and path[2:6].lower() == '%3a/'):
- path = path[:2] + ':/' + path[6:]
path = urllib.url2pathname(path)
except ValueError:
proto = 'file'
path = os.path.abspath(url)
if proto == 'file':
- path = util.pconvert(path)
+ path = path.replace(os.sep, '/')
check = protomap.get(proto, lambda *args: False)
while '/' in path:
if check(ui, path, proto):
@@ -234,7 +219,7 @@ class svn_source(converter_source):
raise NoRepo(_("%s does not look like a Subversion repository")
% url)
if svn is None:
- raise MissingTool(_('could not load Subversion python bindings'))
+ raise MissingTool(_('Could not load Subversion python bindings'))
try:
version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
@@ -283,8 +268,7 @@ class svn_source(converter_source):
except ValueError:
raise util.Abort(_('svn: revision %s is not an integer') % rev)
- self.trunkname = self.ui.config('convert', 'svn.trunk',
- 'trunk').strip('/')
+ self.trunkname = self.ui.config('convert', 'svn.trunk', 'trunk').strip('/')
self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
try:
self.startrev = int(self.startrev)
@@ -322,7 +306,7 @@ class svn_source(converter_source):
def exists(self, path, optrev):
try:
- svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
+ svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
optrev, False, self.ctx)
return True
except SubversionException:
@@ -374,7 +358,7 @@ class svn_source(converter_source):
# Check if branches bring a few more heads to the list
if branches:
rpath = self.url.strip('/')
- branchnames = svn.client.ls(rpath + '/' + quote(branches),
+ branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
rev, False, self.ctx)
for branch in branchnames.keys():
module = '%s/%s/%s' % (oldmodule, branches, branch)
@@ -410,7 +394,7 @@ class svn_source(converter_source):
else:
# Perform a full checkout on roots
uuid, module, revnum = revsplit(rev)
- entries = svn.client.ls(self.baseurl + quote(module),
+ entries = svn.client.ls(self.baseurl + urllib.quote(module),
optrev(revnum), True, self.ctx)
files = [n for n, e in entries.iteritems()
if e.kind == svn.core.svn_node_file]
@@ -444,8 +428,6 @@ class svn_source(converter_source):
if revnum < stop:
stop = revnum + 1
self._fetch_revisions(revnum, stop)
- if rev not in self.commits:
- raise util.Abort(_('svn: revision %s not found') % revnum)
commit = self.commits[rev]
# caller caches the result, so free it here to release memory
del self.commits[rev]
@@ -519,11 +501,11 @@ class svn_source(converter_source):
and not p[2].startswith(badroot + '/')]
# Tell tag renamings from tag creations
- renamings = []
+ remainings = []
for source, sourcerev, dest in pendings:
tagname = dest.split('/')[-1]
if source.startswith(srctagspath):
- renamings.append([source, sourcerev, tagname])
+ remainings.append([source, sourcerev, tagname])
continue
if tagname in tags:
# Keep the latest tag value
@@ -539,7 +521,7 @@ class svn_source(converter_source):
# but were really created in the tag
# directory.
pass
- pendings = renamings
+ pendings = remainings
tagspath = srctagspath
finally:
stream.close()
@@ -560,47 +542,18 @@ class svn_source(converter_source):
def revnum(self, rev):
return int(rev.split('@')[-1])
- def latest(self, path, stop=None):
- """Find the latest revid affecting path, up to stop revision
- number. If stop is None, default to repository latest
- revision. It may return a revision in a different module,
- since a branch may be moved without a change being
- reported. Return None if computed module does not belong to
- rootmodule subtree.
+ def latest(self, path, stop=0):
+ """Find the latest revid affecting path, up to stop. It may return
+ a revision in a different module, since a branch may be moved without
+ a change being reported. Return None if computed module does not
+ belong to rootmodule subtree.
"""
- def findchanges(path, start, stop=None):
- stream = self._getlog([path], start, stop or 1)
- try:
- for entry in stream:
- paths, revnum, author, date, message = entry
- if stop is None and paths:
- # We do not know the latest changed revision,
- # keep the first one with changed paths.
- break
- if revnum <= stop:
- break
-
- for p in paths:
- if (not path.startswith(p) or
- not paths[p].copyfrom_path):
- continue
- newpath = paths[p].copyfrom_path + path[len(p):]
- self.ui.debug("branch renamed from %s to %s at %d\n" %
- (path, newpath, revnum))
- path = newpath
- break
- if not paths:
- revnum = None
- return revnum, path
- finally:
- stream.close()
-
if not path.startswith(self.rootmodule):
# Requests on foreign branches may be forbidden at server level
self.ui.debug('ignoring foreign branch %r\n' % path)
return None
- if stop is None:
+ if not stop:
stop = svn.ra.get_latest_revnum(self.ra)
try:
prevmodule = self.reparent('')
@@ -615,30 +568,34 @@ class svn_source(converter_source):
# stat() gives us the previous revision on this line of
# development, but it might be in *another module*. Fetch the
# log and detect renames down to the latest revision.
- revnum, realpath = findchanges(path, stop, dirent.created_rev)
- if revnum is None:
- # Tools like svnsync can create empty revision, when
- # synchronizing only a subtree for instance. These empty
- # revisions created_rev still have their original values
- # despite all changes having disappeared and can be
- # returned by ra.stat(), at least when stating the root
- # module. In that case, do not trust created_rev and scan
- # the whole history.
- revnum, realpath = findchanges(path, stop)
- if revnum is None:
- self.ui.debug('ignoring empty branch %r\n' % realpath)
- return None
+ stream = self._getlog([path], stop, dirent.created_rev)
+ try:
+ for entry in stream:
+ paths, revnum, author, date, message = entry
+ if revnum <= dirent.created_rev:
+ break
- if not realpath.startswith(self.rootmodule):
- self.ui.debug('ignoring foreign branch %r\n' % realpath)
+ for p in paths:
+ if not path.startswith(p) or not paths[p].copyfrom_path:
+ continue
+ newpath = paths[p].copyfrom_path + path[len(p):]
+ self.ui.debug("branch renamed from %s to %s at %d\n" %
+ (path, newpath, revnum))
+ path = newpath
+ break
+ finally:
+ stream.close()
+
+ if not path.startswith(self.rootmodule):
+ self.ui.debug('ignoring foreign branch %r\n' % path)
return None
- return self.revid(revnum, realpath)
+ return self.revid(dirent.created_rev, path)
def reparent(self, module):
"""Reparent the svn transport and return the previous parent."""
if self.prevmodule == module:
return module
- svnurl = self.baseurl + quote(module)
+ svnurl = self.baseurl + urllib.quote(module)
prevmodule = self.prevmodule
if prevmodule is None:
prevmodule = ''
@@ -813,7 +770,7 @@ class svn_source(converter_source):
branch = None
cset = commit(author=author,
- date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
+ date=util.datestr(date),
desc=log,
parents=parents,
branch=branch,
@@ -870,14 +827,13 @@ class svn_source(converter_source):
pass
except SubversionException, (inst, num):
if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
- raise util.Abort(_('svn: branch has no revision %s')
- % to_revnum)
+ raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
raise
def getfile(self, file, rev):
# TODO: ra.get_file transmits the whole file instead of diffs.
if file in self.removed:
- raise IOError
+ raise IOError()
mode = ''
try:
new_module, revnum = revsplit(rev)[1:]
@@ -898,7 +854,7 @@ class svn_source(converter_source):
notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
if e.apr_err in notfound: # File not found
- raise IOError
+ raise IOError()
raise
if mode == 'l':
link_prefix = "link "
@@ -910,7 +866,7 @@ class svn_source(converter_source):
"""Enumerate all files in path at revnum, recursively."""
path = path.strip('/')
pool = Pool()
- rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
+ rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
if path:
path += '/'
@@ -958,8 +914,8 @@ class svn_source(converter_source):
if not p.startswith('/'):
p = self.module + '/' + p
relpaths.append(p.strip('/'))
- args = [self.baseurl, relpaths, start, end, limit,
- discover_changed_paths, strict_node_history]
+ args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
+ strict_node_history]
arg = encodeargs(args)
hgexe = util.hgexecutable()
cmd = '%s debugsvnlog' % util.shellquote(hgexe)
@@ -1020,25 +976,26 @@ class svn_sink(converter_sink, commandline):
self.wc = None
self.cwd = os.getcwd()
+ path = os.path.realpath(path)
+
created = False
if os.path.isfile(os.path.join(path, '.svn', 'entries')):
- self.wc = os.path.realpath(path)
+ self.wc = path
self.run0('update')
else:
- if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
- path = os.path.realpath(path)
- if os.path.isdir(os.path.dirname(path)):
- if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
- ui.status(_('initializing svn repository %r\n') %
- os.path.basename(path))
- commandline(ui, 'svnadmin').run0('create', path)
- created = path
- path = util.normpath(path)
- if not path.startswith('/'):
- path = '/' + path
- path = 'file://' + path
-
wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
+
+ if os.path.isdir(os.path.dirname(path)):
+ if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
+ ui.status(_('initializing svn repository %r\n') %
+ os.path.basename(path))
+ commandline(ui, 'svnadmin').run0('create', path)
+ created = path
+ path = util.normpath(path)
+ if not path.startswith('/'):
+ path = '/' + path
+ path = 'file://' + path
+
ui.status(_('initializing svn working copy %r\n')
% os.path.basename(wcpath))
self.run0('checkout', path, wcpath)
@@ -1062,29 +1019,6 @@ class svn_sink(converter_sink, commandline):
def wjoin(self, *names):
return os.path.join(self.wc, *names)
- @propertycache
- def manifest(self):
- # As of svn 1.7, the "add" command fails when receiving
- # already tracked entries, so we have to track and filter them
- # ourselves.
- m = set()
- output = self.run0('ls', recursive=True, xml=True)
- doc = xml.dom.minidom.parseString(output)
- for e in doc.getElementsByTagName('entry'):
- for n in e.childNodes:
- if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
- continue
- name = ''.join(c.data for c in n.childNodes
- if c.nodeType == c.TEXT_NODE)
- # Entries are compared with names coming from
- # mercurial, so bytes with undefined encoding. Our
- # best bet is to assume they are in local
- # encoding. They will be passed to command line calls
- # later anyway, so they better be.
- m.add(encoding.tolocal(name.encode('utf-8')))
- break
- return m
-
def putfile(self, filename, flags, data):
if 'l' in flags:
self.wopener.symlink(data, filename)
@@ -1097,13 +1031,20 @@ class svn_sink(converter_sink, commandline):
self.wopener.write(filename, data)
if self.is_exec:
- if self.is_exec(self.wjoin(filename)):
- if 'x' not in flags:
- self.delexec.append(filename)
- else:
- if 'x' in flags:
- self.setexec.append(filename)
- util.setflags(self.wjoin(filename), False, 'x' in flags)
+ was_exec = self.is_exec(self.wjoin(filename))
+ else:
+ # On filesystems not supporting execute-bit, there is no way
+ # to know if it is set but asking subversion. Setting it
+ # systematically is just as expensive and much simpler.
+ was_exec = 'x' not in flags
+
+ util.setflags(self.wjoin(filename), False, 'x' in flags)
+ if was_exec:
+ if 'x' not in flags:
+ self.delexec.append(filename)
+ else:
+ if 'x' in flags:
+ self.setexec.append(filename)
def _copyfile(self, source, dest):
# SVN's copy command pukes if the destination file exists, but
@@ -1120,7 +1061,6 @@ class svn_sink(converter_sink, commandline):
try:
self.run0('copy', source, dest)
finally:
- self.manifest.add(dest)
if exists:
try:
os.unlink(wdest)
@@ -1139,16 +1079,13 @@ class svn_sink(converter_sink, commandline):
def add_dirs(self, files):
add_dirs = [d for d in sorted(self.dirs_of(files))
- if d not in self.manifest]
+ if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
if add_dirs:
- self.manifest.update(add_dirs)
self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
return add_dirs
def add_files(self, files):
- files = [f for f in files if f not in self.manifest]
if files:
- self.manifest.update(files)
self.xargs(files, 'add', quiet=True)
return files
@@ -1158,7 +1095,6 @@ class svn_sink(converter_sink, commandline):
wd = self.wjoin(d)
if os.listdir(wd) == '.svn':
self.run0('delete', d)
- self.manifest.remove(d)
deleted.append(d)
return deleted
@@ -1169,12 +1105,6 @@ class svn_sink(converter_sink, commandline):
return u"svn:%s@%s" % (self.uuid, rev)
def putcommit(self, files, copies, parents, commit, source, revmap):
- for parent in parents:
- try:
- return self.revid(self.childmap[parent])
- except KeyError:
- pass
-
# Apply changes to working copy
for f, v in files:
try:
@@ -1187,6 +1117,11 @@ class svn_sink(converter_sink, commandline):
self.copies.append([copies[f], f])
files = [f[0] for f in files]
+ for parent in parents:
+ try:
+ return self.revid(self.childmap[parent])
+ except KeyError:
+ pass
entries = set(self.delete)
files = frozenset(files)
entries.update(self.add_dirs(files.difference(entries)))
@@ -1196,8 +1131,6 @@ class svn_sink(converter_sink, commandline):
self.copies = []
if self.delete:
self.xargs(self.delete, 'delete')
- for f in self.delete:
- self.manifest.remove(f)
self.delete = []
entries.update(self.add_files(files.difference(entries)))
entries.update(self.tidy_dirs(entries))
@@ -1240,12 +1173,3 @@ class svn_sink(converter_sink, commandline):
def puttags(self, tags):
self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
return None, None
-
- def hascommit(self, rev):
- # This is not correct as one can convert to an existing subversion
- # repository and childmap would not list all revisions. Too bad.
- if rev in self.childmap:
- return True
- raise util.Abort(_('splice map revision %s not found in subversion '
- 'child map (revision lookups are not implemented)')
- % rev)