diff options
Diffstat (limited to 'hgext/convert/subversion.py')
-rw-r--r-- | hgext/convert/subversion.py | 262 |
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) |