Mercurial > hgsubversion
diff cmdutil.py @ 304:ce676eff002b
First merge, totally untested.
author | Dan Villiom Podlaski Christiansen <danchr@gmail.com> |
---|---|
date | Fri, 01 May 2009 10:28:59 +0200 |
parents | 75d4fde9aa2e |
children | b6a9cdee2f68 |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/cmdutil.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +import re +import os +import urllib + +from mercurial import util as hgutil + +from svn import core + +import util +import svnwrap +import svnexternals + + +b_re = re.compile(r'^\+\+\+ b\/([^\n]*)', re.MULTILINE) +a_re = re.compile(r'^--- a\/([^\n]*)', re.MULTILINE) +devnull_re = re.compile(r'^([-+]{3}) /dev/null', re.MULTILINE) +header_re = re.compile(r'^diff --git .* b\/(.*)', re.MULTILINE) +newfile_devnull_re = re.compile(r'^--- /dev/null\n\+\+\+ b/([^\n]*)', + re.MULTILINE) + + +class NoFilesException(Exception): + """Exception raised when you try and commit without files. + """ + +def formatrev(rev): + if rev == -1: + return '\t(working copy)' + return '\t(revision %d)' % rev + + +def filterdiff(diff, oldrev, newrev): + diff = newfile_devnull_re.sub(r'--- \1\t(revision 0)' '\n' + r'+++ \1\t(working copy)', + diff) + oldrev = formatrev(oldrev) + newrev = formatrev(newrev) + diff = a_re.sub(r'--- \1'+ oldrev, diff) + diff = b_re.sub(r'+++ \1' + newrev, diff) + diff = devnull_re.sub(r'\1 /dev/null\t(working copy)', diff) + diff = header_re.sub(r'Index: \1' + '\n' + ('=' * 67), diff) + return diff + + +def parentrev(ui, repo, hge, svn_commit_hashes): + """Find the svn parent revision of the repo's dirstate. + """ + workingctx = repo.parents()[0] + outrev = util.outgoing_revisions(ui, repo, hge, svn_commit_hashes, + workingctx.node()) + if outrev: + workingctx = repo[outrev[-1]].parents()[0] + return workingctx + + +def replay_convert_rev(hg_editor, svn, r): + hg_editor.set_current_rev(r) + svn.get_replay(r.revnum, hg_editor) + i = 1 + if hg_editor.missing_plaintexts: + hg_editor.ui.debug('Fetching %s files that could not use replay.\n' % + len(hg_editor.missing_plaintexts)) + files_to_grab = set() + rootpath = svn.subdir and svn.subdir[1:] or '' + for p in hg_editor.missing_plaintexts: + hg_editor.ui.note('.') + hg_editor.ui.flush() + if p[-1] == '/': + dirpath = p[len(rootpath):] + files_to_grab.update([dirpath + f for f,k in + svn.list_files(dirpath, r.revnum) + if k == 'f']) + else: + files_to_grab.add(p[len(rootpath):]) + hg_editor.ui.note('\nFetching files...\n') + for p in files_to_grab: + hg_editor.ui.note('.') + hg_editor.ui.flush() + if i % 50 == 0: + svn.init_ra_and_client() + i += 1 + data, mode = svn.get_file(p, r.revnum) + hg_editor.set_file(p, data, 'x' in mode, 'l' in mode) + hg_editor.missing_plaintexts = set() + hg_editor.ui.note('\n') + hg_editor.commit_current_delta() + + +def _isdir(svn, branchpath, svndir): + try: + svn.list_dir('%s/%s' % (branchpath, svndir)) + return True + except core.SubversionException: + return False + + +def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges): + """Compute directories to add or delete when moving from parentctx + to ctx, assuming only 'changedfiles' files changed, and 'extchanges' + external references changed (as returned by svnexternals.diff()). + + Return (added, deleted) where 'added' is the list of all added + directories and 'deleted' the list of deleted directories. + Intermediate directories are included: if a/b/c is new and requires + the addition of a/b and a, those will be listed too. Intermediate + deleted directories are also listed, but item order of undefined + in either list. + """ + def finddirs(path, includeself=False): + if includeself: + yield path + pos = path.rfind('/') + while pos != -1: + yield path[:pos] + pos = path.rfind('/', 0, pos) + + def getctxdirs(ctx, keptdirs, extdirs): + dirs = {} + for f in ctx.manifest(): + for d in finddirs(f): + if d in dirs: + break + if d in keptdirs: + dirs[d] = 1 + for extdir in extdirs: + for d in finddirs(extdir, True): + dirs[d] = 1 + return dirs + + deleted, added = [], [] + changeddirs = {} + for f in changedfiles: + if f in parentctx and f in ctx: + # Updated files cannot cause directories to be created + # or removed. + continue + for d in finddirs(f): + changeddirs[d] = 1 + for e in extchanges: + if not e[1] or not e[2]: + for d in finddirs(e[0], True): + changeddirs[d] = 1 + if not changeddirs: + return added, deleted + olddirs = getctxdirs(parentctx, changeddirs, + [e[0] for e in extchanges if e[1]]) + newdirs = getctxdirs(ctx, changeddirs, + [e[0] for e in extchanges if e[2]]) + + for d in newdirs: + if d not in olddirs and not _isdir(svn, branchpath, d): + added.append(d) + + for d in olddirs: + if d not in newdirs and _isdir(svn, branchpath, d): + deleted.append(d) + + return added, deleted + + +def _externals(ctx): + ext = svnexternals.externalsfile() + if '.hgsvnexternals' in ctx: + ext.read(ctx['.hgsvnexternals'].data()) + return ext + + +def commit_from_rev(ui, repo, rev_ctx, hg_editor, svn_url, base_revision, + username, password): + """Build and send a commit from Mercurial to Subversion. + """ + file_data = {} + svn = svnwrap.SubversionRepo(svn_url, username, password) + parent = rev_ctx.parents()[0] + parent_branch = rev_ctx.parents()[0].branch() + branch_path = 'trunk' + + if parent_branch and parent_branch != 'default': + branch_path = 'branches/%s' % parent_branch + + extchanges = list(svnexternals.diff(_externals(parent), + _externals(rev_ctx))) + addeddirs, deleteddirs = _getdirchanges(svn, branch_path, parent, rev_ctx, + rev_ctx.files(), extchanges) + deleteddirs = set(deleteddirs) + + props = {} + copies = {} + for file in rev_ctx.files(): + if file == '.hgsvnexternals': + continue + new_data = base_data = '' + action = '' + if file in rev_ctx: + fctx = rev_ctx.filectx(file) + new_data = fctx.data() + + if 'x' in fctx.flags(): + props.setdefault(file, {})['svn:executable'] = '*' + if 'l' in fctx.flags(): + props.setdefault(file, {})['svn:special'] = '*' + + if file not in parent: + renamed = fctx.renamed() + if renamed: + # TODO current model (and perhaps svn model) does not support + # this kind of renames: a -> b, b -> c + copies[file] = renamed[0] + base_data = parent[renamed[0]].data() + + action = 'add' + dirname = '/'.join(file.split('/')[:-1] + ['']) + else: + base_data = parent.filectx(file).data() + if ('x' in parent.filectx(file).flags() + and 'x' not in rev_ctx.filectx(file).flags()): + props.setdefault(file, {})['svn:executable'] = None + if ('l' in parent.filectx(file).flags() + and 'l' not in rev_ctx.filectx(file).flags()): + props.setdefault(file, {})['svn:special'] = None + action = 'modify' + else: + pos = file.rfind('/') + if pos >= 0: + if file[:pos] in deleteddirs: + # This file will be removed when its directory is removed + continue + action = 'delete' + file_data[file] = base_data, new_data, action + + def svnpath(p): + return '%s/%s' % (branch_path, p) + + changeddirs = [] + for d, v1, v2 in extchanges: + props.setdefault(svnpath(d), {})['svn:externals'] = v2 + if d not in deleteddirs and d not in addeddirs: + changeddirs.append(svnpath(d)) + + # Now we are done with files, we can prune deleted directories + # against themselves: ignore a/b if a/ is already removed + deleteddirs2 = list(deleteddirs) + deleteddirs2.sort(reverse=True) + for d in deleteddirs2: + pos = d.rfind('/') + if pos >= 0 and d[:pos] in deleteddirs: + deleteddirs.remove(d[:pos]) + + newcopies = {} + for source, dest in copies.iteritems(): + newcopies[svnpath(source)] = (svnpath(dest), base_revision) + + new_target_files = [svnpath(f) for f in file_data] + for tf, ntf in zip(file_data, new_target_files): + if tf in file_data: + file_data[ntf] = file_data[tf] + if tf in props: + props[ntf] = props[tf] + del props[tf] + if hgutil.binary(file_data[ntf][1]): + props.setdefault(ntf, {}).update(props.get(ntf, {})) + props.setdefault(ntf, {})['svn:mime-type'] = 'application/octet-stream' + del file_data[tf] + + addeddirs = [svnpath(d) for d in addeddirs] + deleteddirs = [svnpath(d) for d in deleteddirs] + new_target_files += addeddirs + deleteddirs + changeddirs + if not new_target_files: + raise NoFilesException() + try: + svn.commit(new_target_files, rev_ctx.description(), file_data, + base_revision, set(addeddirs), set(deleteddirs), + props, newcopies) + except core.SubversionException, e: + if hasattr(e, 'apr_err') and (e.apr_err == core.SVN_ERR_FS_TXN_OUT_OF_DATE + or e.apr_err == core.SVN_ERR_FS_CONFLICT): + raise hgutil.Abort('Base text was out of date, maybe rebase?') + else: + raise + + return True + +def islocalrepo(url): + if not url.startswith('file:///'): + return False + if '#' in url.split('/')[-1]: # strip off #anchor + url = url[:url.rfind('#')] + path = url[len('file://'):] + path = urllib.url2pathname(path).replace(os.sep, '/') + while '/' in path: + if reduce(lambda x,y: x and y, + map(lambda p: os.path.exists(os.path.join(path, p)), + ('hooks', 'format', 'db', ))): + return True + path = path.rsplit('/', 1)[0] + return False + +def issvnurl(url): + return url.startswith('svn') or islocalrepo(url)