diff -Nru hgsubversion-1.4/.hg_archival.txt hgsubversion-1.5/.hg_archival.txt --- hgsubversion-1.4/.hg_archival.txt 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/.hg_archival.txt 2012-10-29 11:16:19.000000000 +0000 @@ -1,4 +1,4 @@ repo: f2636cfed11500fdc47d1e3822d8e4a2bd636bf7 -node: 07234759a3f750029ccaa001837d42fa12dd33ee +node: 77b22e5b4ea6c248e079afd0f1e544cb5690ce20 branch: default -tag: 1.4 +tag: 1.5 diff -Nru hgsubversion-1.4/.hgignore hgsubversion-1.5/.hgignore --- hgsubversion-1.4/.hgignore 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/.hgignore 2012-10-29 11:16:19.000000000 +0000 @@ -17,3 +17,5 @@ .pydevproject .settings *.orig +.noseids +tests/fixtures/temp diff -Nru hgsubversion-1.4/.hgtags hgsubversion-1.5/.hgtags --- hgsubversion-1.4/.hgtags 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/.hgtags 2012-10-29 11:16:19.000000000 +0000 @@ -6,3 +6,4 @@ 708234ad6c97fb52417e0b46a86c8373e25123a5 1.2 4bbc6bf947f56a92e95a04a27b94a9f72d5482d7 1.2.1 0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 1.3 +07234759a3f750029ccaa001837d42fa12dd33ee 1.4 diff -Nru hgsubversion-1.4/debian/changelog hgsubversion-1.5/debian/changelog --- hgsubversion-1.4/debian/changelog 2012-06-08 11:37:31.000000000 +0000 +++ hgsubversion-1.5/debian/changelog 2013-07-23 11:42:14.000000000 +0000 @@ -1,3 +1,15 @@ +hgsubversion (1.5-1) unstable; urgency=low + + * Move to debian sid from experimental. + + -- Qijiang Fan Sat, 20 Jul 2013 18:17:11 +0800 + +hgsubversion (1.5-1~exp1) experimental; urgency=low + + * New upstream release. + + -- Qijiang Fan Thu, 15 Nov 2012 17:11:37 +0800 + hgsubversion (1.4-1) unstable; urgency=low * New upstream release. diff -Nru hgsubversion-1.4/hgsubversion/__init__.py hgsubversion-1.5/hgsubversion/__init__.py --- hgsubversion-1.4/hgsubversion/__init__.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/__init__.py 2012-10-29 11:16:19.000000000 +0000 @@ -206,6 +206,8 @@ ('', 'username', '', 'username for authentication'), ('', 'password', '', 'password for authentication'), ('r', 'rev', '', 'Mercurial revision'), + ('', 'unsafe-skip-uuid-check', False, + 'skip repository uuid check in rebuildmeta'), ], 'hg svn ...', ), diff -Nru hgsubversion-1.4/hgsubversion/editor.py hgsubversion-1.5/hgsubversion/editor.py --- hgsubversion-1.4/hgsubversion/editor.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/editor.py 2012-10-29 11:16:19.000000000 +0000 @@ -1,6 +1,8 @@ import errno -import cStringIO import sys +import tempfile +import shutil +import os from mercurial import util as hgutil from mercurial import revlog @@ -10,24 +12,86 @@ import util import svnexternals -class NeverClosingStringIO(object): - def __init__(self): - self._fp = cStringIO.StringIO() +class EditingError(Exception): + pass - def __getattr__(self, name): - return getattr(self._fp, name) +class FileStore(object): + def __init__(self, maxsize=None): + self._tempdir = None + self._files = {} + self._created = 0 + self._maxsize = maxsize + if self._maxsize is None: + self._maxsize = 100*(2**20) + self._size = 0 + self._data = {} + self._popped = set() + + def setfile(self, fname, data): + if fname in self._popped: + raise EditingError('trying to set a popped file %s' % fname) + + if self._maxsize < 0 or (len(data) + self._size) <= self._maxsize: + self._data[fname] = data + self._size += len(data) + else: + if self._tempdir is None: + self._tempdir = tempfile.mkdtemp(prefix='hg-subversion-') + # Avoid filename issues with these simple names + fn = str(self._created) + fp = hgutil.posixfile(os.path.join(self._tempdir, fn), 'wb') + try: + fp.write(data) + finally: + fp.close() + self._created += 1 + self._files[fname] = fn + + def delfile(self, fname): + if fname in self._popped: + raise EditingError('trying to delete a popped file %s' % fname) + + if fname in self._data: + del self._data[fname] + elif fname in self._files: + path = os.path.join(self._tempdir, self._files.pop(fname)) + os.unlink(path) + + def getfile(self, fname): + if fname in self._popped: + raise EditingError('trying to get a popped file %s' % fname) + + if fname in self._data: + return self._data[fname] + if self._tempdir is None or fname not in self._files: + raise IOError + path = os.path.join(self._tempdir, self._files[fname]) + fp = hgutil.posixfile(path, 'rb') + try: + return fp.read() + finally: + fp.close() + + def popfile(self, fname): + self.delfile(fname) + self._popped.add(fname) + + def files(self): + return list(self._files) + list(self._data) def close(self): - # svn 1.7 apply_delta driver now calls close() on passed file - # object which prevent us from calling getvalue() afterwards. - pass + if self._tempdir is not None: + tempdir, self._tempdir = self._tempdir, None + shutil.rmtree(tempdir) + self._files = None + self._data = None class RevisionData(object): __slots__ = [ - 'file', 'files', 'deleted', 'rev', 'execfiles', 'symlinks', 'batons', - 'copies', 'missing', 'emptybranches', 'base', 'externals', 'ui', - 'exception', + 'file', 'added', 'deleted', 'rev', 'execfiles', 'symlinks', + 'copies', 'emptybranches', 'base', 'externals', 'ui', + 'exception', 'store', ] def __init__(self, ui): @@ -35,73 +99,74 @@ self.clear() def clear(self): - self.file = None - self.files = {} + self.store = FileStore(util.getfilestoresize(self.ui)) + self.added = set() self.deleted = {} self.rev = None self.execfiles = {} self.symlinks = {} - self.batons = {} # Map fully qualified destination file paths to module source path self.copies = {} - self.missing = set() self.emptybranches = {} - self.base = None self.externals = {} self.exception = None - def set(self, path, data, isexec=False, islink=False): - self.files[path] = data + def set(self, path, data, isexec=False, islink=False, copypath=None): + self.store.setfile(path, data) self.execfiles[path] = isexec self.symlinks[path] = islink if path in self.deleted: del self.deleted[path] - if path in self.missing: - self.missing.remove(path) + if copypath is not None: + self.copies[path] = copypath + + def get(self, path): + if path in self.deleted: + raise IOError(errno.ENOENT, '%s is deleted' % path) + data = self.store.getfile(path) + isexec = self.execfiles.get(path) + islink = self.symlinks.get(path) + copied = self.copies.get(path) + return data, isexec, islink, copied + + def pop(self, path): + ret = self.get(path) + self.store.popfile(path) + return ret def delete(self, path): self.deleted[path] = True - if path in self.files: - del self.files[path] + self.store.delfile(path) self.execfiles[path] = False self.symlinks[path] = False self.ui.note('D %s\n' % path) - def findmissing(self, svn): - - if not self.missing: - return - - msg = 'fetching %s files that could not use replay.\n' - self.ui.debug(msg % len(self.missing)) - root = svn.subdir and svn.subdir[1:] or '' - r = self.rev.revnum - - files = set() - for p in self.missing: - self.ui.note('.') - self.ui.flush() - if p[-1] == '/': - dir = p[len(root):] - new = [p + f for f, k in svn.list_files(dir, r) if k == 'f'] - files.update(new) - else: - files.add(p) + def files(self): + """Return a sorted list of changed files.""" + files = set(self.store.files()) + for g in (self.symlinks, self.execfiles, self.deleted): + files.update(g) + return sorted(files) - i = 1 - self.ui.note('\nfetching files...\n') - for p in files: - self.ui.note('.') - self.ui.flush() - if i % 50 == 0: - svn.init_ra_and_client() - i += 1 - data, mode = svn.get_file(p[len(root):], r) - self.set(p, data, 'x' in mode, 'l' in mode) - - self.missing = set() - self.ui.note('\n') + def close(self): + self.store.close() +class CopiedFile(object): + def __init__(self, node, path, copypath): + self.node = node + self.path = path + self.copypath = copypath + + def resolve(self, getctxfn, ctx=None): + if ctx is None: + ctx = getctxfn(self.node) + fctx = ctx[self.path] + data = fctx.data() + flags = fctx.flags() + islink = 'l' in flags + if islink: + data = 'link ' + data + return data, 'x' in flags, islink, self.copypath class HgEditor(svnwrap.Editor): @@ -110,54 +175,135 @@ self.ui = meta.ui self.repo = meta.repo self.current = RevisionData(meta.ui) + self._clear() + + def setsvn(self, svn): + self._svn = svn + + def _clear(self): + self._filecounter = 0 + # A mapping of svn paths to CopiedFile entries + self._svncopies = {} + # A mapping of batons to (path, data, isexec, islink, copypath) tuples + # data is a SimpleStringIO if the file was edited, a string + # otherwise. + self._openfiles = {} + # A mapping of file paths to batons + self._openpaths = {} + self._deleted = set() + self._getctx = util.lrucachefunc(self.repo.changectx, 3) + # A stack of opened directory (baton, path) pairs. + self._opendirs = [] + self._missing = set() + + def _openfile(self, path, data, isexec, islink, copypath, create=False): + if path in self._openpaths: + raise EditingError('trying to open an already opened file %s' + % path) + if not create and path in self._deleted: + raise EditingError('trying to open a deleted file %s' % path) + if path in self._deleted: + self._deleted.remove(path) + self._filecounter += 1 + baton = 'f%d-%s' % (self._filecounter, path) + self._openfiles[baton] = (path, data, isexec, islink, copypath) + self._openpaths[path] = baton + return baton + + def _opendir(self, path): + self._filecounter += 1 + baton = 'f%d-%s' % (self._filecounter, path) + self._opendirs.append((baton, path)) + return baton + + def _checkparentdir(self, baton): + if not self._opendirs: + raise EditingError('trying to operate on an already closed ' + 'directory: %s' % baton) + if self._opendirs[-1][0] != baton: + raise EditingError('can only operate on the most recently ' + 'opened directory: %s != %s' % (self._opendirs[-1][0], baton)) + + def _deletefile(self, path): + if self.meta.is_path_valid(path): + self._deleted.add(path) + if path in self._svncopies: + del self._svncopies[path] + self._missing.discard(path) + + def addmissing(self, path, isdir=False): + svn = self._svn + root = svn.subdir and svn.subdir[1:] or '' + if not isdir: + self._missing.add(path[len(root):]) + else: + # Resolve missing directories content immediately so the + # missing files maybe processed by delete actions. + rev = self.current.rev.revnum + path = path + '/' + parentdir = path[len(root):] + for f, k in svn.list_files(parentdir, rev): + if k != 'f': + continue + f = parentdir + f + if not self.meta.is_path_valid(f, False): + continue + self._missing.add(f) @svnwrap.ieditor def delete_entry(self, path, revision_bogus, parent_baton, pool=None): + self._checkparentdir(parent_baton) br_path, branch = self.meta.split_branch_path(path)[:2] if br_path == '': if self.meta.get_path_tag(path): # Tag deletion is not handled as branched deletion return self.meta.closebranches.add(branch) + + # Delete copied entries, no need to check they exist in hg + # parent revision. + if path in self._svncopies: + del self._svncopies[path] + prefix = path + '/' + for f in list(self._svncopies): + if f.startswith(prefix): + self._deletefile(f) + if path in self._missing: + self._missing.remove(path) + else: + for f in list(self._missing): + if f.startswith(prefix): + self._missing.remove(f) + if br_path is not None: ha = self.meta.get_parent_revision(self.current.rev.revnum, branch) if ha == revlog.nullid: return - ctx = self.repo.changectx(ha) + ctx = self._getctx(ha) if br_path not in ctx: br_path2 = '' if br_path != '': br_path2 = br_path + '/' # assuming it is a directory self.current.externals[path] = None - map(self.current.delete, [pat for pat in self.current.files.iterkeys() - if pat.startswith(path + '/')]) for f in ctx.walk(util.PrefixMatch(br_path2)): f_p = '%s/%s' % (path, f[len(br_path2):]) - if f_p not in self.current.files: - self.current.delete(f_p) - self.current.delete(path) + self._deletefile(f_p) + self._deletefile(path) @svnwrap.ieditor def open_file(self, path, parent_baton, base_revision, p=None): - self.current.file = None + self._checkparentdir(parent_baton) + if not self.meta.is_path_valid(path): + return None fpath, branch = self.meta.split_branch_path(path)[:2] - if not fpath: - self.ui.debug('WARNING: Opening non-existant file %s\n' % path) - return - self.current.file = path self.ui.note('M %s\n' % path) - if base_revision != -1: - self.current.base = base_revision - else: - self.current.base = None - if self.current.file in self.current.files: - return - - if not self.meta.is_path_valid(path): - return + if path in self._svncopies: + copy = self._svncopies.pop(path) + base, isexec, islink, copypath = copy.resolve(self._getctx) + return self._openfile(path, base, isexec, islink, copypath) baserev = base_revision if baserev is None or baserev == -1: @@ -166,64 +312,92 @@ # replacing branch as parent, but svn delta editor provides delta # agains replaced branch. parent = self.meta.get_parent_revision(baserev + 1, branch, True) - ctx = self.repo[parent] + ctx = self._getctx(parent) if fpath not in ctx: - self.current.missing.add(path) - return + self.addmissing(path) + return None fctx = ctx.filectx(fpath) base = fctx.data() - if 'l' in fctx.flags(): + flags = fctx.flags() + if 'l' in flags: base = 'link ' + base - self.current.set(path, base, 'x' in fctx.flags(), 'l' in fctx.flags()) + return self._openfile(path, base, 'x' in flags, 'l' in flags, None) @svnwrap.ieditor def add_file(self, path, parent_baton=None, copyfrom_path=None, copyfrom_revision=None, file_pool=None): - self.current.file = None - self.current.base = None - if path in self.current.deleted: - del self.current.deleted[path] + self._checkparentdir(parent_baton) + # Use existing=False because we use the fact a file is being + # added here to populate the branchmap which is used with + # existing=True. fpath, branch = self.meta.split_branch_path(path, existing=False)[:2] - if not fpath: - return + if not fpath or fpath not in self.meta.filemap: + return None + if path in self._svncopies: + raise EditingError('trying to replace copied file %s' % path) + if path in self._deleted: + self._deleted.remove(path) if (branch not in self.meta.branches and not self.meta.get_path_tag(self.meta.remotename(branch))): - # we know this branch will exist now, because it has at least one file. Rock. + # we know this branch will exist now, because it has at + # least one file. Rock. self.meta.branches[branch] = None, 0, self.current.rev.revnum - self.current.file = path if not copyfrom_path: self.ui.note('A %s\n' % path) - self.current.set(path, '', False, False) - return + self.current.added.add(path) + return self._openfile(path, '', False, False, None, create=True) self.ui.note('A+ %s\n' % path) (from_file, from_branch) = self.meta.split_branch_path(copyfrom_path)[:2] if not from_file: - self.current.missing.add(path) - return + self.addmissing(path) + return None # Use exact=True because during replacements ('R' action) we select # replacing branch as parent, but svn delta editor provides delta # agains replaced branch. ha = self.meta.get_parent_revision(copyfrom_revision + 1, from_branch, True) - ctx = self.repo.changectx(ha) - if from_file in ctx: - fctx = ctx.filectx(from_file) - flags = fctx.flags() - self.current.set(path, fctx.data(), 'x' in flags, 'l' in flags) - if from_branch == branch: - parentid = self.meta.get_parent_revision( - self.current.rev.revnum, branch) - if parentid != revlog.nullid: - parentctx = self.repo.changectx(parentid) - if util.issamefile(parentctx, ctx, from_file): - self.current.copies[path] = from_file + ctx = self._getctx(ha) + if from_file not in ctx: + self.addmissing(path) + return None + + fctx = ctx.filectx(from_file) + flags = fctx.flags() + self.current.set(path, fctx.data(), 'x' in flags, 'l' in flags) + copypath = None + if from_branch == branch: + parentid = self.meta.get_parent_revision( + self.current.rev.revnum, branch) + if parentid != revlog.nullid: + parentctx = self._getctx(parentid) + if util.issamefile(parentctx, ctx, from_file): + copypath = from_file + return self._openfile(path, fctx.data(), 'x' in flags, 'l' in flags, + copypath, create=True) + + @svnwrap.ieditor + def close_file(self, file_baton, checksum, pool=None): + if file_baton is None: + return + if file_baton not in self._openfiles: + raise EditingError('trying to close a non-open file %s' + % file_baton) + path, data, isexec, islink, copypath = self._openfiles.pop(file_baton) + del self._openpaths[path] + if not isinstance(data, basestring): + # Files can be opened, properties changed and apply_text + # never called, in which case data is still a string. + data = data.getvalue() + self.current.set(path, data, isexec, islink, copypath) @svnwrap.ieditor def add_directory(self, path, parent_baton, copyfrom_path, copyfrom_revision, dir_pool=None): - self.current.batons[path] = path + self._checkparentdir(parent_baton) + baton = self._opendir(path) + br_path, branch = self.meta.split_branch_path(path)[:2] if br_path is not None: if not copyfrom_path and not br_path: @@ -231,16 +405,20 @@ else: self.current.emptybranches[branch] = False if br_path is None or not copyfrom_path: - return path + return baton if self.meta.get_path_tag(path): del self.current.emptybranches[branch] - return path + return baton tag = self.meta.get_path_tag(copyfrom_path) if tag not in self.meta.tags: tag = None - if not self.meta.is_path_valid(copyfrom_path): - self.current.missing.add('%s/' % path) - return path + if not self.meta.is_path_valid(copyfrom_path, existing=False): + # The source path only exists at copyfrom_revision, use + # existing=False to guess a possible branch location and + # test it against the filemap. The actual path and + # revision will be resolved below if necessary. + self.addmissing(path, isdir=True) + return baton if tag: changeid = self.meta.tags[tag] source_rev, source_branch = self.meta.get_source_rev(changeid)[:2] @@ -248,39 +426,61 @@ else: source_rev = copyfrom_revision frompath, source_branch = self.meta.split_branch_path(copyfrom_path)[:2] - if frompath == '' and br_path == '': - assert br_path is not None - tmp = source_branch, source_rev, self.current.rev.revnum - self.meta.branches[branch] = tmp new_hash = self.meta.get_parent_revision(source_rev + 1, source_branch, True) if new_hash == node.nullid: - self.current.missing.add('%s/' % path) - return path - fromctx = self.repo.changectx(new_hash) + self.addmissing(path, isdir=True) + return baton + fromctx = self._getctx(new_hash) if frompath != '/' and frompath != '': frompath = '%s/' % frompath else: frompath = '' + + copyfromparent = False + if frompath == '' and br_path == '': + pnode = self.meta.get_parent_revision( + self.current.rev.revnum, branch) + if pnode == new_hash: + # Data parent is topological parent and relative paths + # are the same, not need to do anything but restore + # files marked as deleted. + copyfromparent = True + # Get the parent which would have been used for this branch + # without the replace action. + oldpnode = self.meta.get_parent_revision( + self.current.rev.revnum, branch, exact=True) + if (oldpnode != revlog.nullid + and util.isancestor(self._getctx(oldpnode), fromctx)): + # Branch-wide replacement, unmark the branch as deleted + self.meta.closebranches.discard(branch) + + svncopies = {} copies = {} for f in fromctx: if not f.startswith(frompath): continue - fctx = fromctx.filectx(f) dest = path + '/' + f[len(frompath):] - self.current.set(dest, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) - if dest in self.current.deleted: - del self.current.deleted[dest] + if not self.meta.is_path_valid(dest): + continue + if dest in self._deleted: + self._deleted.remove(dest) + if copyfromparent: + continue + svncopies[dest] = CopiedFile(new_hash, f, None) if branch == source_branch: copies[dest] = f if copies: # Preserve the directory copy records if no file was changed between # the source and destination revisions, or discard it completely. - parentid = self.meta.get_parent_revision(self.current.rev.revnum, branch) + parentid = self.meta.get_parent_revision( + self.current.rev.revnum, branch) if parentid != revlog.nullid: - parentctx = self.repo.changectx(parentid) + parentctx = self._getctx(parentid) for k, v in copies.iteritems(): if util.issamefile(parentctx, fromctx, v): - self.current.copies[k] = v + svncopies[k].copypath = v + self._svncopies.update(svncopies) + # Copy the externals definitions of copied directories fromext = svnexternals.parse(self.ui, fromctx) for p, v in fromext.iteritems(): @@ -288,62 +488,67 @@ if pp.startswith(frompath): dest = (path + '/' + pp[len(frompath):]).rstrip('/') self.current.externals[dest] = v - return path + return baton @svnwrap.ieditor def change_file_prop(self, file_baton, name, value, pool=None): + if file_baton is None: + return + path, data, isexec, islink, copypath = self._openfiles[file_baton] + changed = False if name == 'svn:executable': - self.current.execfiles[self.current.file] = bool(value is not None) + changed = True + isexec = bool(value is not None) elif name == 'svn:special': - self.current.symlinks[self.current.file] = bool(value is not None) + changed = True + islink = bool(value is not None) + if changed: + self._openfiles[file_baton] = (path, data, isexec, islink, copypath) @svnwrap.ieditor def change_dir_prop(self, dir_baton, name, value, pool=None): - if dir_baton is None: + self._checkparentdir(dir_baton) + if len(self._opendirs) == 1: return - path = self.current.batons[dir_baton] + path = self._opendirs[-1][1] if name == 'svn:externals': self.current.externals[path] = value @svnwrap.ieditor + def open_root(self, edit_baton, base_revision, dir_pool=None): + # We should not have to reset these, unfortunately the editor is + # reused for different revisions. + self._clear() + return self._opendir('') + + @svnwrap.ieditor def open_directory(self, path, parent_baton, base_revision, dir_pool=None): - self.current.batons[path] = path + self._checkparentdir(parent_baton) + baton = self._opendir(path) p_, branch = self.meta.split_branch_path(path)[:2] if p_ == '' or (self.meta.layout == 'single' and p_): if not self.meta.get_path_tag(path): self.current.emptybranches[branch] = False - return path + return baton @svnwrap.ieditor def close_directory(self, dir_baton, dir_pool=None): - if dir_baton is not None: - del self.current.batons[dir_baton] + self._checkparentdir(dir_baton) + self._opendirs.pop() @svnwrap.ieditor def apply_textdelta(self, file_baton, base_checksum, pool=None): - # We know coming in here the file must be one of the following options: - # 1) Deleted (invalid, fail an assertion) - # 2) Missing a base text (bail quick since we have to fetch a full plaintext) - # 3) Has a base text in self.current.files, apply deltas - base = '' - if not self.meta.is_path_valid(self.current.file): + if file_baton is None: return lambda x: None - - if self.current.file in self.current.deleted: - msg = ('cannot apply textdelta to %s: file is deleted' - % self.current.file) - raise IOError(errno.ENOENT, msg) - - if (self.current.file not in self.current.files and - self.current.file not in self.current.missing): - msg = ('cannot apply textdelta to %s: file not found' - % self.current.file) - raise IOError(errno.ENOENT, msg) - - if self.current.file in self.current.missing: + if file_baton not in self._openfiles: + raise EditingError('trying to patch a closed file %s' % file_baton) + path, base, isexec, islink, copypath = self._openfiles[file_baton] + if not isinstance(base, basestring): + raise EditingError('trying to edit a file again: %s' % path) + if not self.meta.is_path_valid(path): return lambda x: None - base = self.current.files[self.current.file] - target = NeverClosingStringIO() + + target = svnwrap.SimpleStringIO(closing=False) self.stream = target handler = svnwrap.apply_txdelta(base, target) @@ -352,19 +557,92 @@ 'cannot call handler!') def txdelt_window(window): try: - if not self.meta.is_path_valid(self.current.file): + if not self.meta.is_path_valid(path): return - handler(window) + try: + handler(window) + except AssertionError, e: # pragma: no cover + # Enhance the exception message + msg, others = e.args[0], e.args[1:] + + if msg: + msg += '\n' + + msg += _TXDELT_WINDOW_HANDLER_FAILURE_MSG + e.args = (msg,) + others + raise e + # window being None means commit this file if not window: - self.current.files[self.current.file] = target.getvalue() + self._openfiles[file_baton] = ( + path, target, isexec, islink, copypath) except svnwrap.SubversionException, e: # pragma: no cover if e.args[1] == svnwrap.ERR_INCOMPLETE_DATA: - self.current.missing.add(self.current.file) + self.addmissing(path) else: # pragma: no cover raise hgutil.Abort(*e.args) except: # pragma: no cover - print len(base), self.current.file self._exception_info = sys.exc_info() raise return txdelt_window + + def close(self): + if self._openfiles: + for e in self._openfiles.itervalues(): + self.ui.debug('error: %s was not closed\n' % e[0]) + raise EditingError('%d edited files were not closed' + % len(self._openfiles)) + + if self._opendirs: + raise EditingError('directory %s was not closed' + % self._opendirs[-1][1]) + + # Resolve by changelog entries to avoid extra reads + nodes = {} + for path, copy in self._svncopies.iteritems(): + nodes.setdefault(copy.node, []).append((path, copy)) + for node, copies in nodes.iteritems(): + for path, copy in copies: + data, isexec, islink, copied = copy.resolve(self._getctx) + self.current.set(path, data, isexec, islink, copied) + self._svncopies.clear() + + # Resolve missing files + if self._missing: + missing = sorted(self._missing) + self.ui.debug('fetching %s files that could not use replay.\n' + % len(missing)) + if self.ui.configbool('hgsubversion', 'failonmissing', False): + raise EditingError('missing entry: %s' % missing[0]) + + svn = self._svn + rev = self.current.rev.revnum + root = svn.subdir and svn.subdir[1:] or '' + i = 1 + for f in missing: + if self.ui.debugflag: + self.ui.debug('fetching %s\n' % f) + else: + self.ui.note('.') + self.ui.flush() + if i % 50 == 0: + svn.init_ra_and_client() + i += 1 + data, mode = svn.get_file(f, rev) + self.current.set(f, data, 'x' in mode, 'l' in mode) + if not self.ui.debugflag: + self.ui.note('\n') + + for f in self._deleted: + self.current.delete(f) + self._deleted.clear() + +_TXDELT_WINDOW_HANDLER_FAILURE_MSG = ( + "Your SVN repository may not be supplying correct replay deltas." + " It is strongly" + "\nadvised that you repull the entire SVN repository using" + " hg pull --stupid." + "\nAlternatively, re-pull just this revision using --stupid and verify" + " that the" + "\nchangeset is correct." +) diff -Nru hgsubversion-1.4/hgsubversion/help/subversion.rst hgsubversion-1.5/hgsubversion/help/subversion.rst --- hgsubversion-1.4/hgsubversion/help/subversion.rst 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/help/subversion.rst 2012-10-29 11:16:19.000000000 +0000 @@ -302,6 +302,25 @@ be included or excluded. See the documentation for ``hg convert`` for more information on filemaps. + ``hgsubversion.filestoresize`` + + Maximum amount of temporary edited files data to be kept in memory, + in megabytes. The replay and stupid mode pull data by retrieving + delta information from the subversion repository and applying it on + known files data. Since the order of file edits is driven by the + subversion delta information order, edited files cannot be committed + immediately and are kept until all of them have been processed for + each changeset. ``filestoresize`` defines the maximum amount of + files data to be kept in memory before falling back to storing them + in a temporary directory. This setting is important with + repositories containing many files or large ones as both the + application of deltas and Mercurial commit process require the whole + file data to be available in memory. By limiting the amount of + temporary data kept in memory, larger files can be retrieved, at the + price of slower disk operations. Set it to a negative value to + disable the fallback behaviour and keep everything in memory. + Default to 200. + ``hgsubversion.username``, ``hgsubversion.password`` Set the username or password for accessing Subversion repositories. @@ -352,6 +371,24 @@ contain tags. The default is to only look in ``tags``. This option has no effect for single-directory clones. + ``hgsubversion.unsafeskip`` + + A space or comma separated list of Subversion revision numbers to + skip over when pulling or cloning. This can be useful for + troublesome commits, such as someone accidentally deleting trunk + and then restoring it. (In delete-and-restore cases, you may also + need to clone or pull in multiple steps, to help hgsubversion + track history correctly.) + + NOTE: this option is dangerous. Careless use can make it + impossible to pull later Subversion revisions cleanly, e.g. if the + content of a file depends on changes made in a skipped rev. + Skipping a rev may also prevent future invocations of ``hg svn + verify`` from succeeding (if the contents of the Mercurial repo + become out of step with the contents of the Subversion repo). If + you use this option, be sure to carefully check the result of a + pull afterwards. + Please note that some of these options may be specified as command line options as well, and when done so, will override the configuration. If an authormap, filemap or branchmap is specified, its contents will be read and stored for use diff -Nru hgsubversion-1.4/hgsubversion/hooks/updatemeta.py hgsubversion-1.5/hgsubversion/hooks/updatemeta.py --- hgsubversion-1.4/hgsubversion/hooks/updatemeta.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/hooks/updatemeta.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,31 @@ +# Mercurial hook to update/rebuild svn metadata if there are svn changes in +# the incoming changegroup. +# +# To install, add the following to your hgrc: +# [hooks] +# changegroup = python:hgsubversion.hooks.updatemeta.hook + +from mercurial import node + +import hgsubversion +import hgsubversion.util +import hgsubversion.svncommands + +def hook(ui, repo, **kwargs): + updatemeta = False + startrev = repo[node.bin(kwargs["node"])].rev() + # Check each rev until we find one that contains svn metadata + for rev in xrange(startrev, len(repo)): + svnrev = hgsubversion.util.getsvnrev(repo[rev]) + if svnrev and svnrev.startswith("svn:"): + updatemeta = True + break + + if updatemeta: + try: + hgsubversion.svncommands.updatemeta(ui, repo, args=[]) + ui.status("Updated svn metadata\n") + except Exception, e: + ui.warn("Failed to update svn metadata: %s" % str(e)) + + return False diff -Nru hgsubversion-1.4/hgsubversion/maps.py hgsubversion-1.5/hgsubversion/maps.py --- hgsubversion-1.4/hgsubversion/maps.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/maps.py 2012-10-29 11:16:19.000000000 +0000 @@ -1,5 +1,6 @@ ''' Module for self-contained maps. ''' +import errno import os from mercurial import util as hgutil from mercurial import node @@ -134,8 +135,7 @@ svncommands.rebuildmeta(repo.ui, repo, ()) return elif ver != self.VERSION: - print 'tagmap too new -- please upgrade' - raise NotImplementedError + raise hgutil.Abort('tagmap too new -- please upgrade') for l in f: ha, revision, tag = l.split(' ', 2) revision = int(revision) @@ -182,7 +182,8 @@ def __init__(self, repo): dict.__init__(self) - self.path = os.path.join(repo.path, 'svn', 'rev_map') + self.path = self.mappath(repo) + self.repo = repo self.ypath = os.path.join(repo.path, 'svn', 'lastpulled') # TODO(durin42): Consider moving management of the youngest # file to svnmeta itself rather than leaving it here. @@ -212,13 +213,25 @@ check = lambda x: x[0][1] == branch and x[0][0] < rev.revnum return sorted(filter(check, self.iteritems()), reverse=True) - def _load(self): - f = open(self.path) + @staticmethod + def mappath(repo): + return os.path.join(repo.path, 'svn', 'rev_map') + + @classmethod + def readmapfile(cls, repo, missingok=True): + try: + f = open(cls.mappath(repo)) + except IOError, err: + if not missingok or err.errno != errno.ENOENT: + raise + return iter([]) ver = int(f.readline()) - if ver != self.VERSION: - print 'revmap too new -- please upgrade' - raise NotImplementedError - for l in f: + if ver != cls.VERSION: + raise hgutil.Abort('revmap too new -- please upgrade') + return f + + def _load(self): + for l in self.readmapfile(self.repo): revnum, ha, branch = l.split(' ', 2) if branch == '\n': branch = None @@ -230,7 +243,6 @@ if revnum < self.oldest or not self.oldest: self.oldest = revnum dict.__setitem__(self, (revnum, branch), node.bin(ha)) - f.close() def _write(self): f = open(self.path, 'w') @@ -311,7 +323,7 @@ msg = 'duplicate %s entry in %s: "%s"\n' self.ui.status(msg % (m, fn, path)) return - bits = m.strip('e'), path + bits = m.rstrip('e'), path self.ui.debug('%sing %s\n' % bits) # respect rule order mapping[path] = len(self) @@ -347,8 +359,7 @@ f = open(self.path) ver = int(f.readline()) if ver != self.VERSION: - print 'filemap too new -- please upgrade' - raise NotImplementedError + raise hgutil.Abort('filemap too new -- please upgrade') self.load_fd(f, self.path) f.close() diff -Nru hgsubversion-1.4/hgsubversion/pushmod.py hgsubversion-1.5/hgsubversion/pushmod.py --- hgsubversion-1.4/hgsubversion/pushmod.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/pushmod.py 2012-10-29 11:16:19.000000000 +0000 @@ -82,6 +82,11 @@ added.append(d) for d in olddirs: + if not d: + # Do not remove the root directory when the hg repo becomes + # empty. hgsubversion cannot create branches, do not remove + # them. + continue if d not in newdirs and _isdir(svn, branchpath, d): deleted.append(d) @@ -133,6 +138,10 @@ # this kind of renames: a -> b, b -> c copies[file] = renamed[0] base_data = parent[renamed[0]].data() + else: + autoprops = svn.autoprops_config.properties(file) + if autoprops: + props.setdefault(file, {}).update(autoprops) action = 'add' dirname = '/'.join(file.split('/')[:-1] + ['']) diff -Nru hgsubversion-1.4/hgsubversion/replay.py hgsubversion-1.5/hgsubversion/replay.py --- hgsubversion-1.4/hgsubversion/replay.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/replay.py 2012-10-29 11:16:19.000000000 +0000 @@ -30,7 +30,6 @@ branches = {} for path, entry in current.externals.iteritems(): if not meta.is_path_valid(path): - ui.warn('WARNING: Invalid path %s in externals\n' % path) continue p, b, bp = meta.split_branch_path(path) @@ -52,11 +51,28 @@ else: current.delete(path) + +def _safe_message(msg): + if msg: + try: + msg.decode('utf-8') + except UnicodeDecodeError: + # ancient svn failed to enforce utf8 encoding + return msg.decode('iso-8859-1').encode('utf-8') + return msg + def convert_rev(ui, meta, svn, r, tbdelta, firstrun): + try: + return _convert_rev(ui, meta, svn, r, tbdelta, firstrun) + finally: + meta.editor.current.close() + +def _convert_rev(ui, meta, svn, r, tbdelta, firstrun): editor = meta.editor editor.current.clear() editor.current.rev = r + editor.setsvn(svn) if firstrun and meta.revmap.oldest <= 0: # We know nothing about this project, so fetch everything before @@ -65,33 +81,28 @@ svn.get_revision(r.revnum, editor) else: svn.get_replay(r.revnum, editor, meta.revmap.oldest) + editor.close() current = editor.current - current.findmissing(svn) updateexternals(ui, meta, current) if current.exception is not None: # pragma: no cover traceback.print_exception(*current.exception) raise ReplayException() - if current.missing: - raise MissingPlainTextError() - # paranoidly generate the list of files to commit - files_to_commit = set(current.files.keys()) - files_to_commit.update(current.symlinks.keys()) - files_to_commit.update(current.execfiles.keys()) - files_to_commit.update(current.deleted.keys()) - # back to a list and sort so we get sane behavior - files_to_commit = list(files_to_commit) - files_to_commit.sort() + files_to_commit = current.files() branch_batches = {} rev = current.rev date = meta.fixdate(rev.date) # build up the branches that have files on them + failoninvalid = ui.configbool('hgsubversion', + 'failoninvalidreplayfile', False) for f in files_to_commit: if not meta.is_path_valid(f): + if failoninvalid: + raise hgutil.Abort('file %s should not be in commit list' % f) continue p, b = meta.split_branch_path(f)[:2] if b not in branch_batches: @@ -144,30 +155,33 @@ def filectxfn(repo, memctx, path): current_file = files[path] - if current_file in current.deleted: - raise IOError(errno.ENOENT, '%s is deleted' % path) - copied = current.copies.get(current_file) - flags = parentctx.flags(path) - is_exec = current.execfiles.get(current_file, 'x' in flags) - is_link = current.symlinks.get(current_file, 'l' in flags) - if current_file in current.files: - data = current.files[current_file] - if is_link and data.startswith('link '): - data = data[len('link '):] - elif is_link: - ui.debug('file marked as link, but may contain data: ' - '%s (%r)\n' % (current_file, flags)) + data, isexec, islink, copied = current.pop(current_file) + if isexec is None or islink is None: + flags = parentctx.flags(path) + if isexec is None: + isexec = 'x' in flags + if islink is None: + islink = 'l' in flags + + if data is not None: + if islink: + if data.startswith('link '): + data = data[len('link '):] + else: + ui.debug('file marked as link, but may contain data: ' + '%s\n' % current_file) else: data = parentctx.filectx(path).data() return context.memfilectx(path=path, data=data, - islink=is_link, isexec=is_exec, + islink=islink, isexec=isexec, copied=copied) + message = _safe_message(rev.message) meta.mapbranch(extra) current_ctx = context.memctx(meta.repo, parents, - rev.message or util.default_commit_msg(ui), + message or util.default_commit_msg(ui), files.keys(), filectxfn, meta.authors[rev.author], @@ -203,7 +217,7 @@ current_ctx = context.memctx(meta.repo, (ha, node.nullid), - rev.message or ' ', + _safe_message(rev.message) or ' ', [], del_all_files, meta.authors[rev.author], diff -Nru hgsubversion-1.4/hgsubversion/stupid.py hgsubversion-1.5/hgsubversion/stupid.py --- hgsubversion-1.4/hgsubversion/stupid.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/stupid.py 2012-10-29 11:16:19.000000000 +0000 @@ -22,7 +22,7 @@ # a # a # +a -# +# # Property changes on: a # ___________________________________________________________________ # Added: svn:executable @@ -235,15 +235,20 @@ def patchrepo(ui, meta, parentctx, patchfp): if not svnbackend: return patchrepoold(ui, meta, parentctx, patchfp) - store = patch.filestore() + store = patch.filestore(util.getfilestoresize(ui)) try: touched = set() backend = svnbackend(ui, meta.repo, parentctx, store) - ret = patch.patchbackend(ui, backend, patchfp, 0, touched) - if ret < 0: - raise BadPatchApply('patching failed') - if ret > 0: - raise BadPatchApply('patching succeeded with fuzz') + + try: + ret = patch.patchbackend(ui, backend, patchfp, 0, touched) + if ret < 0: + raise BadPatchApply('patching failed') + if ret > 0: + raise BadPatchApply('patching succeeded with fuzz') + except patch.PatchError, e: + raise BadPatchApply(str(e)) + files = {} for f in touched: try: @@ -271,8 +276,8 @@ if prev is None or pbranch == branch: # letting patch handle binaries sounded # cool, but it breaks patch in sad ways - d = svn.get_unified_diff(branchpath, r.revnum, deleted=False, - ignore_type=False) + d = svn.get_unified_diff(branchpath, r.revnum, other_rev=prev, + deleted=False, ignore_type=False) else: d = svn.get_unified_diff(branchpath, r.revnum, other_path=ppath, other_rev=prev, @@ -534,7 +539,12 @@ else: branchprefix = (branchpath and branchpath + '/') or '' for path, e in r.paths.iteritems(): - if not path.startswith(branchprefix): + if path == branchpath: + if e.action != 'R' or branch not in meta.branches: + # Full-branch replacements are handled as reverts, + # skip everything else. + continue + elif not path.startswith(branchprefix): continue if not meta.is_path_valid(path): continue @@ -698,6 +708,20 @@ branch = meta.localname(p) if not (r.paths[p].action == 'R' and branch in meta.branches): continue + # Check the branch is not being replaced by one of its + # ancestors, it happens a lot with project-wide reverts. + frompath = r.paths[p].copyfrom_path + frompath, frombranch = meta.split_branch_path( + frompath, existing=False)[:2] + if frompath == '': + fromnode = meta.get_parent_revision( + r.paths[p].copyfrom_rev + 1, frombranch, exact=True) + if fromnode != node.nullid: + fromctx = meta.repo[fromnode] + pctx = meta.repo[meta.get_parent_revision( + r.revnum, branch, exact=True)] + if util.isancestor(pctx, fromctx): + continue closed = checkbranch(meta, r, branch) if closed is not None: deleted_branches[branch] = closed diff -Nru hgsubversion-1.4/hgsubversion/svncommands.py hgsubversion-1.5/hgsubversion/svncommands.py --- hgsubversion-1.4/hgsubversion/svncommands.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svncommands.py 2012-10-29 11:16:19.000000000 +0000 @@ -3,6 +3,7 @@ import cPickle as pickle import sys import traceback +import urlparse from mercurial import commands from mercurial import hg @@ -15,82 +16,28 @@ import svnrepo import util import svnexternals +import verify -def verify(ui, repo, args=None, **opts): - '''verify current revision against Subversion repository - ''' +def updatemeta(ui, repo, args, **opts): + """Do a partial rebuild of the subversion metadata. - if repo is None: - raise error.RepoError("There is no Mercurial repository" - " here (.hg not found)") - - ctx = repo[opts.get('rev', '.')] - if 'close' in ctx.extra(): - ui.write('cannot verify closed branch') - return 0 - convert_revision = ctx.extra().get('convert_revision') - if convert_revision is None or not convert_revision.startswith('svn:'): - raise hgutil.Abort('revision %s not from SVN' % ctx) - - if args: - url = repo.ui.expandpath(args[0]) - else: - url = repo.ui.expandpath('default') - - svn = svnrepo.svnremoterepo(ui, url).svn - meta = repo.svnmeta(svn.uuid, svn.subdir) - srev, branch, branchpath = meta.get_source_rev(ctx=ctx) - - branchpath = branchpath[len(svn.subdir.lstrip('/')):] - branchurl = ('%s/%s' % (url, branchpath)).strip('/') + Assumes that the metadata that currently exists is valid, but that + some is missing, e.g. because you have pulled some revisions via a + native mercurial method. - ui.write('verifying %s against %s@%i\n' % (ctx, branchurl, srev)) - - svnfiles = set() - result = 0 - - svndata = svn.list_files(branchpath, srev) - for i, (fn, type) in enumerate(svndata): - util.progress(ui, 'verify', i) - if type != 'f': - continue - svnfiles.add(fn) - fp = fn - if branchpath: - fp = branchpath + '/' + fn - data, mode = svn.get_file(posixpath.normpath(fp), srev) - try: - fctx = ctx[fn] - except error.LookupError: - result = 1 - continue - dmatch = fctx.data() == data - mmatch = fctx.flags() == mode - if not (dmatch and mmatch): - ui.write('difference in file %s\n' % fn) - result = 1 - - hgfiles = set(ctx) - util.ignoredfiles - if hgfiles != svnfiles: - unexpected = hgfiles - svnfiles - if unexpected: - ui.write('unexpected files:\n') - for f in sorted(unexpected): - ui.write(' %s\n' % f) - missing = svnfiles - hgfiles - if missing: - ui.write('missing files:\n') - for f in sorted(missing): - ui.write(' %s\n' % f) - result = 1 + """ - return result + return _buildmeta(ui, repo, args, partial=True) -def rebuildmeta(ui, repo, args, **opts): +def rebuildmeta(ui, repo, args, unsafe_skip_uuid_check=False, **opts): """rebuild hgsubversion metadata using values stored in revisions """ + return _buildmeta(ui, repo, args, partial=False, + skipuuid=unsafe_skip_uuid_check) + +def _buildmeta(ui, repo, args, partial=False, skipuuid=False): if repo is None: raise error.RepoError("There is no Mercurial repository" @@ -110,14 +57,41 @@ if not os.path.exists(svnmetadir): os.makedirs(svnmetadir) + youngest = 0 + startrev = 0 + sofar = [] + branchinfo = {} + if partial: + try: + youngestpath = os.path.join(svnmetadir, 'lastpulled') + foundpartialinfo = False + if os.path.exists(youngestpath): + youngest = int(util.load_string(youngestpath).strip()) + sofar = list(maps.RevMap.readmapfile(repo)) + if sofar and len(sofar[-1].split(' ', 2)) > 1: + lasthash = sofar[-1].split(' ', 2)[1] + startrev = repo[lasthash].rev() + 1 + branchinfo = pickle.load(open(os.path.join(svnmetadir, + 'branch_info'))) + foundpartialinfo = True + if not foundpartialinfo: + ui.status('missing some metadata -- doing a full rebuild\n') + partial = False + except IOError, err: + if err.errno != errno.ENOENT: + raise + ui.status('missing some metadata -- doing a full rebuild') + except AttributeError: + ui.status('no metadata available -- doing a full rebuild') + + lastpulled = open(os.path.join(svnmetadir, 'lastpulled'), 'wb') revmap = open(os.path.join(svnmetadir, 'rev_map'), 'w') revmap.write('1\n') + revmap.writelines(sofar) last_rev = -1 - branchinfo = {} - noderevnums = {} tagfile = os.path.join(svnmetadir, 'tagmap') - if os.path.exists(maps.Tags.filepath(repo)): + if not partial and os.path.exists(maps.Tags.filepath(repo)) : os.unlink(maps.Tags.filepath(repo)) tags = maps.Tags(repo) @@ -126,7 +100,7 @@ skipped = set() closed = set() - numrevs = len(repo) + numrevs = len(repo) - startrev subdirfile = open(os.path.join(svnmetadir, 'subdir'), 'w') subdirfile.write(subdir.strip('/')) @@ -136,34 +110,38 @@ # it would make us use O(revisions^2) time, so we perform an extra traversal # of the repository instead. During this traversal, we find all converted # changesets that close a branch, and store their first parent - youngest = 0 - for rev in repo: - util.progress(ui, 'prepare', rev, total=numrevs) + for rev in xrange(startrev, len(repo)): + util.progress(ui, 'prepare', rev - startrev, total=numrevs) ctx = repo[rev] - extra = ctx.extra() - convinfo = extra.get('convert_revision', None) + convinfo = util.getsvnrev(ctx, None) if not convinfo: continue svnrevnum = int(convinfo.rsplit('@', 1)[1]) youngest = max(youngest, svnrevnum) - if extra.get('close', None) is None: + if ctx.extra().get('close', None) is None: continue droprev = lambda x: x.rsplit('@', 1)[0] parentctx = ctx.parents()[0] - parentinfo = parentctx.extra().get('convert_revision', '@') + parentinfo = util.getsvnrev(parentctx, '@') if droprev(parentinfo) == droprev(convinfo): - closed.add(parentctx.rev()) + if parentctx.rev() < startrev: + parentbranch = parentctx.branch() + if parentbranch == 'default': + parentbranch = None + branchinfo.pop(parentbranch) + else: + closed.add(parentctx.rev()) lastpulled.write(str(youngest) + '\n') util.progress(ui, 'prepare', None, total=numrevs) - for rev in repo: - util.progress(ui, 'rebuild', rev, total=numrevs) + for rev in xrange(startrev, len(repo)): + util.progress(ui, 'rebuild', rev-startrev, total=numrevs) ctx = repo[rev] - convinfo = ctx.extra().get('convert_revision', None) + convinfo = util.getsvnrev(ctx, None) if not convinfo: continue if '.hgtags' in ctx.files(): @@ -174,7 +152,7 @@ newdata = ctx.filectx('.hgtags').data() for newtag in newdata[len(parentdata):-1].split('\n'): ha, tag = newtag.split(' ', 1) - tagged = repo[ha].extra().get('convert_revision', None) + tagged = util.getsvnrev(repo[ha], None) if tagged is None: tagged = -1 else: @@ -212,9 +190,12 @@ # write repository uuid if required if uuid is None: uuid = convinfo[4:40] - assert uuid == svn.uuid, 'UUIDs did not match!' + if not skipuuid: + if uuid != svn.uuid: + raise hgutil.Abort('remote svn repository identifier ' + 'does not match') uuidfile = open(os.path.join(svnmetadir, 'uuid'), 'w') - uuidfile.write(uuid) + uuidfile.write(svn.uuid) uuidfile.close() # don't reflect closed branches @@ -239,7 +220,6 @@ revmap.write('%s %s %s\n' % (revision, ctx.hex(), commitpath)) revision = int(revision) - noderevnums[ctx.node()] = revision if revision > last_rev: last_rev = revision @@ -252,7 +232,7 @@ parent = ctx while parent.node() != node.nullid: parentextra = parent.extra() - parentinfo = parentextra.get('convert_revision') + parentinfo = util.getsvnrev(parent) assert parentinfo parent = parent.parents()[0] @@ -277,15 +257,19 @@ pass elif branch not in branchinfo: parent = ctx.parents()[0] - if (parent.node() in noderevnums + if (parent.node() not in skipped + and util.getsvnrev(parent, '').startswith('svn:') and parent.branch() != ctx.branch()): parentbranch = parent.branch() if parentbranch == 'default': parentbranch = None else: parentbranch = None + # branchinfo is a map from mercurial branch to a + # (svn branch, svn parent revision, svn revision) tuple + parentrev = util.getsvnrev(parent, '@').split('@')[1] or 0 branchinfo[branch] = (parentbranch, - noderevnums.get(parent.node(), 0), + int(parentrev), revision) util.progress(ui, 'rebuild', None, total=numrevs) @@ -408,7 +392,7 @@ ui.status('Not a child of an svn revision.\n') return 0 r, br = hashes[pn] - subdir = parent.extra()['convert_revision'][40:].split('@')[0] + subdir = util.getsvnrev(parent)[40:].split('@')[0] if meta.layout == 'single': branchpath = '' elif br == None: @@ -520,8 +504,9 @@ 'listauthors': listauthors, 'update': update, 'help': help_, + 'updatemeta': updatemeta, 'rebuildmeta': rebuildmeta, 'updateexternals': svnexternals.updateexternals, - 'verify': verify, + 'verify': verify.verify, } svn.__doc__ = _helpgen() diff -Nru hgsubversion-1.4/hgsubversion/svnmeta.py hgsubversion-1.5/hgsubversion/svnmeta.py --- hgsubversion-1.4/hgsubversion/svnmeta.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svnmeta.py 2012-10-29 11:16:19.000000000 +0000 @@ -292,7 +292,9 @@ return '' if path and path[0] == '/': path = path[1:] - if path and path.startswith(self.subdir): + if path == self.subdir: + return '' + if path and path.startswith(self.subdir + '/'): path = path[len(self.subdir):] if path and path[0] == '/': path = path[1:] @@ -365,7 +367,7 @@ if existing: return None, None, None if path == 'trunk' or path.startswith('trunk/'): - path = path.split('/')[1:] + path = '/'.join(path.split('/')[1:]) test = 'trunk' elif path.startswith('branches/'): elts = path.split('/') @@ -391,10 +393,10 @@ return {ln: (src_branch, src_rev, revnum)} return {} - def is_path_valid(self, path): + def is_path_valid(self, path, existing=True): if path is None: return False - subpath = self.split_branch_path(path)[0] + subpath = self.split_branch_path(path, existing)[0] if subpath is None: return False return subpath in self.filemap @@ -542,7 +544,9 @@ # 1. Is the file located inside any currently known # branch? If yes, then we're done with it, this isn't # interesting. - # 2. Does the file have copyfrom information? If yes, then + # 2. Does the file have copyfrom information? If yes, and + # the branch is being replaced by what would be an + # ancestor, treat it as a regular revert. Otherwise, # we're done: this is a new branch, and we record the # copyfrom in added_branches if it comes from the root # of another branch, or create it from scratch. @@ -563,6 +567,18 @@ if paths[p].action == 'D': self.closebranches.add(br) # case 4 elif paths[p].action == 'R': + # Check the replacing source is not an ancestor + # branch of the branch being replaced, this + # would just be a revert. + cfi, cbr = self.split_branch_path( + paths[p].copyfrom_path, paths[p].copyfrom_rev)[:2] + if cfi == '': + cctx = self.repo[self.get_parent_revision( + paths[p].copyfrom_rev + 1, cbr)] + ctx = self.repo[self.get_parent_revision( + revision.revnum, br)] + if cctx and util.isancestor(ctx, cctx): + continue parent = self._determine_parent_branch( p, paths[p].copyfrom_path, paths[p].copyfrom_rev, revision.revnum) diff -Nru hgsubversion-1.4/hgsubversion/svnrepo.py hgsubversion-1.5/hgsubversion/svnrepo.py --- hgsubversion-1.4/hgsubversion/svnrepo.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svnrepo.py 2012-10-29 11:16:19.000000000 +0000 @@ -18,8 +18,13 @@ from mercurial import error from mercurial import util as hgutil -from mercurial import httprepo -import mercurial.repo + +try: + from mercurial.peer import peerrepository + from mercurial import httppeer +except ImportError: + from mercurial.repo import repository as peerrepository + from mercurial import httprepo as httppeer try: from mercurial import phases @@ -107,12 +112,14 @@ repo.__class__ = svnlocalrepo -class svnremoterepo(mercurial.repo.repository): +class svnremoterepo(peerrepository): """ the dumb wrapper for actual Subversion repositories """ def __init__(self, ui, path=None): self.ui = ui if path is None: + path = self.ui.config('paths', 'default-push') + if path is None: path = self.ui.config('paths', 'default') if not path: raise hgutil.Abort('no Subversion URL specified') @@ -127,6 +134,9 @@ else: self.password_stores = None + def _capabilities(self): + return self.capabilities + @propertycache def svnauth(self): # DO NOT default the user to hg's getuser(). If you provide @@ -177,7 +187,7 @@ if url.startswith('http://') or url.startswith('https://'): try: # may yield a bogus 'real URL...' message - return httprepo.instance(ui, url, create) + return httppeer.instance(ui, url, create) except error.RepoError: ui.traceback() ui.note('(falling back to Subversion support)\n') @@ -185,4 +195,102 @@ if create: raise hgutil.Abort('cannot create new remote Subversion repository') + svnwrap.prompt_callback(SubversionPrompt(ui)) return svnremoterepo(ui, url) + +class SubversionPrompt(object): + def __init__(self, ui): + self.ui = ui + + def maybe_print_realm(self, realm): + if realm: + self.ui.write('Authentication realm: %s\n' % (realm,)) + self.ui.flush() + + def username(self, realm, may_save, pool=None): + self.maybe_print_realm(realm) + username = self.ui.prompt('Username: ', default='') + return (username, bool(may_save)) + + def simple(self, realm, default_username, may_save, pool=None): + self.maybe_print_realm(realm) + if default_username: + username = default_username + else: + username = self.ui.prompt('Username: ', default='') + password = self.ui.getpass('Password for \'%s\': ' % (username,), default='') + return (username, password, bool(may_save)) + + def ssl_client_cert(self, realm, may_save, pool=None): + self.maybe_print_realm(realm) + cert_file = self.ui.prompt('Client certificate filename: ', default='') + return (cert_file, bool(may_save)) + + def ssl_client_cert_pw(self, realm, may_save, pool=None): + password = self.ui.getpass('Passphrase for \'%s\': ' % (realm,), default='') + return (password, bool(may_save)) + + def insecure(fn): + def fun(self, *args, **kwargs): + failures = args[1] + cert_info = args[2] + # cert_info[0] is hostname + # cert_info[1] is fingerprint + + fingerprint = self.ui.config('hostfingerprints', cert_info[0]) + if fingerprint and fingerprint.lower() == cert_info[1].lower(): + # same as the acceptance temporarily + return (failures, False) + + cacerts = self.ui.config('web', 'cacerts') + if not cacerts: + # same as the acceptance temporarily + return (failures, False) + + return fn(self, *args, **kwargs) + return fun + + @insecure + def ssl_server_trust(self, realm, failures, cert_info, may_save, pool=None): + msg = 'Error validating server certificate for \'%s\':\n' % (realm,) + if failures & svnwrap.SSL_UNKNOWNCA: + msg += ( + ' - The certificate is not issued by a trusted authority. Use the\n' + ' fingerprint to validate the certificate manually!\n' + ) + if failures & svnwrap.SSL_CNMISMATCH: + msg += ' - The certificate hostname does not match.\n' + if failures & svnwrap.SSL_NOTYETVALID: + msg += ' - The certificate is not yet valid.\n' + if failures & svnwrap.SSL_EXPIRED: + msg += ' - The certificate has expired.\n' + if failures & svnwrap.SSL_OTHER: + msg += ' - The certificate has an unknown error.\n' + msg += ( + 'Certificate information:\n' + '- Hostname: %s\n' + '- Valid: from %s until %s\n' + '- Issuer: %s\n' + '- Fingerprint: %s\n' + ) % ( + cert_info[0], # hostname + cert_info[2], # valid_from + cert_info[3], # valid_until + cert_info[4], # issuer_dname + cert_info[1], # fingerprint + ) + if may_save: + msg += '(R)eject, accept (t)emporarily or accept (p)ermanently? ' + choices = (('&Reject'), ('&Temporarily'), ('&Permanently')) + else: + msg += '(R)eject or accept (t)emporarily? ' + choices = (('&Reject'), ('&Temporarily')) + choice = self.ui.promptchoice(msg, choices, default=0) + if choice == 1: + creds = (failures, False) + elif may_save and choice == 2: + creds = (failures, True) + else: + creds = None + return creds + diff -Nru hgsubversion-1.4/hgsubversion/svnwrap/common.py hgsubversion-1.5/hgsubversion/svnwrap/common.py --- hgsubversion-1.4/hgsubversion/svnwrap/common.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svnwrap/common.py 2012-10-29 11:16:19.000000000 +0000 @@ -8,6 +8,9 @@ import urlparse import urllib import collections +import fnmatch +import ConfigParser +import sys class SubversionRepoCanNotReplay(Exception): """Exception raised when the svn server is too old to have replay. @@ -78,3 +81,91 @@ def __str__(self): return 'r%d by %s' % (self.revnum, self.author) + + +_svn_config_dir = None + + +class AutoPropsConfig(object): + """Provides the subversion auto-props functionality + when pushing new files. + """ + def __init__(self, config_dir=None): + config_file = config_file_path(config_dir) + self.config = ConfigParser.RawConfigParser() + self.config.read([config_file]) + + def properties(self, file): + """Returns a dictionary of the auto-props applicable for file. + Takes enable-auto-props into account. + """ + properties = {} + if self.autoprops_enabled(): + for pattern,prop_list in self.config.items('auto-props'): + if fnmatch.fnmatchcase(os.path.basename(file), pattern): + properties.update(parse_autoprops(prop_list)) + return properties + + def autoprops_enabled(self): + return (self.config.has_option('miscellany', 'enable-auto-props') + and self.config.getboolean( 'miscellany', 'enable-auto-props') + and self.config.has_section('auto-props')) + + +def config_file_path(config_dir): + if config_dir == None: + global _svn_config_dir + config_dir = _svn_config_dir + if config_dir == None: + if sys.platform == 'win32': + config_dir = os.path.join(os.environ['APPDATA'], 'Subversion') + else: + config_dir = os.path.join(os.environ['HOME'], '.subversion') + return os.path.join(config_dir, 'config') + + +def parse_autoprops(prop_list): + """Parses a string of autoprops and returns a dictionary of + the results. + Emulates the parsing of core.auto_props_enumerator. + """ + def unquote(s): + if len(s)>1 and s[0] in ['"', "'"] and s[0]==s[-1]: + return s[1:-1] + return s + + properties = {} + for prop in prop_list.split(';'): + if '=' in prop: + prop, value = prop.split('=',1) + value = unquote(value.strip()) + else: + value = '' + properties[prop.strip()] = value + return properties + +class SimpleStringIO(object): + """SimpleStringIO can replace a StringIO in write mode. + + cStringIO reallocates and doubles the size of its internal buffer + when it needs to append new data which requires two large blocks for + large inputs. SimpleStringIO stores each individual blocks and joins + them once done. This might cause more memory fragmentation but + requires only one large block. In practice, ra.get_file() seems to + write in 16kB blocks (svn 1.7.5) which should be friendly to memory + allocators. + """ + def __init__(self, closing=True): + self._blocks = [] + self._closing = closing + + def write(self, s): + self._blocks.append(s) + + def getvalue(self): + return ''.join(self._blocks) + + def close(self): + if self._closing: + del self._blocks + diff -Nru hgsubversion-1.4/hgsubversion/svnwrap/subvertpy_wrapper.py hgsubversion-1.5/hgsubversion/svnwrap/subvertpy_wrapper.py --- hgsubversion-1.4/hgsubversion/svnwrap/subvertpy_wrapper.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svnwrap/subvertpy_wrapper.py 2012-10-29 11:16:19.000000000 +0000 @@ -1,5 +1,4 @@ import cStringIO -import getpass import errno import os import shutil @@ -59,6 +58,11 @@ ERR_INCOMPLETE_DATA = subvertpy.ERR_INCOMPLETE_DATA ERR_RA_DAV_PATH_NOT_FOUND = subvertpy.ERR_RA_DAV_PATH_NOT_FOUND ERR_RA_DAV_REQUEST_FAILED = subvertpy.ERR_RA_DAV_REQUEST_FAILED +SSL_UNKNOWNCA = subvertpy.SSL_UNKNOWNCA +SSL_CNMISMATCH = subvertpy.SSL_CNMISMATCH +SSL_NOTYETVALID = subvertpy.SSL_NOTYETVALID +SSL_EXPIRED = subvertpy.SSL_EXPIRED +SSL_OTHER = subvertpy.SSL_OTHER SubversionException = subvertpy.SubversionException apply_txdelta = delta.apply_txdelta_handler # superclass for editor.HgEditor @@ -73,6 +77,11 @@ return fn +_prompt = None +def prompt_callback(callback): + global _prompt + _prompt = callback + _svntypes = { subvertpy.NODE_DIR: 'd', subvertpy.NODE_FILE: 'f', @@ -87,38 +96,18 @@ self.copyfrom_path = intern(self.copyfrom_path) class AbstractEditor(object): - __slots__ = ('editor',) + __slots__ = ('editor', 'baton') - def __init__(self, editor): + def __init__(self, editor, baton=None): self.editor = editor + self.baton = baton def set_target_revision(self, rev): pass def open_root(self, base_revnum): - return self.open_directory('', base_revnum) - - def open_directory(self, path, base_revnum): - self.editor.open_directory(path, None, base_revnum) - return DirectoryEditor(self.editor, path) - - def open_file(self, path, base_revnum): - self.editor.open_file(path, None, base_revnum) - return FileEditor(self.editor, path) - - def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1): - self.editor.add_directory(path, None, copyfrom_path, copyfrom_rev) - return DirectoryEditor(self.editor, path) - - def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1): - self.editor.add_file(path, None, copyfrom_path, copyfrom_rev) - return FileEditor(self.editor, path) - - def apply_textdelta(self, base_checksum): - return self.editor.apply_textdelta(self, None, base_checksum) - - def change_prop(self, name, value): - raise NotImplementedError() + baton = self.editor.open_root(None, base_revnum) + return DirectoryEditor(self.editor, baton) def abort(self): # TODO: should we do something special here? @@ -127,37 +116,51 @@ def close(self): del self.editor - def delete_entry(self, path, revnum): - self.editor.delete_entry(path, revnum, None) - class FileEditor(AbstractEditor): - __slots__ = ('path',) - - def __init__(self, editor, path): - super(FileEditor, self).__init__(editor) - self.path = path + def __init__(self, editor, baton): + super(FileEditor, self).__init__(editor, baton) def change_prop(self, name, value): - self.editor.change_file_prop(self.path, name, value, pool=None) + self.editor.change_file_prop(self.baton, name, value, pool=None) + + def apply_textdelta(self, base_checksum): + return self.editor.apply_textdelta(self.baton, base_checksum) def close(self, checksum=None): + self.editor.close_file(self.baton, checksum) super(FileEditor, self).close() - del self.path class DirectoryEditor(AbstractEditor): - __slots__ = ('path',) + def __init__(self, editor, baton): + super(DirectoryEditor, self).__init__(editor, baton) - def __init__(self, editor, path): - super(DirectoryEditor, self).__init__(editor) - self.path = path + def delete_entry(self, path, revnum): + self.editor.delete_entry(path, revnum, self.baton) + + def open_directory(self, path, base_revnum): + baton = self.editor.open_directory(path, self.baton, base_revnum) + return DirectoryEditor(self.editor, baton) + + def add_directory(self, path, copyfrom_path=None, copyfrom_rev=-1): + baton = self.editor.add_directory( + path, self.baton, copyfrom_path, copyfrom_rev) + return DirectoryEditor(self.editor, baton) + + def open_file(self, path, base_revnum): + baton = self.editor.open_file(path, self.baton, base_revnum) + return FileEditor(self.editor, baton) + + def add_file(self, path, copyfrom_path=None, copyfrom_rev=-1): + baton = self.editor.add_file( + path, self.baton, copyfrom_path, copyfrom_rev) + return FileEditor(self.editor, baton) def change_prop(self, name, value): - self.editor.change_dir_prop(self.path, name, value, pool=None) + self.editor.change_dir_prop(self.baton, name, value, pool=None) def close(self): - self.editor.close_directory(self.path) + self.editor.close_directory(self.baton) super(DirectoryEditor, self).close() - del self.path class SubversionRepo(object): """Wrapper for a Subversion repository. @@ -191,6 +194,7 @@ # expects unquoted paths self.subdir = urllib.unquote(self.subdir) self.hasdiff3 = True + self.autoprops_config = common.AutoPropsConfig() def init_ra_and_client(self): """ @@ -202,11 +206,28 @@ """ def getclientstring(): return 'hgsubversion' - # TODO: handle certificate authentication, Mercurial style - def getpass(realm, username, may_save): - return self.username or username, self.password or '', False - def getuser(realm, may_save): - return self.username or '', False + + def simple(realm, username, may_save): + return _prompt.simple(realm, username, may_save) + + def username(realm, may_save): + return _prompt.username(realm, may_save) + + def ssl_client_cert(realm, may_save): + return _prompt.ssl_client_cert(realm, may_save) + + def ssl_client_cert_pw(realm, may_save): + return _prompt.ssl_client_cert_pw(realm, may_save) + + def ssl_server_trust(realm, failures, cert_info, may_save): + creds = _prompt.ssl_server_trust(realm, failures, cert_info, may_save) + if creds is None: + # We need to reject the certificate, but subvertpy doesn't + # handle None as a return value here, and requires + # we instead return a tuple of (int, bool). Because of that, + # we return (0, False) instead. + creds = (0, False) + return creds providers = ra.get_platform_specific_client_providers() providers += [ @@ -215,9 +236,15 @@ ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), - ra.get_username_prompt_provider(getuser, 0), - ra.get_simple_prompt_provider(getpass, 0), ] + if _prompt: + providers += [ + ra.get_simple_prompt_provider(simple, 2), + ra.get_username_prompt_provider(username, 2), + ra.get_ssl_client_cert_prompt_provider(ssl_client_cert, 2), + ra.get_ssl_client_cert_pw_prompt_provider(ssl_client_cert_pw, 2), + ra.get_ssl_server_trust_prompt_provider(ssl_server_trust), + ] auth = ra.Auth(providers) if self.username: @@ -225,9 +252,20 @@ if self.password: auth.set_parameter(subvertpy.AUTH_PARAM_DEFAULT_PASSWORD, self.password) - self.remote = ra.RemoteAccess(url=self.svn_url, - client_string_func=getclientstring, - auth=auth) + try: + self.remote = ra.RemoteAccess(url=self.svn_url, + client_string_func=getclientstring, + auth=auth) + except SubversionException, e: + # e.child contains a detailed error messages + msglist = [] + svn_exc = e + while svn_exc: + if svn_exc.args[0]: + msglist.append(svn_exc.args[0]) + svn_exc = svn_exc.child + msg = '\n'.join(msglist) + raise common.SubversionConnectionException(msg) self.client = client.Client() self.client.auth = auth @@ -411,10 +449,14 @@ return pathidx - rooteditor = commiteditor.open_root() - visitdir(rooteditor, '', paths, 0) - rooteditor.close() - commiteditor.close() + try: + rooteditor = commiteditor.open_root() + visitdir(rooteditor, '', paths, 0) + rooteditor.close() + commiteditor.close() + except: + commiteditor.abort() + raise def get_replay(self, revision, editor, oldestrev=0): @@ -467,7 +509,7 @@ """ mode = '' try: - out = cStringIO.StringIO() + out = common.SimpleStringIO() rev, info = self.remote.get_file(path, out, revision) data = out.getvalue() out.close() diff -Nru hgsubversion-1.4/hgsubversion/svnwrap/svn_swig_wrapper.py hgsubversion-1.5/hgsubversion/svnwrap/svn_swig_wrapper.py --- hgsubversion-1.4/hgsubversion/svnwrap/svn_swig_wrapper.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/svnwrap/svn_swig_wrapper.py 2012-10-29 11:16:19.000000000 +0000 @@ -1,5 +1,4 @@ import cStringIO -import getpass import errno import os import shutil @@ -43,6 +42,11 @@ ERR_FS_TXN_OUT_OF_DATE = core.SVN_ERR_FS_TXN_OUT_OF_DATE ERR_INCOMPLETE_DATA = core.SVN_ERR_INCOMPLETE_DATA ERR_RA_DAV_REQUEST_FAILED = core.SVN_ERR_RA_DAV_REQUEST_FAILED +SSL_UNKNOWNCA = core.SVN_AUTH_SSL_UNKNOWNCA +SSL_CNMISMATCH = core.SVN_AUTH_SSL_CNMISMATCH +SSL_NOTYETVALID = core.SVN_AUTH_SSL_NOTYETVALID +SSL_EXPIRED = core.SVN_AUTH_SSL_EXPIRED +SSL_OTHER = core.SVN_AUTH_SSL_OTHER SubversionException = core.SubversionException Editor = delta.Editor @@ -57,6 +61,7 @@ optrev.value.number = revnum return optrev +core.svn_config_ensure(None) svn_config = core.svn_config_get_config(None) class RaCallbacks(ra.Callbacks): @staticmethod @@ -88,19 +93,49 @@ raise return fun -def user_pass_prompt(realm, default_username, ms, pool): # pragma: no cover - # FIXME: should use getpass() and username() from mercurial.ui +_prompt = None +def prompt_callback(callback): + global _prompt + _prompt = callback + +def _simple(realm, default_username, ms, pool): + ret = _prompt.simple(realm, default_username, ms, pool) creds = core.svn_auth_cred_simple_t() - creds.may_save = ms - if default_username: - sys.stderr.write('Auth realm: %s\n' % (realm,)) - creds.username = default_username + (creds.username, creds.password, creds.may_save) = ret + return creds + +def _username(realm, ms, pool): + ret = _prompt.username(realm, ms, pool) + creds = core.svn_auth_cred_username_t() + (creds.username, creds.may_save) = ret + return creds + +def _ssl_client_cert(realm, may_save, pool): + ret = _prompt.ssl_client_cert(realm, may_save, pool) + creds = core.svn_auth_cred_ssl_client_cert_t() + (creds.cert_file, creds.may_save) = ret + return creds + +def _ssl_client_cert_pw(realm, may_save, pool): + ret = _prompt.ssl_client_cert_pw(realm, may_save, pool) + creds = core.svn_auth_cred_ssl_client_cert_pw_t() + (creds.password, creds.may_save) = ret + return creds + +def _ssl_server_trust(realm, failures, cert_info, may_save, pool): + cert = [ + cert_info.hostname, + cert_info.fingerprint, + cert_info.valid_from, + cert_info.valid_until, + cert_info.issuer_dname, + ] + ret = _prompt.ssl_server_trust(realm, failures, cert, may_save, pool) + if ret: + creds = core.svn_auth_cred_ssl_server_trust_t() + (creds.accepted_failures, creds.may_save) = ret else: - sys.stderr.write('Auth realm: %s\n' % (realm,)) - sys.stderr.write('Username: ') - sys.stderr.flush() - creds.username = sys.stdin.readline().strip() - creds.password = getpass.getpass('Password for %s: ' % creds.username) + creds = None return creds def _create_auth_baton(pool, password_stores): @@ -145,9 +180,17 @@ client.get_ssl_client_cert_file_provider(), client.get_ssl_client_cert_pw_file_provider(), client.get_ssl_server_trust_file_provider(), - client.get_simple_prompt_provider(user_pass_prompt, 2), ] + if _prompt: + providers += [ + client.get_simple_prompt_provider(_simple, 2), + client.get_username_prompt_provider(_username, 2), + client.get_ssl_client_cert_prompt_provider(_ssl_client_cert, 2), + client.get_ssl_client_cert_pw_prompt_provider(_ssl_client_cert_pw, 2), + client.get_ssl_server_trust_prompt_provider(_ssl_server_trust), + ] + return core.svn_auth_open(providers, pool) _svntypes = { @@ -165,7 +208,7 @@ # --username and --password override URL credentials self.username = parsed[0] self.password = parsed[1] - self.svn_url = parsed[2] + self.svn_url = core.svn_path_canonicalize(parsed[2]) self.auth_baton_pool = core.Pool() self.auth_baton = _create_auth_baton(self.auth_baton_pool, password_stores) # self.init_ra_and_client() assumes that a pool already exists @@ -184,6 +227,7 @@ # expects unquoted paths self.subdir = urllib.unquote(self.subdir) self.hasdiff3 = True + self.autoprops_config = common.AutoPropsConfig() def init_ra_and_client(self): """Initializes the RA and client layers, because sometimes getting @@ -210,19 +254,14 @@ self.ra = ra.open2(self.svn_url, callbacks, svn_config, self.pool) except SubversionException, e: - if e.apr_err == core.SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED: - msg = ('Subversion does not trust the SSL certificate for this ' - 'site; please try running \'svn ls %s\' first.' - % self.svn_url) - elif e.apr_err == core.SVN_ERR_RA_DAV_REQUEST_FAILED: - msg = ('Failed to open Subversion repository; please try ' - 'running \'svn ls %s\' for details.' % self.svn_url) - else: - msg = e.args[0] - for k, v in vars(core).iteritems(): - if k.startswith('SVN_ERR_') and v == e.apr_err: - msg = '%s (%s)' % (msg, k) - break + # e.child contains a detailed error messages + msglist = [] + svn_exc = e + while svn_exc: + if svn_exc.args[0]: + msglist.append(svn_exc.args[0]) + svn_exc = svn_exc.child + msg = '\n'.join(msglist) raise common.SubversionConnectionException(msg) @property @@ -394,9 +433,15 @@ # TODO pass md5(new_text) instead of None editor.close_file(baton, None, pool) - delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb, - self.pool) - editor.close_edit(edit_baton, self.pool) + try: + delta.path_driver(editor, edit_baton, base_revision, paths, driver_cb, + self.pool) + editor.close_edit(edit_baton, self.pool) + except: + # If anything went wrong on the preceding lines, we should + # abort the in-progress transaction. + editor.abort_edit(edit_baton, self.pool) + raise def get_replay(self, revision, editor, oldest_rev_i_have=0): # this method has a tendency to chew through RAM if you don't re-init @@ -415,6 +460,19 @@ else: raise + # if we're not pulling the whole repo, svn fails to report + # file properties for files merged from subtrees outside ours + if self.svn_url != self.root: + links, execs = editor.current.symlinks, editor.current.execfiles + l = len(self.subdir) - 1 + for f in editor.current.added: + sf = f[l:] + if links[f] or execs[f]: + continue + props = self.list_props(sf, revision) + links[f] = props.get('svn:special') == '*' + execs[f] = props.get('svn:executable') == '*' + def get_revision(self, revision, editor): ''' feed the contents of the given revision to the given editor ''' @@ -485,7 +543,7 @@ assert not path.startswith('/') mode = '' try: - out = cStringIO.StringIO() + out = common.SimpleStringIO() info = ra.get_file(self.ra, path, revision, out) data = out.getvalue() out.close() @@ -559,6 +617,9 @@ if not path or path == '.': return self.svn_url assert path[0] != '/', path - return '/'.join((self.svn_url, - urllib.quote(path).rstrip('/'), - )) + path = path.rstrip('/') + try: + # new in svn 1.7 + return core.svn_uri_canonicalize(self.svn_url + '/' + path) + except AttributeError: + return self.svn_url + '/' + urllib.quote(path) diff -Nru hgsubversion-1.4/hgsubversion/util.py hgsubversion-1.5/hgsubversion/util.py --- hgsubversion-1.4/hgsubversion/util.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/util.py 2012-10-29 11:16:19.000000000 +0000 @@ -1,6 +1,8 @@ +import errno import re import os import urllib +from collections import deque from mercurial import cmdutil from mercurial import error @@ -13,6 +15,8 @@ except ImportError: pass +import maps + ignoredfiles = set(['.hgtags', '.hgsvnexternals', '.hgsub', '.hgsubstate']) b_re = re.compile(r'^\+\+\+ b\/([^\n]*)', re.MULTILINE) @@ -208,6 +212,14 @@ encoding.encoding = new_encoding return old +def isancestor(ctx, ancestorctx): + """Return True if ancestorctx is equal or an ancestor of ctx.""" + if ctx == ancestorctx: + return True + for actx in ctx.ancestors(): + if actx == ancestorctx: + return True + return False def issamefile(parentctx, childctx, f): """Return True if f exists and is the same in childctx and parentctx""" @@ -231,11 +243,17 @@ # parentctx is not an ancestor of childctx, files are unrelated return False + +def getsvnrev(ctx, defval=None): + '''Extract SVN revision from commit metadata''' + return ctx.extra().get('convert_revision', defval) + + def _templatehelper(ctx, kw): ''' Helper function for displaying information about converted changesets. ''' - convertinfo = ctx.extra().get('convert_revision', '') + convertinfo = getsvnrev(ctx, '') if not convertinfo or not convertinfo.startswith('svn:'): return '' @@ -273,11 +291,17 @@ ''' args = revset.getargs(x, 0, 0, "fromsvn takes no arguments") - def matches(r): - convertinfo = repo[r].extra().get('convert_revision', '') - return convertinfo[:4] == 'svn:' - - return [r for r in subset if matches(r)] + rev = repo.changelog.rev + bin = node.bin + try: + svnrevs = set(rev(bin(l.split(' ', 2)[1])) + for l in maps.RevMap.readmapfile(repo, missingok=False)) + return filter(svnrevs.__contains__, subset) + except IOError, err: + if err.errno != errno.ENOENT: + raise + raise hgutil.Abort("svn metadata is missing - " + "run 'hg svn rebuildmeta' to reconstruct it") def revset_svnrev(repo, subset, x): '''``svnrev(number)`` @@ -288,19 +312,65 @@ rev = revset.getstring(args[0], "the argument to svnrev() must be a number") try: - rev = int(rev) + revnum = int(rev) except ValueError: raise error.ParseError("the argument to svnrev() must be a number") - def matches(r): - convertinfo = repo[r].extra().get('convert_revision', '') - if convertinfo[:4] != 'svn:': - return False - return int(convertinfo[40:].rsplit('@', 1)[-1]) == rev - - return [r for r in subset if matches(r)] + rev = rev + ' ' + revs = [] + try: + for l in maps.RevMap.readmapfile(repo, missingok=False): + if l.startswith(rev): + n = l.split(' ', 2)[1] + r = repo[node.bin(n)].rev() + if r in subset: + revs.append(r) + return revs + except IOError, err: + if err.errno != errno.ENOENT: + raise + raise hgutil.Abort("svn metadata is missing - " + "run 'hg svn rebuildmeta' to reconstruct it") revsets = { 'fromsvn': revset_fromsvn, 'svnrev': revset_svnrev, } + +def getfilestoresize(ui): + """Return the replay or stupid file memory store size in megabytes or -1""" + size = ui.configint('hgsubversion', 'filestoresize', 200) + if size >= 0: + size = size*(2**20) + else: + size = -1 + return size + +# Copy-paste from mercurial.util to avoid having to deal with backward +# compatibility, plus the cache size is configurable. +def lrucachefunc(func, size): + '''cache most recent results of function calls''' + cache = {} + order = deque() + if func.func_code.co_argcount == 1: + def f(arg): + if arg not in cache: + if len(cache) > size: + del cache[order.popleft()] + cache[arg] = func(arg) + else: + order.remove(arg) + order.append(arg) + return cache[arg] + else: + def f(*args): + if args not in cache: + if len(cache) > size: + del cache[order.popleft()] + cache[args] = func(*args) + else: + order.remove(args) + order.append(args) + return cache[args] + + return f diff -Nru hgsubversion-1.4/hgsubversion/verify.py hgsubversion-1.5/hgsubversion/verify.py --- hgsubversion-1.4/hgsubversion/verify.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/verify.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,196 @@ +import posixpath + +from mercurial import util as hgutil +from mercurial import error + +import svnwrap +import svnrepo +import util +import editor + +def verify(ui, repo, args=None, **opts): + '''verify current revision against Subversion repository + ''' + + if repo is None: + raise error.RepoError("There is no Mercurial repository" + " here (.hg not found)") + + ctx = repo[opts.get('rev', '.')] + if 'close' in ctx.extra(): + ui.write('cannot verify closed branch') + return 0 + convert_revision = ctx.extra().get('convert_revision') + if convert_revision is None or not convert_revision.startswith('svn:'): + raise hgutil.Abort('revision %s not from SVN' % ctx) + + if args: + url = repo.ui.expandpath(args[0]) + else: + url = repo.ui.expandpath('default') + + svn = svnrepo.svnremoterepo(ui, url).svn + meta = repo.svnmeta(svn.uuid, svn.subdir) + srev, branch, branchpath = meta.get_source_rev(ctx=ctx) + + branchpath = branchpath[len(svn.subdir.lstrip('/')):] + branchurl = ('%s/%s' % (url, branchpath)).strip('/') + + ui.write('verifying %s against %s@%i\n' % (ctx, branchurl, srev)) + + if opts.get('stupid', ui.configbool('hgsubversion', 'stupid')): + svnfiles = set() + result = 0 + + hgfiles = set(ctx) - util.ignoredfiles + + svndata = svn.list_files(branchpath, srev) + for i, (fn, type) in enumerate(svndata): + util.progress(ui, 'verify', i, total=len(hgfiles)) + + if type != 'f': + continue + svnfiles.add(fn) + fp = fn + if branchpath: + fp = branchpath + '/' + fn + data, mode = svn.get_file(posixpath.normpath(fp), srev) + try: + fctx = ctx[fn] + except error.LookupError: + result = 1 + continue + if not fctx.data() == data: + ui.write('difference in: %s\n' % fn) + result = 1 + if not fctx.flags() == mode: + ui.write('wrong flags for: %s\n' % fn) + result = 1 + + if hgfiles != svnfiles: + unexpected = hgfiles - svnfiles + for f in sorted(unexpected): + ui.write('unexpected file: %s\n' % f) + missing = svnfiles - hgfiles + for f in sorted(missing): + ui.write('missing file: %s\n' % f) + result = 1 + + util.progress(ui, 'verify', None, total=len(hgfiles)) + + else: + class VerifyEditor(svnwrap.Editor): + """editor that verifies a repository against the given context.""" + def __init__(self, ui, ctx): + self.ui = ui + self.ctx = ctx + self.unexpected = set(ctx) - util.ignoredfiles + self.missing = set() + self.failed = False + + self.total = len(self.unexpected) + self.seen = 0 + + def open_root(self, base_revnum, pool=None): + pass + + def add_directory(self, path, parent_baton, copyfrom_path, + copyfrom_revision, pool=None): + self.file = None + self.props = None + + def open_directory(self, path, parent_baton, base_revision, pool=None): + self.file = None + self.props = None + + def add_file(self, path, parent_baton=None, copyfrom_path=None, + copyfrom_revision=None, file_pool=None): + + if path in self.unexpected: + self.unexpected.remove(path) + self.file = path + self.props = {} + else: + self.total += 1 + self.missing.add(path) + self.failed = True + self.file = None + self.props = None + + self.seen += 1 + util.progress(self.ui, 'verify', self.seen, total=self.total) + + def open_file(self, path, base_revnum): + raise NotImplementedError() + + def apply_textdelta(self, file_baton, base_checksum, pool=None): + stream = svnwrap.SimpleStringIO(closing=False) + handler = svnwrap.apply_txdelta('', stream) + if not callable(handler): + raise hgutil.Abort('Error in Subversion bindings: ' + 'cannot call handler!') + def txdelt_window(window): + handler(window) + # window being None means we're done + if window: + return + + fctx = self.ctx[self.file] + hgdata = fctx.data() + svndata = stream.getvalue() + + if 'svn:executable' in self.props: + if fctx.flags() != 'x': + self.ui.warn('wrong flags for: %s\n' % self.file) + self.failed = True + elif 'svn:special' in self.props: + hgdata = 'link ' + hgdata + if fctx.flags() != 'l': + self.ui.warn('wrong flags for: %s\n' % self.file) + self.failed = True + elif fctx.flags(): + self.ui.warn('wrong flags for: %s\n' % self.file) + self.failed = True + + if hgdata != svndata: + self.ui.warn('difference in: %s\n' % self.file) + self.failed = True + + if self.file is not None: + return txdelt_window + + def change_dir_prop(self, dir_baton, name, value, pool=None): + pass + + def change_file_prop(self, file_baton, name, value, pool=None): + if self.props is not None: + self.props[name] = value + + def close_file(self, file_baton, checksum, pool=None): + pass + + def close_directory(self, dir_baton, pool=None): + pass + + def delete_entry(self, path, revnum, pool=None): + raise NotImplementedError() + + def check(self): + util.progress(self.ui, 'verify', None, total=self.total) + + for f in self.unexpected: + self.ui.warn('unexpected file: %s\n' % f) + self.failed = True + for f in self.missing: + self.ui.warn('missing file: %s\n' % f) + self.failed = True + return not self.failed + + v = VerifyEditor(ui, ctx) + svnrepo.svnremoterepo(ui, branchurl).svn.get_revision(srev, v) + if v.check(): + result = 0 + else: + result = 1 + + return result diff -Nru hgsubversion-1.4/hgsubversion/wrappers.py hgsubversion-1.5/hgsubversion/wrappers.py --- hgsubversion-1.4/hgsubversion/wrappers.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/hgsubversion/wrappers.py 2012-10-29 11:16:19.000000000 +0000 @@ -12,6 +12,7 @@ from mercurial import node from mercurial import i18n from mercurial import extensions +from mercurial import repair import replay import pushmod @@ -65,13 +66,32 @@ return 0 +def getpeer(ui, opts, source): + # Since 2.3 (1ac628cd7113) + peer = getattr(hg, 'peer', None) + if peer: + return peer(ui, opts, source) + return hg.repository(ui, source) + +def getlocalpeer(ui, opts, source): + peer = getpeer(ui, opts, source) + repo = getattr(peer, 'local', lambda: peer)() + if isinstance(repo, bool): + repo = peer + return repo + +def getcaps(other): + return (getattr(other, 'caps', None) or + getattr(other, 'capabilities', None) or set()) + + def incoming(orig, ui, repo, origsource='default', **opts): """show incoming revisions from Subversion """ source, revs, checkout = util.parseurl(ui.expandpath(origsource)) - other = hg.repository(ui, source) - if 'subversion' not in other.capabilities: + other = getpeer(ui, opts, source) + if 'subversion' not in getcaps(other): return orig(ui, repo, origsource, **opts) svn = other.svn @@ -159,95 +179,121 @@ checkpush(force, revs) ui = repo.ui old_encoding = util.swap_out_encoding() - # TODO: implement --rev/#rev support - # TODO: do credentials specified in the URL still work? - svnurl = repo.ui.expandpath(dest.svnurl) - svn = dest.svn - meta = repo.svnmeta(svn.uuid, svn.subdir) + try: + # TODO: implement --rev/#rev support + # TODO: do credentials specified in the URL still work? + svn = dest.svn + meta = repo.svnmeta(svn.uuid, svn.subdir) - # Strategy: - # 1. Find all outgoing commits from this head - if len(repo.parents()) != 1: - ui.status('Cowardly refusing to push branch merge\n') - return 0 # results in nonzero exit status, see hg's commands.py - workingrev = repo.parents()[0] - ui.status('searching for changes\n') - hashes = meta.revmap.hashes() - outgoing = util.outgoing_revisions(repo, hashes, workingrev.node()) - if not (outgoing and len(outgoing)): - ui.status('no changes found\n') - return 1 # so we get a sane exit status, see hg's commands.push - while outgoing: - - # 2. Commit oldest revision that needs to be pushed - oldest = outgoing.pop(-1) - old_ctx = repo[oldest] - old_pars = old_ctx.parents() - if len(old_pars) != 1: - ui.status('Found a branch merge, this needs discussion and ' - 'implementation.\n') + # Strategy: + # 1. Find all outgoing commits from this head + if len(repo.parents()) != 1: + ui.status('Cowardly refusing to push branch merge\n') return 0 # results in nonzero exit status, see hg's commands.py - # We will commit to svn against this node's parent rev. Any file-level - # conflicts here will result in an error reported by svn. - base_ctx = old_pars[0] - base_revision = hashes[base_ctx.node()][0] - svnbranch = base_ctx.branch() - # Find most recent svn commit we have on this branch. - # This node will become the nearest known ancestor of the pushed rev. - oldtipctx = base_ctx - old_children = oldtipctx.descendants() - seen = set(c.node() for c in old_children) - samebranchchildren = [c for c in old_children if c.branch() == svnbranch - and c.node() in hashes] - if samebranchchildren: - # The following relies on descendants being sorted by rev. - oldtipctx = samebranchchildren[-1] - # All set, so commit now. - try: - pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn) - except pushmod.NoFilesException: - ui.warn("Could not push revision %s because it had no changes in svn.\n" % - old_ctx) - return 1 - - # 3. Fetch revisions from svn - # TODO: this probably should pass in the source explicitly - rev too? - r = repo.pull(dest, force=force) - assert not r or r == 0 - - # 4. Find the new head of the target branch - # We expect to get our own new commit back, but we might also get other - # commits that happened since our last pull, or even right after our own - # commit (race). - for c in oldtipctx.descendants(): - if c.node() not in seen and c.branch() == svnbranch: - newtipctx = c - - # 5. Rebase all children of the currently-pushing rev to the new head - heads = repo.heads(old_ctx.node()) - for needs_transplant in heads: + workingrev = repo.parents()[0] + ui.status('searching for changes\n') + hashes = meta.revmap.hashes() + outgoing = util.outgoing_revisions(repo, hashes, workingrev.node()) + to_strip=[] + if not (outgoing and len(outgoing)): + ui.status('no changes found\n') + return 1 # so we get a sane exit status, see hg's commands.push + while outgoing: + + # 2. Commit oldest revision that needs to be pushed + oldest = outgoing.pop(-1) + old_ctx = repo[oldest] + old_pars = old_ctx.parents() + if len(old_pars) != 1: + ui.status('Found a branch merge, this needs discussion and ' + 'implementation.\n') + # results in nonzero exit status, see hg's commands.py + return 0 + # We will commit to svn against this node's parent rev. Any + # file-level conflicts here will result in an error reported + # by svn. + base_ctx = old_pars[0] + base_revision = hashes[base_ctx.node()][0] + svnbranch = base_ctx.branch() + # Find most recent svn commit we have on this branch. This + # node will become the nearest known ancestor of the pushed + # rev. + oldtipctx = base_ctx + old_children = oldtipctx.descendants() + seen = set(c.node() for c in old_children) + samebranchchildren = [c for c in old_children + if c.branch() == svnbranch and c.node() in hashes] + if samebranchchildren: + # The following relies on descendants being sorted by rev. + oldtipctx = samebranchchildren[-1] + # All set, so commit now. + try: + pushmod.commit(ui, repo, old_ctx, meta, base_revision, svn) + except pushmod.NoFilesException: + ui.warn("Could not push revision %s because it had no changes " + "in svn.\n" % old_ctx) + return 1 + + # 3. Fetch revisions from svn + # TODO: this probably should pass in the source explicitly - + # rev too? + r = repo.pull(dest, force=force) + assert not r or r == 0 + + # 4. Find the new head of the target branch + # We expect to get our own new commit back, but we might + # also get other commits that happened since our last pull, + # or even right after our own commit (race). + for c in oldtipctx.descendants(): + if c.node() not in seen and c.branch() == svnbranch: + newtipctx = c + + # 5. Rebase all children of the currently-pushing rev to the + # new head + # + # there may be commits descended from the one we just + # pushed to svn that we aren't going to push to svn in + # this operation + oldhex = node.hex(old_ctx.node()) + needs_rebase_set = "%s:: and not(%s)" % (oldhex, oldhex) def extrafn(ctx, extra): - 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) - # Reload the repo after the rebase. Do not reuse contexts across this. + + util.swap_out_encoding(old_encoding) + try: + hgrebase.rebase(ui, repo, dest=node.hex(newtipctx.node()), + rev=[needs_rebase_set], + extrafn=extrafn, + # We actually want to strip one more rev than + # we're rebasing + keep=True) + finally: + util.swap_out_encoding() + + to_strip.append(old_ctx.node()) + # don't trust the pre-rebase repo. Do not reuse + # contexts across this. newtip = newtipctx.node() - repo = hg.repository(ui, meta.path) + repo = getlocalpeer(ui, {}, meta.path) newtipctx = repo[newtip] - # Rewrite the node ids in outgoing to their rebased versions. + rebasemap = dict() for child in newtipctx.descendants(): rebasesrc = child.extra().get('rebase_source') if rebasesrc: rebasemap[node.bin(rebasesrc)] = child.node() outgoing = [rebasemap.get(n) or n for n in outgoing] - # TODO: stop constantly creating the SVNMeta instances. - meta = repo.svnmeta(svn.uuid, svn.subdir) - hashes = meta.revmap.hashes() - util.swap_out_encoding(old_encoding) + + meta = repo.svnmeta(svn.uuid, svn.subdir) + hashes = meta.revmap.hashes() + util.swap_out_encoding(old_encoding) + try: + hg.update(repo, repo['tip'].node()) + finally: + util.swap_out_encoding() + repair.strip(ui, repo, to_strip, "all") + finally: + util.swap_out_encoding(old_encoding) return 1 # so we get a sane exit status, see hg's commands.push @@ -259,75 +305,90 @@ # Split off #rev svn_url, heads, checkout = util.parseurl(svn_url, heads) old_encoding = util.swap_out_encoding() - + total = None try: - stopat_rev = int(checkout or 0) - except ValueError: - raise hgutil.Abort('unrecognised Subversion revision %s: ' - 'only numbers work.' % checkout) - - have_replay = not repo.ui.configbool('hgsubversion', 'stupid') - if not have_replay: - repo.ui.note('fetching stupidly...\n') - - svn = source.svn - meta = repo.svnmeta(svn.uuid, svn.subdir) - - layout = repo.ui.config('hgsubversion', 'layout', 'auto') - if layout == 'auto': - rootlist = svn.list_dir('', revision=(stopat_rev or None)) - if sum(map(lambda x: x in rootlist, ('branches', 'tags', 'trunk'))): - layout = 'standard' - else: - layout = 'single' - repo.ui.setconfig('hgsubversion', 'layout', layout) - repo.ui.note('using %s layout\n' % layout) + try: + stopat_rev = int(checkout or 0) + except ValueError: + raise hgutil.Abort('unrecognised Subversion revision %s: ' + 'only numbers work.' % checkout) + + have_replay = not repo.ui.configbool('hgsubversion', 'stupid') + if not have_replay: + repo.ui.note('fetching stupidly...\n') - branch = repo.ui.config('hgsubversion', 'branch') - if branch: - if layout != 'single': - msg = ('branch cannot be specified for Subversion clones using ' - 'standard directory layout') - raise hgutil.Abort(msg) + svn = source.svn + meta = repo.svnmeta(svn.uuid, svn.subdir) - meta.branchmap['default'] = branch + layout = repo.ui.config('hgsubversion', 'layout', 'auto') + if layout == 'auto': + try: + rootlist = svn.list_dir('', revision=(stopat_rev or None)) + except svnwrap.SubversionException, e: + err = "%s (subversion error: %d)" % (e.args[0], e.args[1]) + raise hgutil.Abort(err) + if sum(map(lambda x: x in rootlist, ('branches', 'tags', 'trunk'))): + layout = 'standard' + else: + layout = 'single' + repo.ui.setconfig('hgsubversion', 'layout', layout) + repo.ui.note('using %s layout\n' % layout) + + branch = repo.ui.config('hgsubversion', 'branch') + if branch: + if layout != 'single': + msg = ('branch cannot be specified for Subversion clones using ' + 'standard directory layout') + raise hgutil.Abort(msg) + + meta.branchmap['default'] = branch + + ui = repo.ui + start = meta.revmap.youngest + origrevcount = len(meta.revmap) + + if start <= 0: + # we are initializing a new repository + start = repo.ui.config('hgsubversion', 'startrev', 0) + if isinstance(start, str) and start.upper() == 'HEAD': + start = svn.last_changed_rev + else: + start = int(start) - ui = repo.ui - start = meta.revmap.youngest - origrevcount = len(meta.revmap) + if start > 0: + if layout == 'standard': + raise hgutil.Abort('non-zero start revisions are only ' + 'supported for single-directory clones.') + ui.note('starting at revision %d; any prior will be ignored\n' + % start) + # fetch all revisions *including* the one specified... + start -= 1 + + # anything less than zero makes no sense + if start < 0: + start = 0 - if start <= 0: - # we are initializing a new repository - start = repo.ui.config('hgsubversion', 'startrev', 0) - if isinstance(start, str) and start.upper() == 'HEAD': - start = svn.last_changed_rev + skiprevs = repo.ui.configlist('hgsubversion', 'unsafeskip', '') + try: + skiprevs = set(map(int, skiprevs)) + except ValueError: + raise hgutil.Abort('unrecognised Subversion revisions %r: ' + 'only numbers work.' % checkout) + + oldrevisions = len(meta.revmap) + if stopat_rev: + total = stopat_rev - start else: - start = int(start) + total = svn.HEAD - start + lastpulled = None - if start > 0: - if layout == 'standard': - raise hgutil.Abort('non-zero start revisions are only ' - 'supported for single-directory clones.') - ui.note('starting at revision %d; any prior will be ignored\n' - % start) - # fetch all revisions *including* the one specified... - start -= 1 - - # anything less than zero makes no sense - if start < 0: - start = 0 - - oldrevisions = len(meta.revmap) - if stopat_rev: - total = stopat_rev - start - else: - total = svn.HEAD - start - lastpulled = None - try: try: # start converting revisions firstrun = True for r in svn.revisions(start=start, stop=stopat_rev): + if r.revnum in skiprevs: + ui.status('[r%d SKIPPED]\n' % r.revnum) + continue lastpulled = r.revnum if (r.author is None and r.message == 'This is an empty revision for padding.'): @@ -380,9 +441,10 @@ ui.traceback() raise hgutil.Abort(*e.args) except KeyboardInterrupt: - pass + ui.traceback() finally: - util.progress(ui, 'pull', None, total=total) + if total is not None: + util.progress(ui, 'pull', None, total=total) util.swap_out_encoding(old_encoding) if lastpulled is not None: @@ -476,7 +538,7 @@ if isinstance(origsource, str): source, branch, checkout = util.parseurl(ui.expandpath(origsource), opts.get('branch')) - srcrepo = hg.repository(ui, source) + srcrepo = getpeer(ui, opts, source) else: srcrepo = origsource @@ -508,7 +570,12 @@ srcrepo = data.get('srcrepo') if dstrepo.local() and srcrepo.capable('subversion'): - fd = dstrepo.opener("hgrc", "a", text=True) + dst = dstrepo.local() + if isinstance(dst, bool): + # Apparently <= hg@1.9 + fd = dstrepo.opener("hgrc", "a", text=True) + else: + fd = dst.opener("hgrc", "a", text=True) for section in set(s for s, v in optionmap.itervalues()): config = dict(ui.configitems(section)) for name in dontretain[section]: diff -Nru hgsubversion-1.4/setup.py hgsubversion-1.5/setup.py --- hgsubversion-1.4/setup.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/setup.py 2012-10-29 11:16:19.000000000 +0000 @@ -118,7 +118,7 @@ long_description=open(os.path.join(os.path.dirname(__file__), 'README')).read(), keywords='mercurial', - packages=('hgsubversion', 'hgsubversion.svnwrap'), + packages=('hgsubversion', 'hgsubversion.hooks', 'hgsubversion.svnwrap'), package_data={ 'hgsubversion': ['help/subversion.rst'] }, platforms='any', install_requires=requires, diff -Nru hgsubversion-1.4/tests/comprehensive/test_stupid_pull.py hgsubversion-1.5/tests/comprehensive/test_stupid_pull.py --- hgsubversion-1.4/tests/comprehensive/test_stupid_pull.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/comprehensive/test_stupid_pull.py 2012-10-29 11:16:19.000000000 +0000 @@ -45,7 +45,9 @@ attrs = {'_do_case': _do_case, } for case in (f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')): - name = 'test_' + case[:-len('.svndump')] + if case == 'corrupt.svndump': + continue + name = 'test_' + case[:-len('.svndump')].replace('-', '_') # Automatic layout branchtag collision exposes a minor defect # here, but since it isn't a regression we suppress the test case. if case != 'branchtagcollision.svndump': diff -Nru hgsubversion-1.4/tests/comprehensive/test_verify.py hgsubversion-1.5/tests/comprehensive/test_verify.py --- hgsubversion-1.4/tests/comprehensive/test_verify.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/comprehensive/test_verify.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,55 +0,0 @@ -import os -import pickle -import sys -import unittest - -# wrapped in a try/except because of weirdness in how -# run.py works as compared to nose. -try: - import test_util -except ImportError: - sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) - import test_util - -from mercurial import hg -from mercurial import ui - -from hgsubversion import svncommands - -def _do_case(self, name, stupid, layout): - subdir = test_util.subdir.get(name, '') - repo = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid, layout=layout) - assert len(self.repo) > 0 - for i in repo: - ctx = repo[i] - self.assertEqual(svncommands.verify(repo.ui, repo, rev=ctx.node()), 0) - -def buildmethod(case, name, stupid, layout): - m = lambda self: self._do_case(case, stupid, layout) - m.__name__ = name - bits = case, stupid and 'stupid' or 'real', layout - m.__doc__ = 'Test verify on %s with %s replay. (%s)' % bits - return m - -attrs = {'_do_case': _do_case} -fixtures = [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')] -for case in fixtures: - # this fixture results in an empty repository, don't use it - if case == 'project_root_not_repo_root.svndump': - continue - bname = 'test_' + case[:-len('.svndump')] - attrs[bname] = buildmethod(case, bname, False, 'standard') - name = bname + '_stupid' - attrs[name] = buildmethod(case, name, True, 'standard') - name = bname + '_single' - attrs[name] = buildmethod(case, name, False, 'single') - # Disabled because the "stupid and real are the same" tests - # verify this plus even more. - # name = bname + '_single_stupid' - # attrs[name] = buildmethod(case, name, True, 'single') - -VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs) - -def suite(): - all_tests = [unittest.TestLoader().loadTestsFromTestCase(VerifyTests)] - return unittest.TestSuite(all_tests) diff -Nru hgsubversion-1.4/tests/comprehensive/test_verify_and_startrev.py hgsubversion-1.5/tests/comprehensive/test_verify_and_startrev.py --- hgsubversion-1.4/tests/comprehensive/test_verify_and_startrev.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/comprehensive/test_verify_and_startrev.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,116 @@ +import os +import pickle +import sys +import unittest + +# wrapped in a try/except because of weirdness in how +# run.py works as compared to nose. +try: + import test_util +except ImportError: + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + import test_util + +from mercurial import hg +from mercurial import ui + +from hgsubversion import verify + +# these fixtures contain no files at HEAD and would result in empty clones +_skipshallow = set([ + 'binaryfiles.svndump', + 'binaryfiles-broken.svndump', + 'emptyrepo.svndump', + 'correct.svndump', + 'corrupt.svndump', +]) + +_skipall = set([ + 'project_root_not_repo_root.svndump', + 'movetotrunk.svndump', +]) + +_skipstandard = set([ + 'subdir_is_file_prefix.svndump', + 'correct.svndump', + 'corrupt.svndump', + 'emptyrepo2.svndump', +]) + +def _do_case(self, name, stupid, layout): + subdir = test_util.subdir.get(name, '') + repo, svnpath = self.load_and_fetch(name, subdir=subdir, stupid=stupid, + layout=layout) + assert len(self.repo) > 0 + for i in repo: + ctx = repo[i] + self.assertEqual(verify.verify(repo.ui, repo, rev=ctx.node(), + stupid=True), 0) + self.assertEqual(verify.verify(repo.ui, repo, rev=ctx.node(), + stupid=False), 0) + + # check a startrev clone + if layout == 'single' and name not in _skipshallow: + self.wc_path += '_shallow' + shallowrepo = self.fetch(svnpath, subdir=subdir, stupid=stupid, + layout='single', startrev='HEAD') + + self.assertEqual(len(shallowrepo), 1, + "shallow clone should have just one revision, not %d" + % len(shallowrepo)) + + fulltip = repo['tip'] + shallowtip = shallowrepo['tip'] + + repo.ui.pushbuffer() + self.assertEqual(0, verify.verify(repo.ui, shallowrepo, + rev=shallowtip.node(), + stupid=True)) + self.assertEqual(0, verify.verify(repo.ui, shallowrepo, + rev=shallowtip.node(), + stupid=False)) + + stupidui = ui.ui(repo.ui) + stupidui.config('hgsubversion', 'stupid', True) + self.assertEqual(verify.verify(stupidui, repo, rev=ctx.node(), + stupid=True), 0) + self.assertEqual(verify.verify(stupidui, repo, rev=ctx.node(), + stupid=False), 0) + + # viewing diff's of lists of files is easier on the eyes + self.assertMultiLineEqual('\n'.join(fulltip), '\n'.join(shallowtip), + repo.ui.popbuffer()) + + for f in fulltip: + self.assertMultiLineEqual(fulltip[f].data(), shallowtip[f].data()) + + +def buildmethod(case, name, stupid, layout): + m = lambda self: self._do_case(case, stupid, layout) + m.__name__ = name + bits = case, stupid and 'stupid' or 'real', layout + m.__doc__ = 'Test verify on %s with %s replay. (%s)' % bits + return m + +attrs = {'_do_case': _do_case} +fixtures = [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')] +for case in fixtures: + if case in _skipall: + continue + bname = 'test_' + case[:-len('.svndump')] + if case not in _skipstandard: + attrs[bname] = buildmethod(case, bname, False, 'standard') + name = bname + '_stupid' + attrs[name] = buildmethod(case, name, True, 'standard') + name = bname + '_single' + attrs[name] = buildmethod(case, name, False, 'single') + # Disabled because the "stupid and real are the same" tests + # verify this plus even more. + # name = bname + '_single_stupid' + # attrs[name] = buildmethod(case, name, True, 'single') + +VerifyTests = type('VerifyTests', (test_util.TestBase,), attrs) + +def suite(): + all_tests = [unittest.TestLoader().loadTestsFromTestCase(VerifyTests)] + return unittest.TestSuite(all_tests) diff -Nru hgsubversion-1.4/tests/fixtures/addspecial.sh hgsubversion-1.5/tests/fixtures/addspecial.sh --- hgsubversion-1.4/tests/fixtures/addspecial.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/addspecial.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,51 @@ +#!/bin/sh + +mkdir temp +cd temp + +svnadmin create repo +svn co file://`pwd`/repo wc +cd wc + +mkdir -p trunk branches +svn add trunk branches +svn ci -m'initial structure' +cd trunk +echo a>a +svn add a +svn ci -mci1 a +cd .. +svn up +svn cp trunk branches/foo +svn ci -m'branch foo' +cd branches/foo +ln -s a fnord +svn add fnord +svn ci -msymlink fnord +mkdir 'spacy name' +echo a > 'spacy name/spacy file' +svn add 'spacy name' +svn ci -mspacy 'spacy name' +svn up +echo b > 'spacy name/surprise ~' +svn add 'spacy name/surprise ~' +svn ci -mtilde 'spacy name' +svn up ../.. +echo foo > exe +chmod +x exe +svn add exe +svn ci -mexecutable exe +svn up ../.. +cd ../../trunk +svn merge ../branches/foo +svn ci -mmerge +svn up + +pwd +cd ../../.. +svnadmin dump temp/repo > addspecial.svndump +echo +echo 'Complete.' +echo 'You probably want to clean up temp now.' +echo 'Dump in addspecial.svndump' +exit 0 diff -Nru hgsubversion-1.4/tests/fixtures/addspecial.svndump hgsubversion-1.5/tests/fixtures/addspecial.svndump --- hgsubversion-1.4/tests/fixtures/addspecial.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/addspecial.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,302 @@ +SVN-fs-dump-format-version: 2 + +UUID: 01df53ad-5d72-4756-8742-f669dc98f791 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2012-05-13T22:22:43.218190Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 118 +Content-length: 118 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:44.112163Z +K 7 +svn:log +V 17 +initial structure +PROPS-END + +Node-path: branches +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 103 +Content-length: 103 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:45.111247Z +K 7 +svn:log +V 3 +ci1 +PROPS-END + +Node-path: trunk/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b +Content-length: 12 + +PROPS-END +a + + +Revision-number: 3 +Prop-content-length: 111 +Content-length: 111 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:48.110257Z +K 7 +svn:log +V 10 +branch foo +PROPS-END + +Node-path: branches/foo +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk + + +Revision-number: 4 +Prop-content-length: 107 +Content-length: 107 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:49.115096Z +K 7 +svn:log +V 7 +symlink +PROPS-END + +Node-path: branches/foo/fnord +Node-kind: file +Node-action: add +Prop-content-length: 33 +Text-content-length: 6 +Text-content-md5: c118dba188202a1efc975bef6064180b +Text-content-sha1: 41f94e4692313bf7f7c92aa600002f1dff93d6bf +Content-length: 39 + +K 11 +svn:special +V 1 +* +PROPS-END +link a + +Revision-number: 5 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:50.119266Z +K 7 +svn:log +V 5 +spacy +PROPS-END + +Node-path: branches/foo/spacy name +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: branches/foo/spacy name/spacy file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b +Content-length: 12 + +PROPS-END +a + + +Revision-number: 6 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:52.123367Z +K 7 +svn:log +V 5 +tilde +PROPS-END + +Node-path: branches/foo/spacy name/surprise ~ +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 3b5d5c3712955042212316173ccf37be +Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b +Content-length: 12 + +PROPS-END +b + + +Revision-number: 7 +Prop-content-length: 111 +Content-length: 111 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:54.129462Z +K 7 +svn:log +V 10 +executable +PROPS-END + +Node-path: branches/foo/exe +Node-kind: file +Node-action: add +Prop-content-length: 36 +Text-content-length: 4 +Text-content-md5: d3b07384d113edec49eaa6238ad5ff00 +Text-content-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 +Content-length: 40 + +K 14 +svn:executable +V 1 +* +PROPS-END +foo + + +Revision-number: 8 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-13T22:22:57.111370Z +K 7 +svn:log +V 5 +merge +PROPS-END + +Node-path: trunk +Node-kind: dir +Node-action: change +Prop-content-length: 52 +Content-length: 52 + +K 13 +svn:mergeinfo +V 17 +/branches/foo:3-7 +PROPS-END + + +Node-path: trunk/exe +Node-kind: file +Node-action: add +Node-copyfrom-rev: 7 +Node-copyfrom-path: branches/foo/exe +Text-copy-source-md5: d3b07384d113edec49eaa6238ad5ff00 +Text-copy-source-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 + + +Node-path: trunk/fnord +Node-kind: file +Node-action: add +Node-copyfrom-rev: 7 +Node-copyfrom-path: branches/foo/fnord +Text-copy-source-md5: c118dba188202a1efc975bef6064180b +Text-copy-source-sha1: 41f94e4692313bf7f7c92aa600002f1dff93d6bf + + +Node-path: trunk/spacy name +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 7 +Node-copyfrom-path: branches/foo/spacy name + + diff -Nru hgsubversion-1.4/tests/fixtures/copies.sh hgsubversion-1.5/tests/fixtures/copies.sh --- hgsubversion-1.4/tests/fixtures/copies.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/copies.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,29 @@ +#!/bin/sh +# +# Generate copies.svndump +# + +rm -rf temp +mkdir temp +cd temp +mkdir -p import/trunk/dir +echo a > import/trunk/dir/a + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn import import $svnurl -m init + +svn co $svnurl project +cd project +svn cp trunk/dir trunk/dir2 +echo b >> trunk/dir2/a +svn ci -m 'copy/edit trunk/dir/a' +svn up +svn cp trunk/dir2 trunk/dir3 +svn ci -m 'copy dir2 to dir3' +svn rm trunk/dir3/a +svn cp trunk/dir2/a trunk/dir3/a +svn ci -m 'copy and remove' +cd .. + +svnadmin dump testrepo > ../copies.svndump diff -Nru hgsubversion-1.4/tests/fixtures/copies.svndump hgsubversion-1.5/tests/fixtures/copies.svndump --- hgsubversion-1.4/tests/fixtures/copies.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/copies.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,158 @@ +SVN-fs-dump-format-version: 2 + +UUID: 6f377846-a035-4244-a154-e87a9351a653 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2012-10-15T19:02:56.936694Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-15T19:02:56.958201Z +K 7 +svn:log +V 4 +init +PROPS-END + +Node-path: trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/dir +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/dir/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b +Content-length: 12 + +PROPS-END +a + + +Revision-number: 2 +Prop-content-length: 123 +Content-length: 123 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-15T19:02:58.046478Z +K 7 +svn:log +V 21 +copy/edit trunk/dir/a +PROPS-END + +Node-path: trunk/dir2 +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 1 +Node-copyfrom-path: trunk/dir + + +Node-path: trunk/dir2/a +Node-kind: file +Node-action: change +Text-content-length: 4 +Text-content-md5: dd8c6a395b5dd36c56d23275028f526c +Text-content-sha1: 05dec960e24d918b8a73a1c53bcbbaac2ee5c2e0 +Content-length: 4 + +a +b + + +Revision-number: 3 +Prop-content-length: 119 +Content-length: 119 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-15T19:03:01.045897Z +K 7 +svn:log +V 17 +copy dir2 to dir3 +PROPS-END + +Node-path: trunk/dir3 +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk/dir2 + + +Revision-number: 4 +Prop-content-length: 117 +Content-length: 117 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-15T19:03:03.046654Z +K 7 +svn:log +V 15 +copy and remove +PROPS-END + +Node-path: trunk/dir3/a +Node-kind: file +Node-action: delete + +Node-path: trunk/dir3/a +Node-kind: file +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk/dir2/a +Text-copy-source-md5: dd8c6a395b5dd36c56d23275028f526c +Text-copy-source-sha1: 05dec960e24d918b8a73a1c53bcbbaac2ee5c2e0 + + + + diff -Nru hgsubversion-1.4/tests/fixtures/correct.svndump hgsubversion-1.5/tests/fixtures/correct.svndump --- hgsubversion-1.4/tests/fixtures/correct.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/correct.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,103 @@ +SVN-fs-dump-format-version: 2 + +UUID: 00000000-0000-0000-0000-000000000000 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2010-11-30T15:10:25.898546Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 100 +Content-length: 100 + +K 7 +svn:log +V 0 + +K 10 +svn:author +V 6 +danchr +K 8 +svn:date +V 27 +2010-11-30T15:16:01.077550Z +PROPS-END + +Node-path: empty-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 0 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709 +Content-length: 10 + +PROPS-END + + +Node-path: executable-file +Node-kind: file +Node-action: add +Prop-content-length: 36 +Text-content-length: 11 +Text-content-md5: 01839ba8c81c3b2c7486607e0c683e62 +Text-content-sha1: 5e70f8a25fe8ad4ad971bfd3388c258b019268d4 +Content-length: 47 + +K 14 +svn:executable +V 1 +* +PROPS-END +Executable + + +Node-path: regular-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 10 +Text-content-md5: 2e01b7f4ab0c18c05a3059eb2e2420d9 +Text-content-sha1: 6e530e985be313a43dc9734251656be8f0c94ab8 +Content-length: 20 + +PROPS-END +Contents. + + +Node-path: another-regular-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 10 +Text-content-md5: 2e01b7f4ab0c18c05a3059eb2e2420d9 +Text-content-sha1: 6e530e985be313a43dc9734251656be8f0c94ab8 +Content-length: 20 + +PROPS-END +Contents. + + +Node-path: symlink +Node-kind: file +Node-action: add +Prop-content-length: 33 +Text-content-length: 6 +Text-content-md5: 654580f41818cd6f51408c7cbd313728 +Text-content-sha1: 130b8faaf3e1acc1b95f77ac835e9c8b6eee5c96 +Content-length: 39 + +K 11 +svn:special +V 1 +* +PROPS-END +link A + diff -Nru hgsubversion-1.4/tests/fixtures/corrupt.svndump hgsubversion-1.5/tests/fixtures/corrupt.svndump --- hgsubversion-1.4/tests/fixtures/corrupt.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/corrupt.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,97 @@ +SVN-fs-dump-format-version: 2 + +UUID: 00000000-0000-0000-0000-000000000000 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2010-11-30T15:10:25.898546Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 100 +Content-length: 100 + +K 10 +svn:author +V 6 +danchr +K 8 +svn:date +V 27 +2010-11-30T15:16:01.077550Z +K 7 +svn:log +V 0 + +PROPS-END + +Node-path: another-regular-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 0 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709 +Content-length: 10 + +PROPS-END + + +Node-path: executable-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 11 +Text-content-md5: 01839ba8c81c3b2c7486607e0c683e62 +Text-content-sha1: 5e70f8a25fe8ad4ad971bfd3388c258b019268d4 +Content-length: 21 + +PROPS-END +Executable + + +Node-path: missing-file +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 0 +Text-content-md5: d41d8cd98f00b204e9800998ecf8427e +Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709 +Content-length: 10 + +PROPS-END + + +Node-path: regular-file +Node-kind: file +Node-action: add +Prop-content-length: 33 +Text-content-length: 18 +Text-content-md5: adf66a0cec83e25644c63f3c3007ae7c +Text-content-sha1: 047e6e482d0c9cb812f89d18a9f07a43caab76bb +Content-length: 51 + +K 11 +svn:special +V 1 +* +PROPS-END +link Bad contents. + +Node-path: symlink +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 1 +Text-content-md5: 7fc56270e7a70fa81a5935b72eacbe29 +Text-content-sha1: 6dcd4ce23d88e2ee9568ba546c007c63d9131c1b +Content-length: 11 + +PROPS-END +A + diff -Nru hgsubversion-1.4/tests/fixtures/delete_restore_trunk.sh hgsubversion-1.5/tests/fixtures/delete_restore_trunk.sh --- hgsubversion-1.4/tests/fixtures/delete_restore_trunk.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/delete_restore_trunk.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,31 @@ +#!/bin/bash +set -e +mkdir temp +cd temp +svnadmin create repo +svn co file://`pwd`/repo wc +cd wc +mkdir branches trunk tags +svn add * +svn ci -m 'btt' +echo foo > trunk/foo +svn add trunk/foo +svn ci -m 'add file' +svn up +svn rm trunk +svn ci -m 'delete trunk' +svn up +cd .. +svn cp -m 'restore trunk' file://`pwd`/repo/trunk@2 file://`pwd`/repo/trunk +cd wc +svn up +echo bar >> trunk/foo +svn ci -m 'append to file' +svn up +cd ../.. +svnadmin dump temp/repo > delete_restore_trunk.svndump +echo +echo 'Complete.' +echo 'You probably want to clean up temp now.' +echo 'Dump in branch_delete_parent_dir.svndump' +exit 0 diff -Nru hgsubversion-1.4/tests/fixtures/delete_restore_trunk.svndump hgsubversion-1.5/tests/fixtures/delete_restore_trunk.svndump --- hgsubversion-1.4/tests/fixtures/delete_restore_trunk.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/delete_restore_trunk.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,167 @@ +SVN-fs-dump-format-version: 2 + +UUID: fca176f4-a346-479b-ae2c-78c8442c3809 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2012-05-16T22:55:55.613464Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 103 +Content-length: 103 + +K 7 +svn:log +V 3 +btt +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-16T22:55:56.081065Z +PROPS-END + +Node-path: branches +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: tags +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 108 +Content-length: 108 + +K 7 +svn:log +V 8 +add file +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-16T22:55:57.071178Z +PROPS-END + +Node-path: trunk/foo +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 4 +Text-content-md5: d3b07384d113edec49eaa6238ad5ff00 +Text-content-sha1: f1d2d2f924e986ac86fdf7b36c94bcdf32beec15 +Content-length: 14 + +PROPS-END +foo + + +Revision-number: 3 +Prop-content-length: 113 +Content-length: 113 + +K 7 +svn:log +V 12 +delete trunk +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-16T22:55:59.058026Z +PROPS-END + +Node-path: trunk +Node-action: delete + + +Revision-number: 4 +Prop-content-length: 114 +Content-length: 114 + +K 7 +svn:log +V 13 +restore trunk +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-16T22:56:01.055887Z +PROPS-END + +Node-path: trunk +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk + + +Revision-number: 5 +Prop-content-length: 115 +Content-length: 115 + +K 7 +svn:log +V 14 +append to file +K 10 +svn:author +V 6 +bryano +K 8 +svn:date +V 27 +2012-05-16T22:56:02.060991Z +PROPS-END + +Node-path: trunk/foo +Node-kind: file +Node-action: change +Text-content-length: 8 +Text-content-md5: f47c75614087a8dd938ba4acff252494 +Text-content-sha1: 4e48e2c9a3d2ca8a708cb0cc545700544efb5021 +Content-length: 8 + +foo +bar + + diff -Nru hgsubversion-1.4/tests/fixtures/emptyrepo2.sh hgsubversion-1.5/tests/fixtures/emptyrepo2.sh --- hgsubversion-1.4/tests/fixtures/emptyrepo2.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/emptyrepo2.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,44 @@ +#!/bin/sh +# +# Create emptyrepo2.svndump +# +# The generated repository contains a sequence of empty revisions +# created with a combination of svnsync and filtering + +mkdir temp +cd temp + +mkdir project-orig +cd project-orig +mkdir -p sub/trunk other +echo a > other/a +cd .. + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn import project-orig $svnurl -m init + +svn co $svnurl project +cd project +echo a >> other/a +svn ci -m othera +echo a >> other/a +svn ci -m othera2 +echo b > sub/trunk/a +svn add sub/trunk/a +svn ci -m adda +cd .. + +svnadmin create testrepo2 +cat > testrepo2/hooks/pre-revprop-change < ../emptyrepo2.svndump + diff -Nru hgsubversion-1.4/tests/fixtures/emptyrepo2.svndump hgsubversion-1.5/tests/fixtures/emptyrepo2.svndump --- hgsubversion-1.4/tests/fixtures/emptyrepo2.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/emptyrepo2.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,129 @@ +SVN-fs-dump-format-version: 2 + +UUID: 293d1f29-635d-48b8-9cdf-468fd987067a + +Revision-number: 0 +Prop-content-length: 261 +Content-length: 261 + +K 8 +svn:date +V 27 +2012-10-03T18:58:42.535317Z +K 17 +svn:sync-from-url +V 74 +file:///Users/pmezard/dev/hg/hgsubversion/tests/fixtures/temp/testrepo/sub +K 18 +svn:sync-from-uuid +V 36 +241badf9-093f-4e71-8a58-1028abf52758 +K 24 +svn:sync-last-merged-rev +V 1 +4 +PROPS-END + +Revision-number: 1 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:42.556405Z +K 7 +svn:log +V 4 +init +PROPS-END + +Node-path: sub +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: sub/trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 107 +Content-length: 107 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:43.040912Z +K 7 +svn:log +V 6 +othera +PROPS-END + +Revision-number: 3 +Prop-content-length: 108 +Content-length: 108 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:44.042124Z +K 7 +svn:log +V 7 +othera2 +PROPS-END + +Revision-number: 4 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-03T18:58:45.053459Z +K 7 +svn:log +V 4 +adda +PROPS-END + +Node-path: sub/trunk/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 3b5d5c3712955042212316173ccf37be +Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b +Content-length: 12 + +PROPS-END +b + + diff -Nru hgsubversion-1.4/tests/fixtures/invalid_utf8.sh hgsubversion-1.5/tests/fixtures/invalid_utf8.sh --- hgsubversion-1.4/tests/fixtures/invalid_utf8.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/invalid_utf8.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,34 @@ +#!/bin/bash +#-*- coding: utf-8 -*- +# +# Generate invalid_utf8.svndump +# + +#check svnadmin version, must be >= 1.7 +SVNVERSION=$(svnadmin --version | head -n 1 | cut -d \ -f 3) +if [[ "$SVNVERSION" < '1.7' ]] ; then + echo "You MUST have svn 1.7 or above to use this script" + exit 1 +fi + +set -x + +TMPDIR=$(mktemp -d) +WD=$(pwd) + +cd $TMPDIR + +svnadmin create failrepo +svn co file://$PWD/failrepo fail +( + cd fail + touch A + svn add A + svn ci -m blabargrod +) +svnadmin --pre-1.6-compatible create invalid_utf8 +svnadmin dump failrepo | \ + sed "s/blabargrod/$(echo blåbærgrød | iconv -f utf-8 -t latin1)/g" | \ + svnadmin load --bypass-prop-validation invalid_utf8 + +tar cz -C invalid_utf8 -f "$WD"/invalid_utf8.tar.gz . Binary files /tmp/iq7Oeu7hPv/hgsubversion-1.4/tests/fixtures/invalid_utf8.tar.gz and /tmp/SUkisRpe8v/hgsubversion-1.5/tests/fixtures/invalid_utf8.tar.gz differ diff -Nru hgsubversion-1.4/tests/fixtures/movetotrunk.sh hgsubversion-1.5/tests/fixtures/movetotrunk.sh --- hgsubversion-1.4/tests/fixtures/movetotrunk.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/movetotrunk.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,34 @@ +#!/bin/sh +# +# Generate movetotrunk.svndump +# + +mkdir temp +cd temp + +mkdir project-orig +cd project-orig +cd .. + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn mkdir --parents $svnurl/sub1/sub2 -m subpaths +svn import project-orig $svnurl/sub1/sub2 -m "init project" + +svn co $svnurl/sub1/sub2 project +cd project +echo a > a +svn add a +mkdir dir +echo b > dir/b +svn add dir +svn ci -m adda +svn up +mkdir trunk +svn add trunk +svn mv a trunk/a +svn mv dir trunk/dir +svn ci -m 'move to trunk' +cd .. + +svnadmin dump testrepo > ../movetotrunk.svndump diff -Nru hgsubversion-1.4/tests/fixtures/movetotrunk.svndump hgsubversion-1.5/tests/fixtures/movetotrunk.svndump --- hgsubversion-1.4/tests/fixtures/movetotrunk.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/movetotrunk.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,154 @@ +SVN-fs-dump-format-version: 2 + +UUID: bb3f8dfd-83a8-4fe0-b57e-00a3838532ab + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2012-10-20T20:23:15.254324Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 109 +Content-length: 109 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-20T20:23:15.271492Z +K 7 +svn:log +V 8 +subpaths +PROPS-END + +Node-path: sub1 +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: sub1/sub2 +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-20T20:23:16.068226Z +K 7 +svn:log +V 4 +adda +PROPS-END + +Node-path: sub1/sub2/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b +Content-length: 12 + +PROPS-END +a + + +Node-path: sub1/sub2/dir +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: sub1/sub2/dir/b +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 3b5d5c3712955042212316173ccf37be +Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b +Content-length: 12 + +PROPS-END +b + + +Revision-number: 3 +Prop-content-length: 115 +Content-length: 115 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-20T20:23:20.043626Z +K 7 +svn:log +V 13 +move to trunk +PROPS-END + +Node-path: sub1/sub2/trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: sub1/sub2/trunk/a +Node-kind: file +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: sub1/sub2/a +Text-copy-source-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-copy-source-sha1: 3f786850e387550fdab836ed7e6dc881de23001b + + +Node-path: sub1/sub2/trunk/dir +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: sub1/sub2/dir + + +Node-path: sub1/sub2/dir +Node-action: delete + + +Node-path: sub1/sub2/a +Node-action: delete + + diff -Nru hgsubversion-1.4/tests/fixtures/revert.sh hgsubversion-1.5/tests/fixtures/revert.sh --- hgsubversion-1.4/tests/fixtures/revert.sh 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/revert.sh 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,39 @@ +#!/bin/sh +# +# Generate revert.svndump +# + +rm -rf temp +mkdir temp +cd temp +mkdir -p import/trunk/dir +cd import/trunk +echo a > a +echo b > dir/b +cd ../.. + +svnadmin create testrepo +svnurl=file://`pwd`/testrepo +svn import import $svnurl -m init + +svn co $svnurl project +cd project +echo a >> trunk/a +echo b >> trunk/dir/b +svn ci -m changefiles +svn up +# Test directory revert +svn rm trunk +svn cp $svnurl/trunk@1 trunk +svn st +svn ci -m revert +svn up +# Test file revert +svn rm trunk/a +svn rm trunk/dir/b +svn cp $svnurl/trunk/a@2 trunk/a +svn cp $svnurl/trunk/dir/b@2 trunk/dir/b +svn ci -m revert2 +cd .. + +svnadmin dump testrepo > ../revert.svndump diff -Nru hgsubversion-1.4/tests/fixtures/revert.svndump hgsubversion-1.5/tests/fixtures/revert.svndump --- hgsubversion-1.4/tests/fixtures/revert.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/revert.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,197 @@ +SVN-fs-dump-format-version: 2 + +UUID: 307f02f4-2d74-44cb-98a4-4e162241d396 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2012-10-06T08:50:46.559327Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 105 +Content-length: 105 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-06T08:50:46.581582Z +K 7 +svn:log +V 4 +init +PROPS-END + +Node-path: trunk +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/a +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 60b725f10c9c85c70d97880dfe8191b3 +Text-content-sha1: 3f786850e387550fdab836ed7e6dc881de23001b +Content-length: 12 + +PROPS-END +a + + +Node-path: trunk/dir +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Node-path: trunk/dir/b +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 2 +Text-content-md5: 3b5d5c3712955042212316173ccf37be +Text-content-sha1: 89e6c98d92887913cadf06b2adb97f26cde4849b +Content-length: 12 + +PROPS-END +b + + +Revision-number: 2 +Prop-content-length: 113 +Content-length: 113 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-06T08:50:47.048033Z +K 7 +svn:log +V 11 +changefiles +PROPS-END + +Node-path: trunk/a +Node-kind: file +Node-action: change +Text-content-length: 4 +Text-content-md5: 0d227f1abf8c2932d342e9b99cc957eb +Text-content-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29 +Content-length: 4 + +a +a + + +Node-path: trunk/dir/b +Node-kind: file +Node-action: change +Text-content-length: 4 +Text-content-md5: 06ac26ed8b614fc0b141e4542aa067c2 +Text-content-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95 +Content-length: 4 + +b +b + + +Revision-number: 3 +Prop-content-length: 107 +Content-length: 107 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-06T08:50:50.058224Z +K 7 +svn:log +V 6 +revert +PROPS-END + +Node-path: trunk +Node-kind: dir +Node-action: delete + +Node-path: trunk +Node-kind: dir +Node-action: add +Node-copyfrom-rev: 1 +Node-copyfrom-path: trunk + + + + +Revision-number: 4 +Prop-content-length: 108 +Content-length: 108 + +K 10 +svn:author +V 7 +pmezard +K 8 +svn:date +V 27 +2012-10-06T08:50:54.047396Z +K 7 +svn:log +V 7 +revert2 +PROPS-END + +Node-path: trunk/a +Node-kind: file +Node-action: delete + +Node-path: trunk/a +Node-kind: file +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk/a +Text-copy-source-md5: 0d227f1abf8c2932d342e9b99cc957eb +Text-copy-source-sha1: d7c8127a20a396cff08af086a1c695b0636f0c29 + + + + +Node-path: trunk/dir/b +Node-kind: file +Node-action: delete + +Node-path: trunk/dir/b +Node-kind: file +Node-action: add +Node-copyfrom-rev: 2 +Node-copyfrom-path: trunk/dir/b +Text-copy-source-md5: 06ac26ed8b614fc0b141e4542aa067c2 +Text-copy-source-sha1: f6980469e74f7125178e88ec571e06fe6ce86e95 + + + + diff -Nru hgsubversion-1.4/tests/fixtures/subdir_is_file_prefix.svndump hgsubversion-1.5/tests/fixtures/subdir_is_file_prefix.svndump --- hgsubversion-1.4/tests/fixtures/subdir_is_file_prefix.svndump 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/fixtures/subdir_is_file_prefix.svndump 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,72 @@ +SVN-fs-dump-format-version: 2 + +UUID: 924a052a-5e5a-4a8e-a677-da5565bec340 + +Revision-number: 0 +Prop-content-length: 56 +Content-length: 56 + +K 8 +svn:date +V 27 +2011-03-04T12:33:29.342045Z +PROPS-END + +Revision-number: 1 +Prop-content-length: 123 +Content-length: 123 + +K 7 +svn:log +V 22 +Create directory flaf. +K 10 +svn:author +V 6 +danchr +K 8 +svn:date +V 27 +2011-03-04T12:34:00.349950Z +PROPS-END + +Node-path: flaf +Node-kind: dir +Node-action: add +Prop-content-length: 10 +Content-length: 10 + +PROPS-END + + +Revision-number: 2 +Prop-content-length: 138 +Content-length: 138 + +K 7 +svn:log +V 37 +Create the file flaf.txt within flaf. +K 10 +svn:author +V 6 +danchr +K 8 +svn:date +V 27 +2011-03-04T12:45:01.701033Z +PROPS-END + +Node-path: flaf/flaf.txt +Node-kind: file +Node-action: add +Prop-content-length: 10 +Text-content-length: 15 +Text-content-md5: 8c0059c8f7998e8003836b8e8fcb74d7 +Text-content-sha1: b7d680bc5411f46395c4ef267001e1a307d7b0d5 +Content-length: 25 + +PROPS-END +Goodbye world. + + diff -Nru hgsubversion-1.4/tests/run.py hgsubversion-1.5/tests/run.py --- hgsubversion-1.4/tests/run.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/run.py 2012-10-29 11:16:19.000000000 +0000 @@ -18,26 +18,29 @@ import test_fetch_renames import test_fetch_symlinks import test_fetch_truncated + import test_hooks import test_pull + import test_pull_fallback import test_push_command import test_push_renames import test_push_dirs import test_push_eol + import test_push_autoprops import test_rebuildmeta import test_single_dir_clone - import test_startrev import test_svnwrap import test_tags import test_template_keywords import test_utility_commands import test_unaffected_core + import test_updatemeta import test_urls sys.path.append(os.path.dirname(__file__)) sys.path.append(os.path.join(os.path.dirname(__file__), 'comprehensive')) import test_stupid_pull - import test_verify + import test_verify_and_startrev return locals() diff -Nru hgsubversion-1.4/tests/test_fetch_branches.py hgsubversion-1.5/tests/test_fetch_branches.py --- hgsubversion-1.4/tests/test_fetch_branches.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_fetch_branches.py 2012-10-29 11:16:19.000000000 +0000 @@ -58,8 +58,9 @@ self.test_unorderedbranch(True) def test_renamed_branch_to_trunk(self, stupid=False): + config = {'hgsubversion.failonmissing': 'true'} repo = self._load_fixture_and_fetch('branch_rename_to_trunk.svndump', - stupid=stupid) + stupid=stupid, config=config) self.assertEqual(repo['default'].parents()[0].branch(), 'dev_branch') self.assert_('iota' in repo['default']) self.assertEqual(repo['old_trunk'].parents()[0].branch(), 'default') diff -Nru hgsubversion-1.4/tests/test_fetch_command.py hgsubversion-1.5/tests/test_fetch_command.py --- hgsubversion-1.4/tests/test_fetch_command.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_fetch_command.py 2012-10-29 11:16:19.000000000 +0000 @@ -8,6 +8,7 @@ from mercurial import hg from mercurial import node from mercurial import ui +from mercurial import encoding class TestBasicRepoLayout(test_util.TestBase): @@ -94,13 +95,16 @@ assert 'README' not in repo assert '../branches' not in repo - def test_files_copied_from_outside_btt(self): + def test_files_copied_from_outside_btt(self, stupid=False): repo = self._load_fixture_and_fetch( - 'test_files_copied_from_outside_btt.svndump') + 'test_files_copied_from_outside_btt.svndump', stupid=stupid) self.assertEqual(node.hex(repo['tip'].node()), '3c78170e30ddd35f2c32faa0d8646ab75bba4f73') self.assertEqual(len(repo.changelog), 2) + def test_files_copied_from_outside_btt_stupid(self): + self.test_files_copied_from_outside_btt(stupid=True) + def test_file_renamed_in_from_outside_btt(self): repo = self._load_fixture_and_fetch( 'file_renamed_in_from_outside_btt.svndump') @@ -175,7 +179,8 @@ self.assertEqual(repo[r].hex(), repo2[r].hex()) def test_path_quoting_stupid(self): - self.test_path_quoting(True) + repo = self.test_path_quoting(True) + def test_identical_fixtures(self): '''ensure that the non_ascii_path_N fixtures are identical''' @@ -186,6 +191,14 @@ self.assertMultiLineEqual(open(fixturepaths[0]).read(), open(fixturepaths[1]).read()) + def test_invalid_message(self): + repo = self._load_fixture_and_fetch('invalid_utf8.tar.gz') + # changelog returns descriptions in local encoding + desc = encoding.fromlocal(repo[0].description()) + self.assertEqual(desc.decode('utf8'), + u'bl\xe5b\xe6rgr\xf8d') + + class TestStupidPull(test_util.TestBase): def test_stupid(self): repo = self._load_fixture_and_fetch('two_heads.svndump', stupid=True) @@ -216,6 +229,63 @@ self.assertEqual(node.hex(repo['tip'].node()), '1a6c3f30911d57abb67c257ec0df3e7bc44786f7') + def test_empty_repo(self, stupid=False): + # This used to crash HgEditor because it could be closed without + # having been initialized again. + self._load_fixture_and_fetch('emptyrepo2.svndump', stupid=stupid) + + def test_empty_repo_stupid(self): + self.test_empty_repo(stupid=True) + + def test_fetch_revert(self, stupid=False): + repo = self._load_fixture_and_fetch('revert.svndump', stupid=stupid) + graph = self.getgraph(repo) + refgraph = """\ +o changeset: 3:937dcd1206d4 +| branch: +| tags: tip +| summary: revert2 +| files: a dir/b +| +o changeset: 2:9317a748b7c3 +| branch: +| tags: +| summary: revert +| files: a dir/b +| +o changeset: 1:243259a4138a +| branch: +| tags: +| summary: changefiles +| files: a dir/b +| +o changeset: 0:ab86791fc857 + branch: + tags: + summary: init + files: a dir/b +""" + self.assertEqual(refgraph.strip(), graph.strip()) + + def test_fetch_revert_stupid(self): + self.test_fetch_revert(stupid=True) + + def test_fetch_movetotrunk(self, stupid=False): + repo = self._load_fixture_and_fetch('movetotrunk.svndump', + stupid=stupid, subdir='sub1/sub2') + graph = self.getgraph(repo) + refgraph = """\ +o changeset: 0:02996a5980ba + branch: + tags: tip + summary: move to trunk + files: a dir/b +""" + self.assertEqual(refgraph.strip(), graph.strip()) + + def test_fetch_movetotrunk_stupid(self): + self.test_fetch_movetotrunk(stupid=True) + def suite(): all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestBasicRepoLayout), unittest.TestLoader().loadTestsFromTestCase(TestStupidPull), diff -Nru hgsubversion-1.4/tests/test_fetch_mappings.py hgsubversion-1.5/tests/test_fetch_mappings.py --- hgsubversion-1.4/tests/test_fetch_mappings.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_fetch_mappings.py 2012-10-29 11:16:19.000000000 +0000 @@ -13,6 +13,7 @@ from hgsubversion import maps from hgsubversion import svncommands from hgsubversion import util +from hgsubversion import verify class MapTests(test_util.TestBase): @property @@ -100,49 +101,43 @@ all_tests = set(test) self.assertEqual(fromself.symmetric_difference(all_tests), set()) - def test_file_map(self, stupid=False): - repo_path = self.load_svndump('replace_trunk_with_branch.svndump') + def _loadwithfilemap(self, svndump, filemapcontent, stupid=False, + failonmissing=True): + repo_path = self.load_svndump(svndump) filemap = open(self.filemap, 'w') - filemap.write("include alpha\n") + filemap.write(filemapcontent) filemap.close() ui = self.ui(stupid) ui.setconfig('hgsubversion', 'filemap', self.filemap) + ui.setconfig('hgsubversion', 'failoninvalidreplayfile', 'true') + ui.setconfig('hgsubversion', 'failonmissing', failonmissing) commands.clone(ui, test_util.fileurl(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') + return self.repo + + def test_file_map(self, stupid=False): + repo = self._loadwithfilemap('replace_trunk_with_branch.svndump', + "include alpha\n", stupid) + self.assertEqual(node.hex(repo[0].node()), '88e2c7492d83e4bf30fbb2dcbf6aa24d60ac688d') + self.assertEqual(node.hex(repo['default'].node()), 'e524296152246b3837fe9503c83b727075835155') def test_file_map_stupid(self): # TODO: re-enable test if we ever reinstate this feature self.assertRaises(hgutil.Abort, self.test_file_map, True) def test_file_map_exclude(self, stupid=False): - repo_path = self.load_svndump('replace_trunk_with_branch.svndump') - filemap = open(self.filemap, 'w') - filemap.write("exclude alpha\n") - filemap.close() - ui = self.ui(stupid) - ui.setconfig('hgsubversion', 'filemap', self.filemap) - commands.clone(ui, test_util.fileurl(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') + repo = self._loadwithfilemap('replace_trunk_with_branch.svndump', + "exclude alpha\n", stupid) + self.assertEqual(node.hex(repo[0].node()), '2c48f3525926ab6c8b8424bcf5eb34b149b61841') + self.assertEqual(node.hex(repo['default'].node()), 'b37a3c0297b71f989064d9b545b5a478bbed7cc1') def test_file_map_exclude_stupid(self): # TODO: re-enable test if we ever reinstate this feature self.assertRaises(hgutil.Abort, self.test_file_map_exclude, True) def test_file_map_rule_order(self): - repo_path = self.load_svndump('replace_trunk_with_branch.svndump') - filemap = open(self.filemap, 'w') - filemap.write("exclude alpha\n") - filemap.write("include .\n") - filemap.write("exclude gamma\n") - filemap.close() - ui = self.ui(False) - ui.setconfig('hgsubversion', 'filemap', self.filemap) - commands.clone(ui, test_util.fileurl(repo_path), - self.wc_path, filemap=self.filemap) + repo = self._loadwithfilemap('replace_trunk_with_branch.svndump', + "exclude alpha\ninclude .\nexclude gamma\n") # The exclusion of alpha is overridden by the later rule to # include all of '.', whereas gamma should remain excluded # because it's excluded after the root directory. @@ -151,6 +146,33 @@ self.assertEqual(self.repo['default'].manifest().keys(), ['alpha', 'beta']) + def test_file_map_copy(self): + # Exercise excluding files copied from a non-excluded directory. + # There will be missing files as we are copying from an excluded + # directory. + repo = self._loadwithfilemap('copies.svndump', "exclude dir2\n", + failonmissing=False) + self.assertEqual(['dir/a', 'dir3/a'], list(repo[2])) + + def test_file_map_exclude_copy_source_and_dest(self): + # dir3 is excluded and copied from dir2 which is also excluded. + # dir3 files should not be marked as missing and fetched. + repo = self._loadwithfilemap('copies.svndump', + "exclude dir2\nexclude dir3\n") + self.assertEqual(['dir/a'], list(repo[2])) + + def test_file_map_include_file_exclude_dir(self): + # dir3 is excluded but we want dir3/a, which is also copied from + # an exluded dir2. dir3/a should be fetched. + repo = self._loadwithfilemap('copies.svndump', + "include .\nexclude dir2\nexclude dir3\ninclude dir3/a\n", + failonmissing=False) + self.assertEqual(['dir/a', 'dir3/a'], list(repo[2])) + + def test_file_map_delete_dest(self): + repo = self._loadwithfilemap('copies.svndump', 'exclude dir3\n') + self.assertEqual(['dir/a', 'dir2/a'], list(repo[3])) + def test_branchmap(self, stupid=False): repo_path = self.load_svndump('branchmap.svndump') branchmap = open(self.branchmap, 'w') @@ -244,6 +266,8 @@ ui = self.ui(stupid) src, dest = test_util.hgclone(ui, self.wc_path, self.wc_path + '_clone', update=False) + src = test_util.getlocalpeer(src) + dest = test_util.getlocalpeer(dest) svncommands.rebuildmeta(ui, dest, args=[test_util.fileurl(repo_path)]) @@ -270,7 +294,7 @@ repo = self.repo for r in repo: - self.assertEquals(svncommands.verify(ui, repo, rev=r), 0) + self.assertEquals(verify.verify(ui, repo, rev=r), 0) def test_branchmap_verify_stupid(self): '''test verify on a branchmapped clone (stupid)''' diff -Nru hgsubversion-1.4/tests/test_fetch_renames.py hgsubversion-1.5/tests/test_fetch_renames.py --- hgsubversion-1.4/tests/test_fetch_renames.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_fetch_renames.py 2012-10-29 11:16:19.000000000 +0000 @@ -14,8 +14,11 @@ w('%s: %r %r\n' % (f, fctx.data(), fctx.renamed())) def _test_rename(self, stupid): - repo = self._load_fixture_and_fetch('renames.svndump', stupid=stupid) - # self._debug_print_copies(repo) + config = { + 'hgsubversion.filestoresize': '0', + } + repo = self._load_fixture_and_fetch('renames.svndump', stupid=stupid, + config=config) # Map revnum to mappings of dest name to (source name, dest content) copies = { diff -Nru hgsubversion-1.4/tests/test_fetch_symlinks.py hgsubversion-1.5/tests/test_fetch_symlinks.py --- hgsubversion-1.4/tests/test_fetch_symlinks.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_fetch_symlinks.py 2012-10-29 11:16:19.000000000 +0000 @@ -50,7 +50,17 @@ def test_symlinks_stupid(self): self.test_symlinks(True) +class TestMergeSpecial(test_util.TestBase): + def test_special(self): + repo = self._load_fixture_and_fetch('addspecial.svndump', + subdir='trunk') + ctx = repo['tip'] + self.assertEqual(ctx['fnord'].flags(), 'l') + self.assertEqual(ctx['exe'].flags(), 'x') + def suite(): - all_tests = [unittest.TestLoader().loadTestsFromTestCase(TestFetchSymlinks), - ] + all_tests = [ + unittest.TestLoader().loadTestsFromTestCase(TestFetchSymlinks), + unittest.TestLoader().loadTestsFromTestCase(TestMergeSpecial), + ] return unittest.TestSuite(all_tests) diff -Nru hgsubversion-1.4/tests/test_helpers.py hgsubversion-1.5/tests/test_helpers.py --- hgsubversion-1.4/tests/test_helpers.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/test_helpers.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,37 @@ +import os, sys, unittest + +_rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, _rootdir) + +from hgsubversion import editor + +class TestHelpers(unittest.TestCase): + def test_filestore(self): + fs = editor.FileStore(2) + fs.setfile('a', 'a') + fs.setfile('b', 'b') + self.assertEqual('a', fs._data.get('a')) + self.assertEqual('b', fs._data.get('b')) + + fs.delfile('b') + self.assertRaises(IOError, lambda: fs.getfile('b')) + fs.setfile('bb', 'bb') + self.assertTrue('bb' in fs._files) + self.assertTrue('bb' not in fs._data) + self.assertEqual('bb', fs.getfile('bb')) + + fs.delfile('bb') + self.assertTrue('bb' not in fs._files) + self.assertEqual([], os.listdir(fs._tempdir)) + self.assertRaises(IOError, lambda: fs.getfile('bb')) + + fs.setfile('bb', 'bb') + self.assertEqual(1, len(os.listdir(fs._tempdir))) + fs.popfile('bb') + self.assertEqual([], os.listdir(fs._tempdir)) + self.assertRaises(editor.EditingError, lambda: fs.getfile('bb')) + +def suite(): + return unittest.TestSuite([ + unittest.TestLoader().loadTestsFromTestCase(TestHelpers), + ]) diff -Nru hgsubversion-1.4/tests/test_hooks.py hgsubversion-1.5/tests/test_hooks.py --- hgsubversion-1.4/tests/test_hooks.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/test_hooks.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,48 @@ +import sys +import test_util +import unittest + +from mercurial import hg +from mercurial import commands + +class TestHooks(test_util.TestBase): + def setUp(self): + super(TestHooks, self).setUp() + + def _loadupdate(self, fixture_name, *args, **kwargs): + kwargs = kwargs.copy() + kwargs.update(stupid=False, noupdate=False) + repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs) + return repo, repo_path + + def test_updatemetahook(self): + repo, repo_path = self._loadupdate('single_rev.svndump') + state = repo.parents() + self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'}) + commands.pull(self.repo.ui, self.repo) + + # Clone to a new repository and add a hook + new_wc_path = "%s-2" % self.wc_path + commands.clone(self.repo.ui, self.wc_path, new_wc_path) + newrepo = hg.repository(test_util.testui(), new_wc_path) + newrepo.ui.setconfig('hooks', 'changegroup.meta', + 'python:hgsubversion.hooks.updatemeta.hook') + + # Commit a rev that should trigger svn meta update + self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed Again'}) + commands.pull(self.repo.ui, self.repo) + + self.called = False + import hgsubversion.svncommands + oldupdatemeta = hgsubversion.svncommands.updatemeta + def _updatemeta(ui, repo, args=[]): + self.called = True + hgsubversion.svncommands.updatemeta = _updatemeta + + # Pull and make sure our updatemeta function gets called + commands.pull(newrepo.ui, newrepo) + hgsubversion.svncommands.updatemeta = oldupdatemeta + self.assertTrue(self.called) + +def suite(): + return unittest.findTestCases(sys.modules[__name__]) diff -Nru hgsubversion-1.4/tests/test_pull.py hgsubversion-1.5/tests/test_pull.py --- hgsubversion-1.4/tests/test_pull.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_pull.py 2012-10-29 11:16:19.000000000 +0000 @@ -6,14 +6,16 @@ from mercurial import ui from mercurial import util as hgutil from mercurial import commands +from hgsubversion import verify class TestPull(test_util.TestBase): def setUp(self): super(TestPull, self).setUp() - def _loadupdate(self, fixture_name): - repo, repo_path = self.load_and_fetch(fixture_name, stupid=False, - noupdate=False) + def _loadupdate(self, fixture_name, *args, **kwargs): + kwargs = kwargs.copy() + kwargs.update(stupid=False, noupdate=False) + repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs) return repo, repo_path def test_nochanges(self): @@ -58,6 +60,26 @@ commands.pull(repo.ui, repo) self.assertEqual(oldheads, map(node.hex, repo.heads())) + def test_skip_basic(self): + repo, repo_path = self._loadupdate('single_rev.svndump') + self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'}) + self.add_svn_rev(repo_path, {'trunk/beta': 'More changed'}) + self.add_svn_rev(repo_path, {'trunk/gamma': 'Even more changeder'}) + repo.ui.setconfig('hgsubversion', 'unsafeskip', '3 4') + commands.pull(repo.ui, repo) + tip = repo['tip'].rev() + self.assertEqual(tip, 1) + self.assertEquals(verify.verify(repo.ui, repo, rev=tip), 1) + + def test_skip_delete_restore(self): + repo, repo_path = self._loadupdate('delete_restore_trunk.svndump', + rev=2) + repo.ui.setconfig('hgsubversion', 'unsafeskip', '3 4') + commands.pull(repo.ui, repo) + tip = repo['tip'].rev() + self.assertEqual(tip, 1) + self.assertEquals(verify.verify(repo.ui, repo, rev=tip), 0) + def suite(): import unittest, sys return unittest.findTestCases(sys.modules[__name__]) diff -Nru hgsubversion-1.4/tests/test_pull_fallback.py hgsubversion-1.5/tests/test_pull_fallback.py --- hgsubversion-1.4/tests/test_pull_fallback.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/test_pull_fallback.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,106 @@ +import test_util + +import re +import mercurial +from mercurial import commands +from hgsubversion import stupid +from hgsubversion import svnwrap +from hgsubversion import wrappers + +class TestPullFallback(test_util.TestBase): + def setUp(self): + super(TestPullFallback, self).setUp() + + def _loadupdate(self, fixture_name, *args, **kwargs): + kwargs = kwargs.copy() + kwargs.update(noupdate=False) + repo, repo_path = self.load_and_fetch(fixture_name, *args, **kwargs) + return repo, repo_path + + def test_stupid_fallback_to_stupid_fullrevs(self): + return + to_patch = { + 'mercurial.patch.patchbackend': _patchbackend_raise, + 'stupid.diff_branchrev': stupid.diff_branchrev, + 'stupid.fetch_branchrev': stupid.fetch_branchrev, + } + + expected_calls = { + 'mercurial.patch.patchbackend': 1, + 'stupid.diff_branchrev': 1, + 'stupid.fetch_branchrev': 1, + } + + repo, repo_path = self._loadupdate( + 'single_rev.svndump', stupid=True) + + # Passing stupid=True doesn't seem to be working - force it + repo.ui.setconfig('hgsubversion', 'stupid', "true") + state = repo.parents() + + calls, replaced = _monkey_patch(to_patch) + + try: + self.add_svn_rev(repo_path, {'trunk/alpha': 'Changed'}) + commands.pull(self.repo.ui, repo, update=True) + self.failIfEqual(state, repo.parents()) + self.assertTrue('tip' in repo[None].tags()) + self.assertEqual(expected_calls, calls) + + finally: + _monkey_unpatch(replaced) + +def _monkey_patch(to_patch, start=None): + if start is None: + import sys + start = sys.modules[__name__] + + calls = {} + replaced = {} + + for path, replacement in to_patch.iteritems(): + obj = start + owner, attr = path.rsplit('.', 1) + + for a in owner.split('.', -1): + obj = getattr(obj, a) + + replaced[path] = getattr(obj, attr) + calls[path] = 0 + + def outer(path=path, calls=calls, replacement=replacement): + def wrapper(*p, **kw): + calls[path] += 1 + return replacement(*p, **kw) + + return wrapper + + setattr(obj, attr, outer()) + + return calls, replaced + +def _monkey_unpatch(to_patch, start=None): + if start is None: + import sys + start = sys.modules[__name__] + + replaced = {} + + for path, replacement in to_patch.iteritems(): + obj = start + owner, attr = path.rsplit('.', 1) + + for a in owner.split('.', -1): + obj = getattr(obj, a) + + replaced[path] = getattr(obj, attr) + setattr(obj, attr, replacement) + + return replaced + +def _patchbackend_raise(*p, **kw): + raise mercurial.patch.PatchError("patch failed") + +def suite(): + import unittest, sys + return unittest.findTestCases(sys.modules[__name__]) diff -Nru hgsubversion-1.4/tests/test_push_autoprops.py hgsubversion-1.5/tests/test_push_autoprops.py --- hgsubversion-1.4/tests/test_push_autoprops.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/test_push_autoprops.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,107 @@ +import subprocess +import sys +import unittest +import os + +import test_util + +from hgsubversion import svnwrap + +class PushAutoPropsTests(test_util.TestBase): + def setUp(self): + test_util.TestBase.setUp(self) + repo, self.repo_path = self.load_and_fetch('emptyrepo.svndump') + + def test_push_honors_svn_autoprops(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + changes = [('test.py', 'test.py', 'echo hallo')] + self.commitchanges(changes) + self.pushrevisions(True) + prop_val = test_util.svnpropget( + self.repo_path, "trunk/test.py", 'test:prop') + self.assertEqual('success', prop_val) + + +class AutoPropsConfigTest(test_util.TestBase): + def test_use_autoprops_for_matching_file_when_enabled(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({ 'test:prop': 'success'}, props) + + def new_autoprops_config(self): + return svnwrap.AutoPropsConfig(self.config_dir) + + def test_ignore_nonexisting_config(self): + config_file = os.path.join(self.config_dir, 'config') + os.remove(config_file) + self.assertTrue(not os.path.exists(config_file)) + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({}, props) + + def test_ignore_autoprops_when_file_doesnt_match(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.sh') + self.assertEqual({}, props) + + def test_ignore_autoprops_when_disabled(self): + self.setup_svn_config( + "[miscellany]\n" + "#enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({}, props) + + def test_combine_properties_of_multiple_matches(self): + self.setup_svn_config( + "[miscellany]\n" + "enable-auto-props = yes\n" + "[auto-props]\n" + "*.py = test:prop=success\n" + "test.* = test:prop2=success\n") + props = self.new_autoprops_config().properties('xxx/test.py') + self.assertEqual({ + 'test:prop': 'success', 'test:prop2': 'success'}, props) + + +class ParseAutoPropsTests(test_util.TestBase): + def test_property_value_is_optional(self): + props = svnwrap.parse_autoprops("svn:executable") + self.assertEqual({'svn:executable': ''}, props) + props = svnwrap.parse_autoprops("svn:executable=") + self.assertEqual({'svn:executable': ''}, props) + + def test_property_value_may_be_quoted(self): + props = svnwrap.parse_autoprops("svn:eol-style=\" native \"") + self.assertEqual({'svn:eol-style': ' native '}, props) + props = svnwrap.parse_autoprops("svn:eol-style=' native '") + self.assertEqual({'svn:eol-style': ' native '}, props) + + def test_surrounding_whitespaces_are_ignored(self): + props = svnwrap.parse_autoprops(" svn:eol-style = native ") + self.assertEqual({'svn:eol-style': 'native'}, props) + + def test_multiple_properties_are_separated_by_semicolon(self): + props = svnwrap.parse_autoprops( + "svn:eol-style=native;svn:executable=true\n") + self.assertEqual({ + 'svn:eol-style': 'native', + 'svn:executable': 'true'}, + props) + + +def suite(): + return unittest.findTestCases(sys.modules[__name__]) + diff -Nru hgsubversion-1.4/tests/test_push_command.py hgsubversion-1.5/tests/test_push_command.py --- hgsubversion-1.4/tests/test_push_command.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_push_command.py 2012-10-29 11:16:19.000000000 +0000 @@ -134,7 +134,7 @@ finally: if sys.version_info >= (2,6): svnserve.kill() - else: + else: test_util.kill_process(svnserve) def test_push_over_svnserve(self): @@ -487,7 +487,40 @@ 'Outgoing changesets parent is not at subversion ' 'HEAD\n' '(pull again and rebase on a newer revision)') + # verify that any pending transactions on the server got cleaned up + self.assertEqual([], os.listdir( + os.path.join(self.tmpdir, 'testrepo-1', 'db', 'transactions'))) + def test_push_encoding(self): + self.test_push_two_revs() + # Writing then rebasing UTF-8 filenames in a cp1252 windows console + # used to fail because hg internal encoding was being changed during + # the interactions with subversion, *and during the rebase*, which + # confused the dirstate and made it believe the file was deleted. + fn = 'pi\xc3\xa8ce/test' + changes = [(fn, fn, 'a')] + par = self.repo['tip'].rev() + self.commitchanges(changes, parent=par) + self.pushrevisions() + + def test_push_emptying_changeset(self): + r = self.repo['tip'] + changes = [ + ('alpha', None, None), + ('beta', None, None), + ] + parent = self.repo['tip'].rev() + self.commitchanges(changes, parent=parent) + self.pushrevisions() + self.assertEqual({}, self.repo['tip'].manifest()) + + # Try to re-add a file after emptying the branch + changes = [ + ('alpha', 'alpha', 'alpha'), + ] + self.commitchanges(changes, parent=self.repo['tip'].rev()) + self.pushrevisions() + self.assertEqual(['alpha'], list(self.repo['tip'].manifest())) def suite(): test_classes = [PushTests, ] diff -Nru hgsubversion-1.4/tests/test_rebuildmeta.py hgsubversion-1.5/tests/test_rebuildmeta.py --- hgsubversion-1.4/tests/test_rebuildmeta.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_rebuildmeta.py 2012-10-29 11:16:19.000000000 +0000 @@ -34,6 +34,8 @@ wc2_path = self.wc_path + '_clone' u = ui.ui() src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False) + src = test_util.getlocalpeer(src) + dest = test_util.getlocalpeer(dest) # insert a wrapper that prevents calling changectx.children() def failfn(orig, ctx): @@ -51,9 +53,48 @@ # remove the wrapper context.changectx.children = origchildren + self._run_assertions(name, stupid, single, src, dest, u) + + wc3_path = self.wc_path + '_partial' + src, dest = test_util.hgclone(u, + self.wc_path, + wc3_path, + update=False, + rev=[0]) + srcrepo = test_util.getlocalpeer(src) + dest = test_util.getlocalpeer(dest) + + # insert a wrapper that prevents calling changectx.children() + extensions.wrapfunction(context.changectx, 'children', failfn) + + try: + svncommands.rebuildmeta(u, dest, + args=[test_util.fileurl(repo_path + + subdir), ]) + finally: + # remove the wrapper + context.changectx.children = origchildren + + dest.pull(src) + + # insert a wrapper that prevents calling changectx.children() + extensions.wrapfunction(context.changectx, 'children', failfn) + try: + svncommands.updatemeta(u, dest, + args=[test_util.fileurl(repo_path + + subdir), ]) + finally: + # remove the wrapper + context.changectx.children = origchildren + + self._run_assertions(name, stupid, single, srcrepo, dest, u) + + +def _run_assertions(self, name, stupid, single, src, dest, u): + 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')), + self.assertTrue(os.path.isdir(os.path.join(dest.path, 'svn')), 'no .hg/svn directory in the destination!') dest = hg.repository(u, os.path.dirname(dest.path)) for tf in ('lastpulled', 'rev_map', 'uuid', 'tagmap', 'layout', 'subdir',): @@ -68,7 +109,7 @@ self.assertNotEqual(old, new, 'rebuildmeta unexpected match on youngest rev!') continue - self.assertMultiLineEqual(old, new) + self.assertMultiLineEqual(old, new, tf + ' differs') self.assertEqual(src.branchtags(), dest.branchtags()) srcbi = pickle.load(open(os.path.join(src.path, 'svn', 'branch_info'))) destbi = pickle.load(open(os.path.join(dest.path, 'svn', 'branch_info'))) @@ -101,11 +142,17 @@ return m +skip = set([ + 'project_root_not_repo_root.svndump', + 'corrupt.svndump', +]) + attrs = {'_do_case': _do_case, + '_run_assertions': _run_assertions, } for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]: # this fixture results in an empty repository, don't use it - if case == 'project_root_not_repo_root.svndump': + if case in skip: continue bname = 'test_' + case[:-len('.svndump')] attrs[bname] = buildmethod(case, bname, False, False) diff -Nru hgsubversion-1.4/tests/test_single_dir_clone.py hgsubversion-1.5/tests/test_single_dir_clone.py --- hgsubversion-1.4/tests/test_single_dir_clone.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_single_dir_clone.py 2012-10-29 11:16:19.000000000 +0000 @@ -44,6 +44,15 @@ self.assertEqual(repo.branchtags().keys(), ['default', ]) self.assertEqual(repo['default'].manifest().keys(), oldmanifest) + def test_clone_subdir_is_file_prefix(self, stupid=False): + FIXTURE = 'subdir_is_file_prefix.svndump' + repo = self._load_fixture_and_fetch(FIXTURE, + stupid=stupid, + layout='single', + subdir=test_util.subdir[FIXTURE]) + self.assertEqual(repo.branchtags().keys(), ['default']) + self.assertEqual(repo['tip'].manifest().keys(), ['flaf.txt']) + def test_externals_single(self): repo = self._load_fixture_and_fetch('externals.svndump', stupid=False, diff -Nru hgsubversion-1.4/tests/test_startrev.py hgsubversion-1.5/tests/test_startrev.py --- hgsubversion-1.4/tests/test_startrev.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_startrev.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -import test_util - -import os -import unittest - -def _do_case(self, name, subdir, stupid): - wc_base = self.wc_path - self.wc_path = wc_base + '_full' - headclone = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid, - layout='single', startrev='HEAD') - self.wc_path = wc_base + '_head' - fullclone = self._load_fixture_and_fetch(name, subdir=subdir, stupid=stupid, - layout='single') - - fulltip = fullclone['tip'] - headtip = headclone['tip'] - # viewing diff's of lists of files is easier on the eyes - self.assertMultiLineEqual('\n'.join(fulltip), '\n'.join(headtip)) - - for f in fulltip: - self.assertMultiLineEqual(fulltip[f].data(), headtip[f].data()) - - self.assertNotEqual(len(fullclone), 0, "full clone shouldn't be empty") - self.assertEqual(len(headclone), 1, - "shallow clone should have just one revision, not %d" - % len(headclone)) - -def buildmethod(case, name, subdir, stupid): - m = lambda self: self._do_case(case, subdir.strip('/'), stupid) - m.__name__ = name - m.__doc__ = ('Test clone with startrev on %s%s with %s replay.' % - (case, subdir, (stupid and 'stupid') or 'real')) - return m - - -# these fixtures contain no files at HEAD and would result in empty clones -nofiles = set([ - 'binaryfiles.svndump', - 'binaryfiles-broken.svndump', - 'emptyrepo.svndump', -]) - -# these fixtures contain no files in trunk at HEAD and would result in an empty -# shallow clone if cloning trunk, so we use another subdirectory -subdirmap = { - 'commit-to-tag.svndump': '/branches/magic', - 'pushexternals.svndump': '', - 'tag_name_same_as_branch.svndump': '/branches/magic', -} - -attrs = {'_do_case': _do_case, - } - -for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]: - if case in nofiles: - continue - - subdir = test_util.subdir.get(case, '') + subdirmap.get(case, '/trunk') - - bname = 'test_' + case[:-len('.svndump')] - attrs[bname] = buildmethod(case, bname, subdir, False) - name = bname + '_stupid' - attrs[name] = buildmethod(case, name, subdir, True) - -StartRevTests = type('StartRevTests', (test_util.TestBase,), attrs) - - -def suite(): - all_tests = [unittest.TestLoader().loadTestsFromTestCase(StartRevTests), - ] - return unittest.TestSuite(all_tests) diff -Nru hgsubversion-1.4/tests/test_tags.py hgsubversion-1.5/tests/test_tags.py --- hgsubversion-1.4/tests/test_tags.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_tags.py 2012-10-29 11:16:19.000000000 +0000 @@ -112,6 +112,7 @@ "You should check that before assuming issues with this test.\n") wc2_path = self.wc_path + '2' src, dest = test_util.hgclone(repo.ui, self.wc_path, wc2_path, update=False) + dest = test_util.getlocalpeer(dest) svncommands.rebuildmeta(repo.ui, dest, args=[test_util.fileurl(repo_path), ]) diff -Nru hgsubversion-1.4/tests/test_updatemeta.py hgsubversion-1.5/tests/test_updatemeta.py --- hgsubversion-1.4/tests/test_updatemeta.py 1970-01-01 00:00:00.000000000 +0000 +++ hgsubversion-1.5/tests/test_updatemeta.py 2012-10-29 11:16:19.000000000 +0000 @@ -0,0 +1,81 @@ +import test_util + +import os +import pickle +import unittest +import test_rebuildmeta + +from mercurial import context +from mercurial import extensions +from mercurial import hg +from mercurial import ui + +from hgsubversion import svncommands +from hgsubversion import svnmeta + + + +def _do_case(self, name, stupid, single): + subdir = test_util.subdir.get(name, '') + layout = 'auto' + if single: + layout = 'single' + repo, repo_path = self.load_and_fetch(name, subdir=subdir, stupid=stupid, + layout=layout) + assert len(self.repo) > 0 + wc2_path = self.wc_path + '_clone' + u = ui.ui() + src, dest = test_util.hgclone(u, self.wc_path, wc2_path, update=False) + src = test_util.getlocalpeer(src) + dest = test_util.getlocalpeer(dest) + + # insert a wrapper that prevents calling changectx.children() + def failfn(orig, ctx): + self.fail('calling %s is forbidden; it can cause massive slowdowns ' + 'when rebuilding large repositories' % orig) + + origchildren = getattr(context.changectx, 'children') + extensions.wrapfunction(context.changectx, 'children', failfn) + + # test updatemeta on an empty repo + try: + svncommands.updatemeta(u, dest, + args=[test_util.fileurl(repo_path + + subdir), ]) + finally: + # remove the wrapper + context.changectx.children = origchildren + + self._run_assertions(name, stupid, single, src, dest, u) + + +def _run_assertions(self, name, stupid, single, src, dest, u): + test_rebuildmeta._run_assertions(self, name, stupid, single, src, dest, u) + + +skip = set([ + 'project_root_not_repo_root.svndump', + 'corrupt.svndump', +]) + +attrs = {'_do_case': _do_case, + '_run_assertions': _run_assertions, + } +for case in [f for f in os.listdir(test_util.FIXTURES) if f.endswith('.svndump')]: + # this fixture results in an empty repository, don't use it + if case in skip: + continue + bname = 'test_' + case[:-len('.svndump')] + attrs[bname] = test_rebuildmeta.buildmethod(case, bname, False, False) + name = bname + '_stupid' + attrs[name] = test_rebuildmeta.buildmethod(case, name, True, False) + name = bname + '_single' + attrs[name] = test_rebuildmeta.buildmethod(case, name, False, True) + +UpdateMetaTests = type('UpdateMetaTests', (test_util.TestBase,), attrs) + + +def suite(): + all_tests = [unittest.TestLoader().loadTestsFromTestCase(UpdateMetaTests), + ] + return unittest.TestSuite(all_tests) diff -Nru hgsubversion-1.4/tests/test_util.py hgsubversion-1.5/tests/test_util.py --- hgsubversion-1.4/tests/test_util.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_util.py 2012-10-29 11:16:19.000000000 +0000 @@ -7,6 +7,7 @@ import stat import subprocess import sys +import tarfile import tempfile import unittest import urllib @@ -37,6 +38,7 @@ SkipTest = None from hgsubversion import util +from hgsubversion import svnwrap # Documentation for Subprocess.Popen() says: # "Note that on Windows, you cannot set close_fds to true and @@ -96,11 +98,17 @@ 'project_name_with_space.svndump': '/project name', 'non_ascii_path_1.svndump': '/b\xC3\xB8b', 'non_ascii_path_2.svndump': '/b%C3%B8b', + 'subdir_is_file_prefix.svndump': '/flaf', } FIXTURES = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'fixtures') +def getlocalpeer(repo): + localrepo = getattr(repo, 'local', lambda: repo)() + if isinstance(localrepo, bool): + localrepo = repo + return localrepo def _makeskip(name, message): if SkipTest: @@ -143,7 +151,10 @@ path = os.path.abspath(path).replace(os.sep, '/') drive, path = os.path.splitdrive(path) if drive: - drive = '/' + drive + # In svn 1.7, the swig svn wrapper returns local svn URLs + # with an uppercase drive letter, try to match that to + # simplify svn info tests. + drive = '/' + drive.upper() url = 'file://%s%s' % (drive, path) return url @@ -158,11 +169,8 @@ return u def dispatch(cmd): - try: - req = dispatchmod.request(cmd) - dispatchmod.dispatch(req) - except AttributeError, e: - dispatchmod.dispatch(cmd) + cmd = getattr(dispatchmod, 'request', lambda x: x)(cmd) + dispatchmod.dispatch(cmd) def rmtree(path): # Read-only files cannot be removed under Windows @@ -198,12 +206,12 @@ 'from the wrong path!' ) -def hgclone(ui, source, dest, update=True): +def hgclone(ui, source, dest, update=True, rev=None): if getattr(hg, 'peer', None): # Since 1.9 (d976542986d2) - src, dest = hg.clone(ui, {}, source, dest, update=update) + src, dest = hg.clone(ui, {}, source, dest, update=update, rev=rev) else: - src, dest = hg.clone(ui, source, dest, update=update) + src, dest = hg.clone(ui, source, dest, update=update, rev=rev) return src, dest def svnls(repo_path, path, rev='HEAD'): @@ -236,6 +244,9 @@ def setUp(self): _verify_our_modules() + # the Python 2.7 default of 640 is obnoxiously low + self.maxDiff = 4096 + self.oldenv = dict([(k, os.environ.get(k, None),) for k in ('LANG', 'LC_ALL', 'HGRCPATH',)]) self.oldt = i18n.t @@ -255,6 +266,10 @@ self.wc_path = '%s/testrepo_wc' % self.tmpdir self.svn_wc = None + self.config_dir = self.tmpdir + svnwrap.common._svn_config_dir = self.config_dir + self.setup_svn_config('') + # 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 @@ -263,6 +278,10 @@ self.patch = (ui.ui.write_err, ui.ui.write) setattr(ui.ui, self.patch[0].func_name, self.patch[1]) + def setup_svn_config(self, config): + with open(self.config_dir + '/config', 'w') as c: + c.write(config) + def _makerepopath(self): self.repocount += 1 return '%s/testrepo-%d' % (self.tmpdir, self.repocount) @@ -297,15 +316,31 @@ proc.communicate() return path - def load_and_fetch(self, fixture_name, subdir=None, stupid=False, - layout='auto', startrev=0, externals=None, - noupdate=True): + def load_repo_tarball(self, fixture_name): + '''Extracts a tarball of an svn repo and returns the svn repo path.''' + path = self._makerepopath() + assert not os.path.exists(path) + os.mkdir(path) + tarball = tarfile.open(os.path.join(FIXTURES, fixture_name)) + # This is probably somewhat fragile, but I'm not sure how to + # do better in particular, I think it assumes that the tar + # entries are in the right order and that directories appear + # before their contents. This is a valid assummption for sane + # tarballs, from what I can tell. In particular, for a simple + # tarball of a svn repo with paths relative to the repo root, + # it seems to work + for entry in tarball: + tarball.extract(entry, path) + return path + + def fetch(self, repo_path, subdir=None, stupid=False, layout='auto', + startrev=0, externals=None, noupdate=True, dest=None, rev=None, + config=None): if layout == 'single': if subdir is None: subdir = 'trunk' elif subdir is None: subdir = '' - repo_path = self.load_svndump(fixture_name) projectpath = repo_path if subdir: projectpath += '/' + subdir @@ -321,12 +356,27 @@ cmd.append('--stupid') if noupdate: cmd.append('--noupdate') + if rev is not None: + cmd.append('--rev=%s' % rev) + config = dict(config or {}) if externals: - cmd[:0] = ['--config', 'hgsubversion.externals=%s' % externals] + config['hgsubversion.externals'] = str(externals) + for k,v in reversed(sorted(config.iteritems())): + cmd[:0] = ['--config', '%s=%s' % (k, v)] dispatch(cmd) - return hg.repository(testui(), self.wc_path), repo_path + return hg.repository(testui(), self.wc_path) + + def load_and_fetch(self, fixture_name, *args, **opts): + if fixture_name.endswith('.svndump'): + repo_path = self.load_svndump(fixture_name) + elif fixture_name.endswith('tar.gz'): + repo_path = self.load_repo_tarball(fixture_name) + else: + assert False, 'Unknown fixture type' + + return self.fetch(repo_path, *args, **opts), repo_path def _load_fixture_and_fetch(self, *args, **kwargs): repo, repo_path = self.load_and_fetch(*args, **kwargs) @@ -470,7 +520,7 @@ msg = '%s\n%s' % (msg or '', diff) raise self.failureException, msg - def draw(self, repo): + def getgraph(self, repo): """Helper function displaying a repository graph, especially useful when debugging comprehensive tests. """ @@ -487,4 +537,10 @@ files: {files} """ + _ui.pushbuffer() graphlog.graphlog(_ui, repo, rev=None, template=templ) + return _ui.popbuffer() + + def draw(self, repo): + sys.stdout.write(self.getgraph(repo)) + diff -Nru hgsubversion-1.4/tests/test_utility_commands.py hgsubversion-1.5/tests/test_utility_commands.py --- hgsubversion-1.4/tests/test_utility_commands.py 2012-04-26 14:54:03.000000000 +0000 +++ hgsubversion-1.5/tests/test_utility_commands.py 2012-10-29 11:16:19.000000000 +0000 @@ -14,6 +14,7 @@ from hgsubversion import util from hgsubversion import svncommands +from hgsubversion import verify from hgsubversion import wrappers expected_info_output = '''URL: %(repourl)s/%(branch)s @@ -66,6 +67,23 @@ 'rev': 5, }) self.assertMultiLineEqual(actual, expected) + destpath = self.wc_path + '_clone' + test_util.hgclone(u, self.repo, destpath) + repo2 = hg.repository(u, destpath) + repo2.ui.setconfig('paths', 'default-push', + self.repo.ui.config('paths', 'default')) + hg.update(repo2, 'default') + svncommands.rebuildmeta(u, repo2, []) + u.pushbuffer() + svncommands.info(u, repo2) + actual = u.popbuffer() + expected = (expected_info_output % + {'date': '2008-10-08 01:39:29 +0000 (Wed, 08 Oct 2008)', + 'repourl': repourl(repo_path), + 'branch': 'trunk', + 'rev': 6, + }) + self.assertMultiLineEqual(actual, expected) def test_info_single(self): repo, repo_path = self.load_and_fetch('two_heads.svndump', subdir='trunk') @@ -245,28 +263,82 @@ authors=author_path) self.assertMultiLineEqual(open(author_path).read(), 'Augie=\nevil=\n') - def test_svnverify(self): + def test_svnverify(self, stupid=False): repo, repo_path = self.load_and_fetch('binaryfiles.svndump', - noupdate=False) - ret = svncommands.verify(self.ui(), repo, [], rev=1) + noupdate=False, stupid=stupid) + ret = verify.verify(self.ui(), repo, [], rev=1, stupid=stupid) self.assertEqual(0, ret) repo_path = self.load_svndump('binaryfiles-broken.svndump') u = self.ui() u.pushbuffer() - ret = svncommands.verify(u, repo, [test_util.fileurl(repo_path)], - rev=1) + ret = verify.verify(u, repo, [test_util.fileurl(repo_path)], + rev=1, stupid=stupid) output = u.popbuffer() self.assertEqual(1, ret) output = re.sub(r'file://\S+', 'file://', output) - self.assertEqual("""\ + self.assertMultiLineEqual("""\ verifying d51f46a715a1 against file:// -difference in file binary2 -unexpected files: - binary1 -missing files: - binary3 +difference in: binary2 +unexpected file: binary1 +missing file: binary3 """, output) + def test_svnverify_stupid(self): + self.test_svnverify(True) + + def test_corruption(self, stupid=False): + SUCCESS = 0 + FAILURE = 1 + + repo, repo_path = self.load_and_fetch('correct.svndump', layout='single', + subdir='', stupid=stupid) + + ui = self.ui() + + self.assertEqual(SUCCESS, verify.verify(ui, self.repo, rev='tip', + stupid=stupid)) + + corrupt_source = test_util.fileurl(self.load_svndump('corrupt.svndump')) + + repo.ui.setconfig('paths', 'default', corrupt_source) + + ui.pushbuffer() + code = verify.verify(ui, repo, rev='tip') + actual = ui.popbuffer() + + actual = actual.replace(corrupt_source, '$REPO') + actual = set(actual.splitlines()) + + expected = set([ + 'verifying 78e965230a13 against $REPO@1', + 'missing file: missing-file', + 'wrong flags for: executable-file', + 'wrong flags for: symlink', + 'wrong flags for: regular-file', + 'difference in: another-regular-file', + 'difference in: regular-file', + 'unexpected file: empty-file', + ]) + + self.assertEqual((FAILURE, expected), (code, actual)) + + def test_corruption_stupid(self): + self.test_corruption(True) + + def test_svnrebuildmeta(self): + otherpath = self.load_svndump('binaryfiles-broken.svndump') + otherurl = test_util.fileurl(otherpath) + self.load_and_fetch('replace_trunk_with_branch.svndump') + # rebuildmeta with original repo + svncommands.rebuildmeta(self.ui(), repo=self.repo, args=[]) + # rebuildmeta with unrelated repo + self.assertRaises(hgutil.Abort, + svncommands.rebuildmeta, + self.ui(), repo=self.repo, args=[otherurl]) + # rebuildmeta --unsafe-skip-uuid-check with unrelated repo + svncommands.rebuildmeta(self.ui(), repo=self.repo, args=[otherurl], + unsafe_skip_uuid_check=True) + def suite(): all_tests = [unittest.TestLoader().loadTestsFromTestCase(UtilityTests), ]