Commit 206ab31e authored by Qijiang Fan's avatar Qijiang Fan

Upstream version 1.5

parent 41ab46d5
repo: f2636cfed11500fdc47d1e3822d8e4a2bd636bf7
node: 07234759a3f750029ccaa001837d42fa12dd33ee
node: 77b22e5b4ea6c248e079afd0f1e544cb5690ce20
branch: default
tag: 1.4
tag: 1.5
......@@ -17,3 +17,5 @@ nbproject
......@@ -6,3 +6,4 @@
708234ad6c97fb52417e0b46a86c8373e25123a5 1.2
4bbc6bf947f56a92e95a04a27b94a9f72d5482d7 1.2.1
0cbf9fd89672e73165e1bb4db1ec8f7f65b95c94 1.3
07234759a3f750029ccaa001837d42fa12dd33ee 1.4
......@@ -206,6 +206,8 @@ cmdtable = {
('', '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 <subcommand> ...',
This diff is collapsed.
......@@ -302,6 +302,25 @@ settings:
be included or excluded. See the documentation for ``hg convert`` for more
information on filemaps.
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 @@ The following options only have an effect on the initial clone of a repository:
contain tags. The default is to only look in ``tags``. This option has no
effect for single-directory clones.
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
# 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
if updatemeta:
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
''' Module for self-contained maps. '''
import errno
import os
from mercurial import util as hgutil
from mercurial import node
......@@ -134,8 +135,7 @@ class Tags(dict):
svncommands.rebuildmeta(repo.ui, repo, ())
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 @@ class RevMap(dict):
def __init__(self, repo):
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 @@ class RevMap(dict):
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)
def mappath(repo):
return os.path.join(repo.path, 'svn', 'rev_map')
def readmapfile(cls, repo, missingok=True):
f = open(cls.mappath(repo))
except IOError, err:
if not missingok or err.errno != errno.ENOENT:
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 @@ class RevMap(dict):
if revnum < self.oldest or not self.oldest:
self.oldest = revnum
dict.__setitem__(self, (revnum, branch), node.bin(ha))
def _write(self):
f = open(self.path, 'w')
......@@ -311,7 +323,7 @@ class FileMap(object):
msg = 'duplicate %s entry in %s: "%s"\n'
self.ui.status(msg % (m, fn, path))
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 @@ class FileMap(object):
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)
......@@ -82,6 +82,11 @@ def _getdirchanges(svn, branchpath, parentctx, ctx, changedfiles, extchanges):
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.
if d not in newdirs and _isdir(svn, branchpath, d):
......@@ -133,6 +138,10 @@ def commit(ui, repo, rev_ctx, meta, base_revision, svn):
# this kind of renames: a -> b, b -> c
copies[file] = renamed[0]
base_data = parent[renamed[0]].data()
autoprops =
if autoprops:
props.setdefault(file, {}).update(autoprops)
action = 'add'
dirname = '/'.join(file.split('/')[:-1] + [''])
......@@ -30,7 +30,6 @@ def updateexternals(ui, meta, current):
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)
p, b, bp = meta.split_branch_path(path)
......@@ -52,11 +51,28 @@ def updateexternals(ui, meta, current):
def _safe_message(msg):
if msg:
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):
return _convert_rev(ui, meta, svn, r, tbdelta, firstrun)
def _convert_rev(ui, meta, svn, r, tbdelta, firstrun):
editor = meta.editor
editor.current.rev = r
if firstrun and meta.revmap.oldest <= 0:
# We know nothing about this project, so fetch everything before
......@@ -65,33 +81,28 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
svn.get_revision(r.revnum, editor)
svn.get_replay(r.revnum, editor, meta.revmap.oldest)
current = editor.current
updateexternals(ui, meta, current)
if current.exception is not None: # pragma: no cover
raise ReplayException()
if current.missing:
raise MissingPlainTextError()
# paranoidly generate the list of files to commit
files_to_commit = set(current.files.keys())
# back to a list and sort so we get sane behavior
files_to_commit = list(files_to_commit)
files_to_commit = current.files()
branch_batches = {}
rev = current.rev
date = meta.fixdate(
# 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)
p, b = meta.split_branch_path(f)[:2]
if b not in branch_batches:
......@@ -144,30 +155,33 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
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 '):]
ui.debug('file marked as link, but may contain data: '
'%s\n' % current_file)
data = parentctx.filectx(path).data()
return context.memfilectx(path=path,
islink=is_link, isexec=is_exec,
islink=islink, isexec=isexec,
message = _safe_message(rev.message)
current_ctx = context.memctx(meta.repo,
rev.message or util.default_commit_msg(ui),
message or util.default_commit_msg(ui),
......@@ -203,7 +217,7 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
current_ctx = context.memctx(meta.repo,
(ha, node.nullid),
rev.message or ' ',
_safe_message(rev.message) or ' ',
......@@ -22,7 +22,7 @@ import util
# a
# a
# +a
# Property changes on: a
# ___________________________________________________________________
# Added: svn:executable
......@@ -235,15 +235,20 @@ except AttributeError:
def patchrepo(ui, meta, parentctx, patchfp):
if not svnbackend:
return patchrepoold(ui, meta, parentctx, patchfp)
store = patch.filestore()
store = patch.filestore(util.getfilestoresize(ui))
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')
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:
......@@ -271,8 +276,8 @@ def diff_branchrev(ui, svn, meta, branch, branchpath, r, parentctx):
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,
d = svn.get_unified_diff(branchpath, r.revnum, other_rev=prev,
deleted=False, ignore_type=False)
d = svn.get_unified_diff(branchpath, r.revnum,
other_path=ppath, other_rev=prev,
......@@ -534,7 +539,12 @@ def fetch_branchrev(svn, meta, branch, branchpath, r, parentctx):
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.
elif not path.startswith(branchprefix):
if not meta.is_path_valid(path):
......@@ -698,6 +708,20 @@ def convert_rev(ui, meta, svn, r, tbdelta, firstrun):
branch = meta.localname(p)
if not (r.paths[p].action == 'R' and branch in meta.branches):
# 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):
closed = checkbranch(meta, r, branch)
if closed is not None:
deleted_branches[branch] = closed
This diff is collapsed.
......@@ -292,7 +292,9 @@ class SVNMeta(object):
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 @@ class SVNMeta(object):
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 @@ class SVNMeta(object):
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 @@ class SVNMeta(object):
# 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 @@ class SVNMeta(object):
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):
parent = self._determine_parent_branch(
p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
......@@ -18,8 +18,13 @@ import errno
from mercurial import error
from mercurial import util as hgutil
from mercurial import httprepo
import mercurial.repo
from mercurial.peer import peerrepository
from mercurial import httppeer
except ImportError:
from mercurial.repo import repository as peerrepository
from mercurial import httprepo as httppeer
from mercurial import phases
......@@ -107,11 +112,13 @@ def generate_repo_class(ui, repo):
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:
......@@ -127,6 +134,9 @@ class svnremoterepo(mercurial.repo.repository):
self.password_stores = None
def _capabilities(self):
return self.capabilities
def svnauth(self):
# DO NOT default the user to hg's getuser(). If you provide
......@@ -177,7 +187,7 @@ def instance(ui, url, create):
if url.startswith('http://') or url.startswith('https://'):
# may yield a bogus 'real URL...' message
return httprepo.instance(ui, url, create)
return httppeer.instance(ui, url, create)
except error.RepoError:
ui.note('(falling back to Subversion support)\n')
......@@ -185,4 +195,102 @@ def instance(ui, url, create):
if create:
raise hgutil.Abort('cannot create new remote Subversion repository')
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,))
def username(self, realm, may_save, pool=None):
username = self.ui.prompt('Username: ', default='')
return (username, bool(may_save))
def simple(self, realm, default_username, may_save, pool=None):
if default_username:
username = default_username
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):
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
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'))
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)
creds = None
return creds
......@@ -8,6 +8,9 @@ import tempfile
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 @@ class Revision(tuple):
def __str__(self):
return 'r%d by %s' % (self.revnum,
_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()[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):
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):