mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 01:07:15 +03:00
1086 lines
44 KiB
Python
1086 lines
44 KiB
Python
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 svn import delta
|
|
from svn import core
|
|
|
|
import svnexternals
|
|
import util
|
|
|
|
def pickle_atomic(data, file_path, dir=None):
|
|
"""pickle some data to a path atomically.
|
|
|
|
This is present because I kept corrupting my revmap by managing to hit ^C
|
|
during the pickle of that file.
|
|
"""
|
|
try:
|
|
f, path = tempfile.mkstemp(prefix='pickling', dir=dir)
|
|
f = os.fdopen(f, 'w')
|
|
pickle.dump(data, f)
|
|
f.close()
|
|
except: #pragma: no cover
|
|
raise
|
|
else:
|
|
hgutil.rename(path, file_path)
|
|
|
|
def stash_exception_on_self(fn):
|
|
"""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 HgChangeReceiver(delta.Editor):
|
|
def add_to_revmap(self, revnum, branch, node_hash):
|
|
f = open(self.revmap_file, 'a')
|
|
f.write(str(revnum) + ' ' + node.hex(node_hash) + ' ' + (branch or '') + '\n')
|
|
f.flush()
|
|
f.close()
|
|
self.revmap[revnum, branch] = node_hash
|
|
|
|
def last_known_revision(self):
|
|
"""Obtain the highest numbered -- i.e. latest -- revision known.
|
|
|
|
Currently, this function just iterates over the entire revision map
|
|
using the max() builtin. This may be slow for extremely large
|
|
repositories, but for now, it's fast enough.
|
|
"""
|
|
try:
|
|
return max(k[0] for k in self.revmap.iterkeys())
|
|
except ValueError:
|
|
return 0
|
|
|
|
def __init__(self, path=None, repo=None, ui_=None,
|
|
subdir='', author_host='',
|
|
tag_locations=['tags'],
|
|
authors=None,
|
|
filemap=None):
|
|
"""path is the path to the target hg repo.
|
|
|
|
subdir is the subdirectory of the edits *on the svn server*.
|
|
It is needed for stripping paths off in certain cases.
|
|
"""
|
|
if repo and repo.ui and not ui_:
|
|
ui_ = repo.ui
|
|
if not ui_:
|
|
ui_ = ui.ui()
|
|
self.ui = ui_
|
|
if repo:
|
|
self.repo = repo
|
|
self.path = os.path.normpath(os.path.join(self.repo.path, '..'))
|
|
elif path:
|
|
self.path = path
|
|
self.__setup_repo(path)
|
|
else: #pragma: no cover
|
|
raise TypeError("Expected either path or repo argument")
|
|
|
|
self.subdir = subdir
|
|
if self.subdir and self.subdir[0] == '/':
|
|
self.subdir = self.subdir[1:]
|
|
self.revmap = {}
|
|
if os.path.exists(self.revmap_file):
|
|
self.revmap = util.parse_revmap(self.revmap_file)
|
|
self.branches = {}
|
|
if os.path.exists(self.branch_info_file):
|
|
f = open(self.branch_info_file)
|
|
self.branches = pickle.load(f)
|
|
f.close()
|
|
self.tags = {}
|
|
if os.path.exists(self.tag_info_file):
|
|
f = open(self.tag_info_file)
|
|
self.tags = pickle.load(f)
|
|
f.close()
|
|
if os.path.exists(self.tag_locations_file):
|
|
f = open(self.tag_locations_file)
|
|
self.tag_locations = pickle.load(f)
|
|
f.close()
|
|
else:
|
|
self.tag_locations = tag_locations
|
|
pickle_atomic(self.tag_locations, self.tag_locations_file,
|
|
self.meta_data_dir)
|
|
|
|
self.clear_current_info()
|
|
self.author_host = author_host
|
|
self.authors = {}
|
|
if os.path.exists(self.authors_file):
|
|
self.readauthors(self.authors_file)
|
|
if authors and os.path.exists(authors):
|
|
self.readauthors(authors)
|
|
if self.authors:
|
|
self.writeauthors()
|
|
|
|
self.lastdate = '1970-01-01 00:00:00'
|
|
self.includepaths = {}
|
|
self.excludepaths = {}
|
|
if filemap and os.path.exists(filemap):
|
|
self.readfilemap(filemap)
|
|
|
|
def fixdate(self, date):
|
|
if date is not None:
|
|
date = date.replace('T', ' ').replace('Z', '').split('.')[0]
|
|
date += ' -0000'
|
|
self.lastdate = date
|
|
else:
|
|
date = self.lastdate
|
|
return date
|
|
|
|
def __setup_repo(self, repo_path):
|
|
"""Verify the repo is going to work out for us.
|
|
|
|
This method will fail an assertion if the repo exists but doesn't have
|
|
the Subversion metadata.
|
|
"""
|
|
if os.path.isdir(repo_path) and len(os.listdir(repo_path)):
|
|
self.repo = hg.repository(self.ui, repo_path)
|
|
assert os.path.isfile(self.revmap_file)
|
|
assert os.path.isfile(self.svn_url_file)
|
|
assert os.path.isfile(self.uuid_file)
|
|
else:
|
|
self.repo = hg.repository(self.ui, repo_path, create=True)
|
|
os.makedirs(os.path.dirname(self.uuid_file))
|
|
f = open(self.revmap_file, 'w')
|
|
f.write('%s\n' % util.REVMAP_FILE_VERSION)
|
|
f.flush()
|
|
f.close()
|
|
|
|
def clear_current_info(self):
|
|
'''Clear the info relevant to a replayed revision so that the next
|
|
revision can be replayed.
|
|
'''
|
|
# Map files to raw svn data (symlink prefix is preserved)
|
|
self.current_files = {}
|
|
self.deleted_files = {}
|
|
self.current_rev = None
|
|
self.current_files_exec = {}
|
|
self.current_files_symlink = {}
|
|
self.dir_batons = {}
|
|
# Map fully qualified destination file paths to module source path
|
|
self.copies = {}
|
|
self.missing_plaintexts = set()
|
|
self.commit_branches_empty = {}
|
|
self.base_revision = None
|
|
self.branches_to_delete = set()
|
|
self.externals = {}
|
|
|
|
def _save_metadata(self):
|
|
'''Save the Subversion metadata. This should really be called after
|
|
every revision is created.
|
|
'''
|
|
pickle_atomic(self.branches, self.branch_info_file, self.meta_data_dir)
|
|
pickle_atomic(self.tags, self.tag_info_file, self.meta_data_dir)
|
|
|
|
def branches_in_paths(self, paths, revnum, checkpath, listdir):
|
|
'''Given a list of paths, return mapping of all branches touched
|
|
to their branch path.
|
|
'''
|
|
branches = {}
|
|
paths_need_discovery = []
|
|
for p in paths:
|
|
relpath, branch, branchpath = self._split_branch_path(p)
|
|
if relpath is not None:
|
|
branches[branch] = branchpath
|
|
elif paths[p].action == 'D' and not self._is_path_tag(p):
|
|
ln = self._localname(p)
|
|
# must check in branches_to_delete as well, because this runs after we
|
|
# already updated the branch map
|
|
if ln in self.branches or ln in self.branches_to_delete:
|
|
branches[self._localname(p)] = p
|
|
else:
|
|
paths_need_discovery.append(p)
|
|
if paths_need_discovery:
|
|
paths_need_discovery = [(len(p), p) for p in paths_need_discovery]
|
|
paths_need_discovery.sort()
|
|
paths_need_discovery = [p[1] for p in paths_need_discovery]
|
|
actually_files = []
|
|
while paths_need_discovery:
|
|
p = paths_need_discovery.pop(0)
|
|
path_could_be_file = True
|
|
ind = 0
|
|
while ind < len(paths_need_discovery) and not paths_need_discovery:
|
|
if op.startswith(p):
|
|
path_could_be_file = False
|
|
ind += 1
|
|
if path_could_be_file:
|
|
if checkpath(p, revnum) == 'f':
|
|
actually_files.append(p)
|
|
# if there's a copyfrom_path and there were files inside that copyfrom,
|
|
# we need to detect those branches. It's a little thorny and slow, but
|
|
# seems to be the best option.
|
|
elif paths[p].copyfrom_path and not p.startswith('tags/'):
|
|
paths_need_discovery.extend(['%s/%s' % (p,x[0])
|
|
for x in listdir(p, revnum)
|
|
if x[1] == 'f'])
|
|
if actually_files:
|
|
filepaths = [p.split('/') for p in actually_files]
|
|
filepaths = [(len(p), p) for p in filepaths]
|
|
filepaths.sort()
|
|
filepaths = [p[1] for p in filepaths]
|
|
while filepaths:
|
|
path = filepaths.pop(0)
|
|
parentdir = '/'.join(path[:-1])
|
|
filepaths = [p for p in filepaths if not '/'.join(p).startswith(parentdir)]
|
|
branchpath = self._normalize_path(parentdir)
|
|
if branchpath.startswith('tags/'):
|
|
continue
|
|
branchname = self._localname(branchpath)
|
|
if branchpath.startswith('trunk/'):
|
|
branches[self._localname('trunk')] = 'trunk'
|
|
continue
|
|
if branchname and branchname.startswith('../'):
|
|
continue
|
|
branches[branchname] = branchpath
|
|
|
|
return branches
|
|
|
|
def _path_and_branch_for_path(self, path, existing=True):
|
|
return self._split_branch_path(path, existing=existing)[:2]
|
|
|
|
def _localname(self, path):
|
|
"""Compute the local name for a branch located at path.
|
|
"""
|
|
assert not path.startswith('tags/')
|
|
if path == 'trunk':
|
|
return None
|
|
elif path.startswith('branches/'):
|
|
return path[len('branches/'):]
|
|
return '../%s' % path
|
|
|
|
def _remotename(self, branch):
|
|
if branch == 'default' or branch is None:
|
|
return 'trunk'
|
|
elif branch.startswith('../'):
|
|
return branch[3:]
|
|
return 'branches/%s' % branch
|
|
|
|
def _split_branch_path(self, path, existing=True):
|
|
"""Figure out which branch inside our repo this path represents, and
|
|
also figure out which path inside that branch it is.
|
|
|
|
Returns a tuple of (path within branch, local branch name, server-side branch path).
|
|
|
|
If existing=True, will return None, None, None if the file isn't on some known
|
|
branch. If existing=False, then it will guess what the branch would be if it were
|
|
known.
|
|
"""
|
|
path = self._normalize_path(path)
|
|
if path.startswith('tags/'):
|
|
return None, None, None
|
|
test = ''
|
|
path_comps = path.split('/')
|
|
while self._localname(test) not in self.branches and len(path_comps):
|
|
if not test:
|
|
test = path_comps.pop(0)
|
|
else:
|
|
test += '/%s' % path_comps.pop(0)
|
|
if self._localname(test) in self.branches:
|
|
return path[len(test)+1:], self._localname(test), test
|
|
if existing:
|
|
return None, None, None
|
|
if path.startswith('trunk/'):
|
|
path = test.split('/')[1:]
|
|
test = 'trunk'
|
|
elif path.startswith('branches/'):
|
|
elts = path.split('/')
|
|
test = '/'.join(elts[:2])
|
|
path = '/'.join(elts[2:])
|
|
else:
|
|
path = test.split('/')[-1]
|
|
test = '/'.join(test.split('/')[:-1])
|
|
ln = self._localname(test)
|
|
if ln and ln.startswith('../'):
|
|
return None, None, None
|
|
return path, ln, test
|
|
|
|
def set_current_rev(self, rev):
|
|
"""Set the revision we're currently converting.
|
|
"""
|
|
self.current_rev = rev
|
|
|
|
def set_file(self, path, data, isexec=False, islink=False):
|
|
if islink:
|
|
data = 'link ' + data
|
|
self.current_files[path] = data
|
|
self.current_files_exec[path] = isexec
|
|
self.current_files_symlink[path] = islink
|
|
if path in self.deleted_files:
|
|
del self.deleted_files[path]
|
|
if path in self.missing_plaintexts:
|
|
self.missing_plaintexts.remove(path)
|
|
|
|
def delete_file(self, path):
|
|
self.deleted_files[path] = True
|
|
if path in self.current_files:
|
|
del self.current_files[path]
|
|
self.current_files_exec[path] = False
|
|
self.current_files_symlink[path] = False
|
|
self.ui.note('D %s\n' % path)
|
|
|
|
def _normalize_path(self, path):
|
|
'''Normalize a path to strip of leading slashes and our subdir if we
|
|
have one.
|
|
'''
|
|
if path and path[0] == '/':
|
|
path = path[1:]
|
|
if path and path.startswith(self.subdir):
|
|
path = path[len(self.subdir):]
|
|
if path and path[0] == '/':
|
|
path = path[1:]
|
|
return path
|
|
|
|
def _is_file_included(self, subpath):
|
|
def checkpathinmap(path, mapping):
|
|
def rpairs(name):
|
|
yield '.', name
|
|
e = len(name)
|
|
while e != -1:
|
|
yield name[:e], name[e+1:]
|
|
e = name.rfind('/', 0, e)
|
|
|
|
for pre, suf in rpairs(path):
|
|
try:
|
|
return mapping[pre]
|
|
except KeyError, err:
|
|
pass
|
|
return None
|
|
|
|
if len(self.includepaths) and len(subpath):
|
|
inc = checkpathinmap(subpath, self.includepaths)
|
|
else:
|
|
inc = subpath
|
|
if len(self.excludepaths) and len(subpath):
|
|
exc = checkpathinmap(subpath, self.excludepaths)
|
|
else:
|
|
exc = None
|
|
if inc is None or exc is not None:
|
|
return False
|
|
return True
|
|
|
|
def _is_path_valid(self, path):
|
|
if path is None:
|
|
return False
|
|
subpath = self._split_branch_path(path)[0]
|
|
if subpath is None:
|
|
return False
|
|
return self._is_file_included(subpath)
|
|
|
|
|
|
def _is_path_tag(self, path):
|
|
"""If path could represent the path to a tag, returns the potential tag name.
|
|
|
|
Note that it's only a tag if it was copied from the path '' in a branch (or tag)
|
|
we have, for our purposes.
|
|
|
|
Otherwise, returns False.
|
|
"""
|
|
path = self._normalize_path(path)
|
|
for tags_path in self.tag_locations:
|
|
if path and (path.startswith(tags_path) and
|
|
len(path) > len('%s/' % tags_path)):
|
|
return path[len(tags_path)+1:]
|
|
return False
|
|
|
|
def get_parent_svn_branch_and_rev(self, number, branch):
|
|
number -= 1
|
|
if (number, branch) in self.revmap:
|
|
return number, branch
|
|
real_num = 0
|
|
for num, br in self.revmap.iterkeys():
|
|
if br != branch:
|
|
continue
|
|
if num <= number and num > real_num:
|
|
real_num = num
|
|
if branch in self.branches:
|
|
parent_branch = self.branches[branch][0]
|
|
parent_branch_rev = self.branches[branch][1]
|
|
# check to see if this branch already existed and is the same
|
|
if parent_branch_rev < real_num:
|
|
return real_num, branch
|
|
# if that wasn't true, then this is the a new branch with the
|
|
# same name as some old deleted branch
|
|
if parent_branch_rev <= 0 and real_num == 0:
|
|
return None, None
|
|
branch_created_rev = self.branches[branch][2]
|
|
if parent_branch == 'trunk':
|
|
parent_branch = None
|
|
if branch_created_rev <= number+1 and branch != parent_branch:
|
|
return self.get_parent_svn_branch_and_rev(
|
|
parent_branch_rev+1,
|
|
parent_branch)
|
|
if real_num != 0:
|
|
return real_num, branch
|
|
return None, None
|
|
|
|
def get_parent_revision(self, number, branch):
|
|
'''Get the parent revision hash for a commit on a specific branch.
|
|
'''
|
|
r, br = self.get_parent_svn_branch_and_rev(number, branch)
|
|
if r is not None:
|
|
return self.revmap[r, br]
|
|
return revlog.nullid
|
|
|
|
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 __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 update_branch_tag_map_for_rev(self, revision):
|
|
paths = revision.paths
|
|
added_branches = {}
|
|
added_tags = {}
|
|
self.branches_to_delete = set()
|
|
tags_to_delete = set()
|
|
for p in sorted(paths):
|
|
t_name = self._is_path_tag(p)
|
|
if t_name != False:
|
|
src_p, src_rev = paths[p].copyfrom_path, paths[p].copyfrom_rev
|
|
# if you commit to a tag, I'm calling you stupid and ignoring
|
|
# you.
|
|
if src_p is not None and src_rev is not None:
|
|
file, branch = self._path_and_branch_for_path(src_p)
|
|
if file is None:
|
|
# some crazy people make tags from other tags
|
|
file = ''
|
|
from_tag = self._is_path_tag(src_p)
|
|
if not from_tag:
|
|
continue
|
|
branch, src_rev = self.tags[from_tag]
|
|
if t_name not in added_tags:
|
|
added_tags[t_name] = branch, src_rev
|
|
elif file and src_rev > added_tags[t_name][1]:
|
|
added_tags[t_name] = branch, src_rev
|
|
elif (paths[p].action == 'D' and p.endswith(t_name)
|
|
and t_name in self.tags):
|
|
tags_to_delete.add(t_name)
|
|
continue
|
|
# At this point we know the path is not a tag. In that case, we only care if it
|
|
# is the root of a new branch (in this function). This is determined by the
|
|
# following checks:
|
|
# 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 that means it is a copy from the root
|
|
# of some other branch?
|
|
# If yes, then we're done: this is a new branch, and we record the copyfrom in
|
|
# added_branches
|
|
# 3. Neither of the above. This could be a branch, but it might never work out for
|
|
# us. It's only ever a branch (as far as we're concerned) if it gets committed
|
|
# to, which we have to detect at file-write time anyway. So we do nothing here.
|
|
# 4. It's the root of an already-known branch, with an action of 'D'. We mark the
|
|
# branch as deleted.
|
|
# 5. It's the parent directory of one or more already-known branches, so we mark them
|
|
# as deleted.
|
|
# 6. It's a branch being replaced by another branch - the action will be 'R'.
|
|
fi, br = self._path_and_branch_for_path(p)
|
|
if fi is not None:
|
|
if fi == '':
|
|
if paths[p].action == 'D':
|
|
self.branches_to_delete.add(br) # case 4
|
|
elif paths[p].action == 'R':
|
|
added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path,
|
|
paths[p].copyfrom_rev,
|
|
revision.revnum))
|
|
continue # case 1
|
|
if paths[p].action == 'D':
|
|
# check for case 5
|
|
for known in self.branches:
|
|
if self._svnpath(known).startswith(p):
|
|
self.branches_to_delete.add(br) # case 5
|
|
added_branches.update(self.__determine_parent_branch(p, paths[p].copyfrom_path,
|
|
paths[p].copyfrom_rev, revision.revnum))
|
|
for t in tags_to_delete:
|
|
del self.tags[t]
|
|
for br in self.branches_to_delete:
|
|
del self.branches[br]
|
|
for t, info in added_tags.items():
|
|
self.ui.status('Tagged %s@%s as %s\n' %
|
|
(info[0] or 'trunk', info[1], t))
|
|
self.tags.update(added_tags)
|
|
self.branches.update(added_branches)
|
|
self._save_metadata()
|
|
|
|
def _updateexternals(self):
|
|
if not self.externals:
|
|
return
|
|
# Accumulate externals records for all branches
|
|
revnum = self.current_rev.revnum
|
|
branches = {}
|
|
for path, entry in self.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):
|
|
if hasattr(self, '_exception_info'): #pragma: no cover
|
|
traceback.print_exception(*self._exception_info)
|
|
raise ReplayException()
|
|
if self.missing_plaintexts:
|
|
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_files_symlink.keys())
|
|
files_to_commit.update(self.current_files_exec.keys())
|
|
files_to_commit.update(self.deleted_files.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))
|
|
# close any branches that need it
|
|
closed_revs = set()
|
|
for branch in self.branches_to_delete:
|
|
closed = revlog.nullid
|
|
if 'closed-branches' in self.repo.branchtags():
|
|
closed = self.repo['closed-branches'].node()
|
|
branchedits = sorted(filter(lambda x: x[0][1] == branch and x[0][0] < rev.revnum,
|
|
self.revmap.iteritems()), reverse=True)
|
|
if len(branchedits) < 1:
|
|
# can't close a branch that never existed
|
|
continue
|
|
ha = branchedits[0][1]
|
|
closed_revs.add(ha)
|
|
# self.get_parent_revision(rev.revnum, branch)
|
|
parentctx = self.repo.changectx(ha)
|
|
parents = (ha, closed)
|
|
def del_all_files(*args):
|
|
raise IOError
|
|
files = parentctx.manifest().keys()
|
|
current_ctx = context.memctx(self.repo,
|
|
parents,
|
|
rev.message or ' ',
|
|
files,
|
|
del_all_files,
|
|
self.authorforsvnauthor(rev.author),
|
|
date,
|
|
{'branch': 'closed-branches'})
|
|
new_hash = self.repo.commitctx(current_ctx)
|
|
self.ui.status('Marked branch %s as closed.\n' % (branch or
|
|
'default'))
|
|
for branch, files in branch_batches.iteritems():
|
|
if branch in self.commit_branches_empty and files:
|
|
del self.commit_branches_empty[branch]
|
|
files = dict(files)
|
|
|
|
parents = (self.get_parent_revision(rev.revnum, branch),
|
|
revlog.nullid)
|
|
if parents[0] in closed_revs and branch in self.branches_to_delete:
|
|
continue
|
|
extra = util.build_extra(rev.revnum, branch,
|
|
open(self.uuid_file).read(),
|
|
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.deleted_files:
|
|
raise IOError()
|
|
copied = self.copies.get(current_file)
|
|
flags = parent_ctx.flags(path)
|
|
is_exec = self.current_files_exec.get(current_file, 'x' in flags)
|
|
is_link = self.current_files_symlink.get(current_file, 'l' in flags)
|
|
if current_file in self.current_files:
|
|
data = self.current_files[current_file]
|
|
if is_link:
|
|
assert data.startswith('link ')
|
|
data = data[len('link '):]
|
|
else:
|
|
data = parent_ctx.filectx(path).data()
|
|
return context.memfilectx(path=path,
|
|
data=data,
|
|
islink=is_link, isexec=is_exec,
|
|
copied=copied)
|
|
current_ctx = context.memctx(self.repo,
|
|
parents,
|
|
rev.message or '...',
|
|
files.keys(),
|
|
filectxfn,
|
|
self.authorforsvnauthor(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.add_to_revmap(rev.revnum, branch, new_hash)
|
|
# now we handle branches that need to be committed without any files
|
|
for branch in self.commit_branches_empty:
|
|
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.commit_branches_empty[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,
|
|
open(self.uuid_file).read(),
|
|
self.subdir)
|
|
current_ctx = context.memctx(self.repo,
|
|
(ha, node.nullid),
|
|
rev.message or ' ',
|
|
[],
|
|
del_all_files,
|
|
self.authorforsvnauthor(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.add_to_revmap(rev.revnum, branch, new_hash)
|
|
self._save_metadata()
|
|
self.clear_current_info()
|
|
|
|
def authorforsvnauthor(self, author):
|
|
if author in self.authors:
|
|
return self.authors[author]
|
|
return '%s%s' % (author, self.author_host)
|
|
|
|
def svnauthorforauthor(self, author):
|
|
for svnauthor, hgauthor in self.authors.iteritems():
|
|
if author == hgauthor:
|
|
return svnauthor
|
|
else:
|
|
# return the original svn-side author
|
|
return author.rsplit('@', 1)[0]
|
|
|
|
def readauthors(self, authorfile):
|
|
self.ui.note(('Reading authormap from %s\n') % authorfile)
|
|
f = open(authorfile, 'r')
|
|
for line in f:
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
srcauth, dstauth = line.split('=', 1)
|
|
srcauth = srcauth.strip()
|
|
dstauth = dstauth.strip()
|
|
if srcauth in self.authors and dstauth != self.authors[srcauth]:
|
|
self.ui.status(('Overriding author mapping for "%s" ' +
|
|
'from "%s" to "%s"\n')
|
|
% (srcauth, self.authors[srcauth], dstauth))
|
|
else:
|
|
self.ui.debug(('Mapping author "%s" to "%s"\n')
|
|
% (srcauth, dstauth))
|
|
self.authors[srcauth] = dstauth
|
|
except IndexError:
|
|
self.ui.warn(
|
|
('Ignoring bad line in author map file %s: %s\n')
|
|
% (authorfile, line.rstrip()))
|
|
f.close()
|
|
|
|
def writeauthors(self):
|
|
self.ui.debug(('Writing author map to %s\n') % self.authors_file)
|
|
f = open(self.authors_file, 'w+')
|
|
for author in self.authors:
|
|
f.write("%s=%s\n" % (author, self.authors[author]))
|
|
f.close()
|
|
|
|
def readfilemap(self, filemapfile):
|
|
self.ui.note(
|
|
('Reading file map from %s\n')
|
|
% filemapfile)
|
|
def addpathtomap(path, mapping, mapname):
|
|
if path in mapping:
|
|
self.ui.warn(('Duplicate %s entry in %s: "%d"\n') %
|
|
(mapname, filemapfile, path))
|
|
else:
|
|
self.ui.debug(('%sing %s\n') %
|
|
(mapname.capitalize().strip('e'), path))
|
|
mapping[path] = path
|
|
|
|
f = open(filemapfile, 'r')
|
|
for line in f:
|
|
if line.strip() == '' or line.strip()[0] == '#':
|
|
continue
|
|
try:
|
|
cmd, path = line.split(' ', 1)
|
|
cmd = cmd.strip()
|
|
path = path.strip()
|
|
if cmd == 'include':
|
|
addpathtomap(path, self.includepaths, 'include')
|
|
elif cmd == 'exclude':
|
|
addpathtomap(path, self.excludepaths, 'exclude')
|
|
else:
|
|
self.ui.warn(
|
|
('Unknown filemap command %s\n')
|
|
% cmd)
|
|
except IndexError:
|
|
self.ui.warn(
|
|
('Ignoring bad line in filemap %s: %s\n')
|
|
% (filemapfile, line.rstrip()))
|
|
f.close()
|
|
|
|
def meta_data_dir(self):
|
|
return os.path.join(self.path, '.hg', 'svn')
|
|
meta_data_dir = property(meta_data_dir)
|
|
|
|
def meta_file_named(self, name):
|
|
return os.path.join(self.meta_data_dir, name)
|
|
|
|
def revmap_file(self):
|
|
return self.meta_file_named('rev_map')
|
|
revmap_file = property(revmap_file)
|
|
|
|
def svn_url_file(self):
|
|
return self.meta_file_named('url')
|
|
svn_url_file = property(svn_url_file)
|
|
|
|
def uuid_file(self):
|
|
return self.meta_file_named('uuid')
|
|
uuid_file = property(uuid_file)
|
|
|
|
def branch_info_file(self):
|
|
return self.meta_file_named('branch_info')
|
|
branch_info_file = property(branch_info_file)
|
|
|
|
def tag_info_file(self):
|
|
return self.meta_file_named('tag_info')
|
|
tag_info_file = property(tag_info_file)
|
|
|
|
def tag_locations_file(self):
|
|
return self.meta_file_named('tag_locations')
|
|
tag_locations_file = property(tag_locations_file)
|
|
|
|
def url(self):
|
|
return open(self.svn_url_file).read()
|
|
url = property(url)
|
|
|
|
def authors_file(self):
|
|
return self.meta_file_named('authors')
|
|
authors_file = property(authors_file)
|
|
|
|
def load_base_from_ctx(self, svnpath, path, ctx):
|
|
if not self._is_path_valid(svnpath):
|
|
return
|
|
if path in ctx:
|
|
fctx = ctx.filectx(path)
|
|
base = fctx.data()
|
|
if 'l' in fctx.flags():
|
|
base = 'link ' + base
|
|
self.set_file(svnpath, base, 'x' in fctx.flags(), 'l' in fctx.flags())
|
|
else:
|
|
self.missing_plaintexts.add(path)
|
|
|
|
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.branches_to_delete.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.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)
|
|
delete_entry = stash_exception_on_self(delete_entry)
|
|
|
|
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 fpath:
|
|
self.current_file = path
|
|
self.ui.note('M %s\n' % path)
|
|
if base_revision != -1:
|
|
self.base_revision = base_revision
|
|
else:
|
|
self.base_revision = None
|
|
if self.current_file not in self.current_files:
|
|
baserev = base_revision
|
|
if baserev is None or baserev == -1:
|
|
baserev = self.current_rev.revnum - 1
|
|
parent = self.get_parent_revision(baserev + 1, branch)
|
|
self.load_base_from_ctx(path, fpath, self.repo.changectx(parent))
|
|
else:
|
|
self.ui.warn('WARNING: Opening non-existant file %s\n' % path)
|
|
open_file = stash_exception_on_self(open_file)
|
|
|
|
def aresamefiles(self, parentctx, childctx, files):
|
|
"""Assuming all files exist in childctx and parentctx, return True
|
|
if none of them was changed in-between.
|
|
"""
|
|
if parentctx == childctx:
|
|
return True
|
|
if parentctx.rev() > childctx.rev():
|
|
parentctx, childctx = childctx, parentctx
|
|
|
|
def selfandancestors(selfctx):
|
|
yield selfctx
|
|
for ctx in selfctx.ancestors():
|
|
yield ctx
|
|
|
|
files = dict.fromkeys(files)
|
|
for pctx in selfandancestors(childctx):
|
|
if pctx.rev() <= parentctx.rev():
|
|
return True
|
|
for f in pctx.files():
|
|
if f in files:
|
|
return False
|
|
# parentctx is not an ancestor of childctx, files are unrelated
|
|
return False
|
|
|
|
def add_file(self, path, parent_baton, copyfrom_path,
|
|
copyfrom_revision, file_pool=None):
|
|
self.current_file = None
|
|
self.base_revision = None
|
|
if path in self.deleted_files:
|
|
del self.deleted_files[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.missing_plaintexts.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 self.aresamefiles(parentctx, ctx, [from_file]):
|
|
self.copies[path] = from_file
|
|
add_file = stash_exception_on_self(add_file)
|
|
|
|
def add_directory(self, path, parent_baton, copyfrom_path,
|
|
copyfrom_revision, dir_pool=None):
|
|
self.dir_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.commit_branches_empty[branch] = True
|
|
else:
|
|
self.commit_branches_empty[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.missing_plaintexts.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.missing_plaintexts.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.deleted_files:
|
|
del self.deleted_files[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 self.aresamefiles(parentctx, cp_f_ctx, copies.values()):
|
|
self.copies.update(copies)
|
|
return path
|
|
add_directory = stash_exception_on_self(add_directory)
|
|
|
|
def change_file_prop(self, file_baton, name, value, pool=None):
|
|
if name == 'svn:executable':
|
|
self.current_files_exec[self.current_file] = bool(value is not None)
|
|
elif name == 'svn:special':
|
|
self.current_files_symlink[self.current_file] = bool(value is not None)
|
|
change_file_prop = stash_exception_on_self(change_file_prop)
|
|
|
|
def change_dir_prop(self, dir_baton, name, value, pool=None):
|
|
if dir_baton is None:
|
|
return
|
|
path = self.dir_batons[dir_baton]
|
|
if name == 'svn:externals':
|
|
self.externals[path] = value
|
|
change_dir_prop = stash_exception_on_self(change_dir_prop)
|
|
|
|
def open_directory(self, path, parent_baton, base_revision, dir_pool=None):
|
|
self.dir_batons[path] = path
|
|
p_, branch = self._path_and_branch_for_path(path)
|
|
if p_ == '':
|
|
self.commit_branches_empty[branch] = False
|
|
return path
|
|
open_directory = stash_exception_on_self(open_directory)
|
|
|
|
def close_directory(self, dir_baton, dir_pool=None):
|
|
if dir_baton is not None:
|
|
del self.dir_batons[dir_baton]
|
|
close_directory = stash_exception_on_self(close_directory)
|
|
|
|
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.deleted_files, (
|
|
'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.missing_plaintexts), '%s not found' % self.current_file
|
|
if self.current_file in self.missing_plaintexts:
|
|
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.missing_plaintexts.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
|
|
apply_textdelta = stash_exception_on_self(apply_textdelta)
|
|
|
|
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.
|
|
"""
|