Mercurial > hgsubversion
diff hgsubversion/svnmeta.py @ 414:343da842dbe6
split parts of HgChangeReceiver out into an SVNMeta class
Less obvious things:
- my reordering in the previous was incomplete
- _branch_for_path() was unused, so I removed it
- _svnpath() was removed in favor of identical _remotename()
- I've checked "no cover" bits manually
author | Dirkjan Ochtman <dirkjan@ochtman.nl> |
---|---|
date | Thu, 11 Jun 2009 18:56:35 +0200 |
parents | hgsubversion/hg_delta_editor.py@ac0cc3c9ea63 |
children | b17b2969861c |
line wrap: on
line diff
copy from hgsubversion/hg_delta_editor.py copy to hgsubversion/svnmeta.py --- a/hgsubversion/hg_delta_editor.py +++ b/hgsubversion/svnmeta.py @@ -1,33 +1,15 @@ -import cStringIO import cPickle as pickle import os -import sys import tempfile -import traceback from mercurial import context -from mercurial import hg -from mercurial import ui from mercurial import util as hgutil from mercurial import revlog from mercurial import node -from mercurial import error -from svn import delta -from svn import core -import svnexternals import util import maps -class MissingPlainTextError(Exception): - """Exception raised when the repo lacks a source file required for replaying - a txdelta. - """ - -class ReplayException(Exception): - """Exception raised when you try and commit but the replay encountered an - exception. - """ def pickle_atomic(data, file_path, dir=None): """pickle some data to a path atomically. @@ -45,55 +27,8 @@ def pickle_atomic(data, file_path, dir=N else: hgutil.rename(path, file_path) -def ieditor(fn): - """Helps identify methods used by the SVN editor interface. - - Stash any exception raised in the method on self. - - This is required because the SWIG bindings just mutate any exception into - a generic Subversion exception with no way of telling what the original was. - This allows the editor object to notice when you try and commit and really - got an exception in the replay process. - """ - def fun(self, *args, **kwargs): - try: - return fn(self, *args, **kwargs) - except: #pragma: no cover - if not hasattr(self, '_exception_info'): - self._exception_info = sys.exc_info() - raise - return fun - - -class RevisionData(object): - - __slots__ = [ - 'file', 'files', 'deleted', 'rev', 'execfiles', 'symlinks', 'batons', - 'copies', 'missing', 'emptybranches', 'base', 'closebranches', - 'externals', - ] - - def __init__(self): - self.clear() - - def clear(self): - self.file = None - self.files = {} - 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.closebranches = set() - self.externals = {} - -class HgChangeReceiver(delta.Editor): +class SVNMeta(object): def __init__(self, repo, uuid=None, subdir=''): """path is the path to the target hg repo. @@ -139,7 +74,6 @@ class HgChangeReceiver(delta.Editor): self.tag_locations.sort() self.tag_locations.reverse() - self.current = RevisionData() self.authors = maps.AuthorMap(self.ui, self.authors_file, defaulthost=author_host) if authors: self.authors.load(authors) @@ -304,6 +238,20 @@ class HgChangeReceiver(delta.Editor): return None, None, None return path, ln, test + def _path_and_branch_for_path(self, path, existing=True): + return self._split_branch_path(path, existing=existing)[:2] + + def _determine_parent_branch(self, p, src_path, src_rev, revnum): + if src_path is not None: + src_file, src_branch = self._path_and_branch_for_path(src_path) + src_tag = self._is_path_tag(src_path) + if src_tag != False or src_file == '': # case 2 + ln = self._localname(p) + if src_tag != False: + src_branch, src_rev = self.tags[src_tag] + return {ln: (src_branch, src_rev, revnum)} + return {} + def _is_path_valid(self, path): if path is None: return False @@ -355,7 +303,7 @@ class HgChangeReceiver(delta.Editor): paths = revision.paths added_branches = {} added_tags = {} - self.current.closebranches = set() + self.closebranches = set() tags_to_delete = set() for p in sorted(paths): t_name = self._is_path_tag(p) @@ -408,7 +356,7 @@ class HgChangeReceiver(delta.Editor): if fi is not None: if fi == '': if paths[p].action == 'D': - self.current.closebranches.add(br) # case 4 + self.closebranches.add(br) # case 4 elif paths[p].action == 'R': parent = self._determine_parent_branch( p, paths[p].copyfrom_path, paths[p].copyfrom_rev, @@ -417,7 +365,7 @@ class HgChangeReceiver(delta.Editor): continue # case 1 if paths[p].action == 'D': for known in self.branches: - if self._svnpath(known).startswith(p): + if self._remotename(known).startswith(p): self.current.closebranches.add(known) # case 5 parent = self._determine_parent_branch( p, paths[p].copyfrom_path, paths[p].copyfrom_rev, revision.revnum) @@ -431,7 +379,7 @@ class HgChangeReceiver(delta.Editor): rmtags = dict((t, self.tags[t][0]) for t in tags_to_delete) return { 'tags': (added_tags, rmtags), - 'branches': (added_branches, self.current.closebranches), + 'branches': (added_branches, self.closebranches), } def save_tbdelta(self, tbdelta): @@ -514,449 +462,3 @@ class HgChangeReceiver(delta.Editor): extra) new = self.repo.commitctx(ctx) self.ui.status('Marked branch %s as closed.\n' % (branch or 'default')) - - def set_file(self, path, data, isexec=False, islink=False): - if islink: - data = 'link ' + data - self.current.files[path] = data - self.current.execfiles[path] = isexec - self.current.symlinks[path] = islink - if path in self.current.deleted: - del self.current.deleted[path] - if path in self.current.missing: - self.current.missing.remove(path) - - def delete_file(self, path): - self.current.deleted[path] = True - if path in self.current.files: - del self.current.files[path] - self.current.execfiles[path] = False - self.current.symlinks[path] = False - self.ui.note('D %s\n' % path) - - def _svnpath(self, branch): - """Return the relative path in svn of branch. - """ - if branch == None or branch == 'default': - return 'trunk' - elif branch.startswith('../'): - return branch[3:] - return 'branches/%s' % branch - - def _path_and_branch_for_path(self, path, existing=True): - return self._split_branch_path(path, existing=existing)[:2] - - def _branch_for_path(self, path, existing=True): - return self._path_and_branch_for_path(path, existing=existing)[1] - - def _determine_parent_branch(self, p, src_path, src_rev, revnum): - if src_path is not None: - src_file, src_branch = self._path_and_branch_for_path(src_path) - src_tag = self._is_path_tag(src_path) - if src_tag != False: - # also case 2 - src_branch, src_rev = self.tags[src_tag] - return {self._localname(p): (src_branch, src_rev, revnum )} - if src_file == '': - # case 2 - return {self._localname(p): (src_branch, src_rev, revnum )} - return {} - - def _updateexternals(self): - if not self.current.externals: - return - # Accumulate externals records for all branches - revnum = self.current.rev.revnum - branches = {} - for path, entry in self.current.externals.iteritems(): - if not self._is_path_valid(path): - self.ui.warn('WARNING: Invalid path %s in externals\n' % path) - continue - p, b, bp = self._split_branch_path(path) - if bp not in branches: - external = svnexternals.externalsfile() - parent = self.get_parent_revision(revnum, b) - pctx = self.repo[parent] - if '.hgsvnexternals' in pctx: - external.read(pctx['.hgsvnexternals'].data()) - branches[bp] = external - else: - external = branches[bp] - external[p] = entry - - # Register the file changes - for bp, external in branches.iteritems(): - path = bp + '/.hgsvnexternals' - if external: - self.set_file(path, external.write(), False, False) - else: - self.delete_file(path) - - def commit_current_delta(self, tbdelta): - if hasattr(self, '_exception_info'): #pragma: no cover - traceback.print_exception(*self._exception_info) - raise ReplayException() - if self.current.missing: - raise MissingPlainTextError() - self._updateexternals() - # paranoidly generate the list of files to commit - files_to_commit = set(self.current.files.keys()) - files_to_commit.update(self.current.symlinks.keys()) - files_to_commit.update(self.current.execfiles.keys()) - files_to_commit.update(self.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() - branch_batches = {} - rev = self.current.rev - date = self.fixdate(rev.date) - - # build up the branches that have files on them - for f in files_to_commit: - if not self._is_path_valid(f): - continue - p, b = self._path_and_branch_for_path(f) - if b not in branch_batches: - branch_batches[b] = [] - branch_batches[b].append((p, f)) - - closebranches = {} - for branch in tbdelta['branches'][1]: - branchedits = self.branchedits(branch, rev) - if len(branchedits) < 1: - # can't close a branch that never existed - continue - ha = branchedits[0][1] - closebranches[branch] = ha - - # 1. handle normal commits - closedrevs = closebranches.values() - for branch, files in branch_batches.iteritems(): - if branch in self.current.emptybranches and files: - del self.current.emptybranches[branch] - files = dict(files) - - parents = (self.get_parent_revision(rev.revnum, branch), - revlog.nullid) - if parents[0] in closedrevs and branch in self.current.closebranches: - continue - extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir) - if branch is not None: - if (branch not in self.branches - and branch not in self.repo.branchtags()): - continue - parent_ctx = self.repo.changectx(parents[0]) - if '.hgsvnexternals' not in parent_ctx and '.hgsvnexternals' in files: - # Do not register empty externals files - if (files['.hgsvnexternals'] in self.current.files - and not self.current.files[files['.hgsvnexternals']]): - del files['.hgsvnexternals'] - - def filectxfn(repo, memctx, path): - current_file = files[path] - if current_file in self.current.deleted: - raise IOError() - copied = self.current.copies.get(current_file) - flags = parent_ctx.flags(path) - is_exec = self.current.execfiles.get(current_file, 'x' in flags) - is_link = self.current.symlinks.get(current_file, 'l' in flags) - if current_file in self.current.files: - data = self.current.files[current_file] - if is_link and data.startswith('link '): - data = data[len('link '):] - elif is_link: - self.ui.warn('file marked as link, but contains data: ' - '%s (%r)\n' % (current_file, flags)) - else: - data = parent_ctx.filectx(path).data() - return context.memfilectx(path=path, - data=data, - islink=is_link, isexec=is_exec, - copied=copied) - if not self.usebranchnames: - extra.pop('branch', None) - current_ctx = context.memctx(self.repo, - parents, - rev.message or '...', - files.keys(), - filectxfn, - self.authors[rev.author], - date, - extra) - new_hash = self.repo.commitctx(current_ctx) - util.describe_commit(self.ui, new_hash, branch) - if (rev.revnum, branch) not in self.revmap: - self.revmap[rev.revnum, branch] = new_hash - - # 2. handle branches that need to be committed without any files - for branch in self.current.emptybranches: - ha = self.get_parent_revision(rev.revnum, branch) - if ha == node.nullid: - continue - parent_ctx = self.repo.changectx(ha) - def del_all_files(*args): - raise IOError - # True here meant nuke all files, shouldn't happen with branch closing - if self.current.emptybranches[branch]: #pragma: no cover - raise hgutil.Abort('Empty commit to an open branch attempted. ' - 'Please report this issue.') - extra = util.build_extra(rev.revnum, branch, self.uuid, self.subdir) - if not self.usebranchnames: - extra.pop('branch', None) - current_ctx = context.memctx(self.repo, - (ha, node.nullid), - rev.message or ' ', - [], - del_all_files, - self.authors[rev.author], - date, - extra) - new_hash = self.repo.commitctx(current_ctx) - util.describe_commit(self.ui, new_hash, branch) - if (rev.revnum, branch) not in self.revmap: - self.revmap[rev.revnum, branch] = new_hash - - # 3. handle tags - if tbdelta['tags'][0] or tbdelta['tags'][1]: - self.committags(tbdelta['tags'], rev, closebranches) - - # 4. close any branches that need it - for branch, parent in closebranches.iteritems(): - if parent is None: - continue - self.delbranch(branch, parent, rev) - - self._save_metadata() - self.current.clear() - - # Here come all the actual editor methods - - @ieditor - def delete_entry(self, path, revision_bogus, parent_baton, pool=None): - br_path, branch = self._path_and_branch_for_path(path) - if br_path == '': - self.current.closebranches.add(branch) - if br_path is not None: - ha = self.get_parent_revision(self.current.rev.revnum, branch) - if ha == revlog.nullid: - return - ctx = self.repo.changectx(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.delete_file, [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.delete_file(f_p) - self.delete_file(path) - - @ieditor - def open_file(self, path, parent_baton, base_revision, p=None): - self.current.file = None - fpath, branch = self._path_and_branch_for_path(path) - 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 - - baserev = base_revision - if baserev is None or baserev == -1: - baserev = self.current.rev.revnum - 1 - parent = self.get_parent_revision(baserev + 1, branch) - - ctx = self.repo[parent] - if not self._is_path_valid(path): - return - - if fpath not in ctx: - self.current.missing.add(path) - - fctx = ctx.filectx(fpath) - base = fctx.data() - if 'l' in fctx.flags(): - base = 'link ' + base - self.set_file(path, base, 'x' in fctx.flags(), 'l' in fctx.flags()) - - @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] - fpath, branch = self._path_and_branch_for_path(path, existing=False) - if not fpath: - return - if branch not in self.branches: - # we know this branch will exist now, because it has at least one file. Rock. - self.branches[branch] = None, 0, self.current.rev.revnum - self.current.file = path - if not copyfrom_path: - self.ui.note('A %s\n' % path) - self.set_file(path, '', False, False) - return - self.ui.note('A+ %s\n' % path) - (from_file, - from_branch) = self._path_and_branch_for_path(copyfrom_path) - if not from_file: - self.current.missing.add(path) - return - ha = self.get_parent_revision(copyfrom_revision + 1, - from_branch) - ctx = self.repo.changectx(ha) - if from_file in ctx: - fctx = ctx.filectx(from_file) - flags = fctx.flags() - self.set_file(path, fctx.data(), 'x' in flags, 'l' in flags) - if from_branch == branch: - parentid = self.get_parent_revision(self.current.rev.revnum, - branch) - if parentid != revlog.nullid: - parentctx = self.repo.changectx(parentid) - if util.aresamefiles(parentctx, ctx, [from_file]): - self.current.copies[path] = from_file - - @ieditor - def add_directory(self, path, parent_baton, copyfrom_path, - copyfrom_revision, dir_pool=None): - self.current.batons[path] = path - br_path, branch = self._path_and_branch_for_path(path) - if br_path is not None: - if not copyfrom_path and not br_path: - self.current.emptybranches[branch] = True - else: - self.current.emptybranches[branch] = False - if br_path is None or not copyfrom_path: - return path - if copyfrom_path: - tag = self._is_path_tag(copyfrom_path) - if tag not in self.tags: - tag = None - if not self._is_path_valid(copyfrom_path) and not tag: - self.current.missing.add('%s/' % path) - return path - if tag: - source_branch, source_rev = self.tags[tag] - cp_f = '' - else: - source_rev = copyfrom_revision - cp_f, source_branch = self._path_and_branch_for_path(copyfrom_path) - if cp_f == '' and br_path == '': - assert br_path is not None - self.branches[branch] = source_branch, source_rev, self.current.rev.revnum - new_hash = self.get_parent_revision(source_rev + 1, - source_branch) - if new_hash == node.nullid: - self.current.missing.add('%s/' % path) - return path - cp_f_ctx = self.repo.changectx(new_hash) - if cp_f != '/' and cp_f != '': - cp_f = '%s/' % cp_f - else: - cp_f = '' - copies = {} - for f in cp_f_ctx: - if not f.startswith(cp_f): - continue - f2 = f[len(cp_f):] - fctx = cp_f_ctx.filectx(f) - fp_c = path + '/' + f2 - self.set_file(fp_c, fctx.data(), 'x' in fctx.flags(), 'l' in fctx.flags()) - if fp_c in self.current.deleted: - del self.current.deleted[fp_c] - if branch == source_branch: - copies[fp_c] = 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.get_parent_revision(self.current.rev.revnum, branch) - if parentid != revlog.nullid: - parentctx = self.repo.changectx(parentid) - if util.aresamefiles(parentctx, cp_f_ctx, copies.values()): - self.current.copies.update(copies) - return path - - @ieditor - def change_file_prop(self, file_baton, name, value, pool=None): - if name == 'svn:executable': - self.current.execfiles[self.current.file] = bool(value is not None) - elif name == 'svn:special': - self.current.symlinks[self.current.file] = bool(value is not None) - - @ieditor - def change_dir_prop(self, dir_baton, name, value, pool=None): - if dir_baton is None: - return - path = self.current.batons[dir_baton] - if name == 'svn:externals': - self.current.externals[path] = value - - @ieditor - def open_directory(self, path, parent_baton, base_revision, dir_pool=None): - self.current.batons[path] = path - p_, branch = self._path_and_branch_for_path(path) - if p_ == '': - self.current.emptybranches[branch] = False - return path - - @ieditor - def close_directory(self, dir_baton, dir_pool=None): - if dir_baton is not None: - del self.current.batons[dir_baton] - - @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._is_path_valid(self.current.file): - return lambda x: None - assert self.current.file not in self.current.deleted, ( - 'Cannot apply_textdelta to a deleted file: %s' % self.current.file) - assert (self.current.file in self.current.files - or self.current.file in self.current.missing), '%s not found' % self.current.file - if self.current.file in self.current.missing: - return lambda x: None - base = self.current.files[self.current.file] - source = cStringIO.StringIO(base) - target = cStringIO.StringIO() - self.stream = target - - handler, baton = delta.svn_txdelta_apply(source, target, None) - if not callable(handler): #pragma: no cover - raise hgutil.Abort('Error in Subversion bindings: ' - 'cannot call handler!') - def txdelt_window(window): - try: - if not self._is_path_valid(self.current.file): - return - handler(window, baton) - # window being None means commit this file - if not window: - self.current.files[self.current.file] = target.getvalue() - except core.SubversionException, e: #pragma: no cover - if e.apr_err == core.SVN_ERR_INCOMPLETE_DATA: - self.current.missing.add(self.current.file) - 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