--- a/hgext/bugzilla.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/bugzilla.py Sat May 12 13:20:26 2012 +0200
@@ -585,11 +585,13 @@
# inheritance with a new-style class.
class cookietransport(cookietransportrequest, xmlrpclib.Transport):
def __init__(self, use_datetime=0):
- xmlrpclib.Transport.__init__(self, use_datetime)
+ if util.safehasattr(xmlrpclib.Transport, "__init__"):
+ xmlrpclib.Transport.__init__(self, use_datetime)
class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
def __init__(self, use_datetime=0):
- xmlrpclib.SafeTransport.__init__(self, use_datetime)
+ if util.safehasattr(xmlrpclib.Transport, "__init__"):
+ xmlrpclib.SafeTransport.__init__(self, use_datetime)
class bzxmlrpc(bzaccess):
"""Support for access to Bugzilla via the Bugzilla XMLRPC API.
--- a/hgext/largefiles/overrides.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/largefiles/overrides.py Sat May 12 13:20:26 2012 +0200
@@ -552,7 +552,8 @@
for lfile in modified:
lfutil.updatestandin(repo, lfutil.standin(lfile))
for lfile in missing:
- os.unlink(repo.wjoin(lfutil.standin(lfile)))
+ if (os.path.exists(repo.wjoin(lfutil.standin(lfile)))):
+ os.unlink(repo.wjoin(lfutil.standin(lfile)))
try:
ctx = repo[opts.get('rev')]
@@ -696,6 +697,33 @@
ui.status(_("%d largefiles cached\n") % numcached)
return result
+def overrideclone(orig, ui, source, dest=None, **opts):
+ result = hg.clone(ui, opts, source, dest,
+ pull=opts.get('pull'),
+ stream=opts.get('uncompressed'),
+ rev=opts.get('rev'),
+ update=True, # required for successful walkchangerevs
+ branch=opts.get('branch'))
+ if result is None:
+ return True
+ totalsuccess = 0
+ totalmissing = 0
+ if opts.get('all_largefiles'):
+ sourcerepo, destrepo = result
+ matchfn = scmutil.match(destrepo[None],
+ [destrepo.wjoin(lfutil.shortname)], {})
+ def prepare(ctx, fns):
+ pass
+ for ctx in cmdutil.walkchangerevs(destrepo, matchfn, {'rev' : None},
+ prepare):
+ success, missing = lfcommands.cachelfiles(ui, destrepo, ctx.node())
+ totalsuccess += len(success)
+ totalmissing += len(missing)
+ ui.status(_("%d additional largefiles cached\n") % totalsuccess)
+ if totalmissing > 0:
+ ui.status(_("%d largefiles failed to download\n") % totalmissing)
+ return totalmissing != 0
+
def overriderebase(orig, ui, repo, **opts):
repo._isrebasing = True
try:
@@ -953,6 +981,8 @@
ui.status(_('largefiles: %d to upload\n') % len(toupload))
def overrideaddremove(orig, ui, repo, *pats, **opts):
+ if not lfutil.islfilesrepo(repo):
+ return orig(ui, repo, *pats, **opts)
# Get the list of missing largefiles so we can remove them
lfdirstate = lfutil.openlfdirstate(ui, repo)
s = lfdirstate.status(match_.always(repo.root, repo.getcwd()), [], False,
--- a/hgext/largefiles/uisetup.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/largefiles/uisetup.py Sat May 12 13:20:26 2012 +0200
@@ -70,6 +70,12 @@
overrides.overrideupdate)
entry = extensions.wrapcommand(commands.table, 'pull',
overrides.overridepull)
+ entry = extensions.wrapcommand(commands.table, 'clone',
+ overrides.overrideclone)
+ cloneopt = [('', 'all-largefiles', None,
+ _('download all versions of all largefiles'))]
+
+ entry[1].extend(cloneopt)
entry = extensions.wrapcommand(commands.table, 'cat',
overrides.overridecat)
entry = extensions.wrapfunction(merge, '_checkunknownfile',
--- a/hgext/mq.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/mq.py Sat May 12 13:20:26 2012 +0200
@@ -46,6 +46,17 @@
You will by default be managing a patch queue named "patches". You can
create other, independent patch queues with the :hg:`qqueue` command.
+
+If the working directory contains uncommitted files, qpush, qpop and
+qgoto abort immediately. If -f/--force is used, the changes are
+discarded. Setting:
+
+ [mq]
+ check = True
+
+make them behave as if -c/--check were passed, and non-conflicting
+local changes will be tolerated and preserved. If incompatible options
+such as -f/--force or --exact are passed, this setting is ignored.
'''
from mercurial.i18n import _
@@ -280,6 +291,9 @@
if phase is not None:
repo.ui.restoreconfig(backup)
+class AbortNoCleanup(error.Abort):
+ pass
+
class queue(object):
def __init__(self, ui, path, patchdir=None):
self.basepath = path
@@ -554,6 +568,18 @@
except OSError, inst:
self.ui.warn(_('error removing undo: %s\n') % str(inst))
+ def backup(self, repo, files, copy=False):
+ # backup local changes in --force case
+ for f in sorted(files):
+ absf = repo.wjoin(f)
+ if os.path.lexists(absf):
+ self.ui.note(_('saving current version of %s as %s\n') %
+ (f, f + '.orig'))
+ if copy:
+ util.copyfile(absf, absf + '.orig')
+ else:
+ util.rename(absf, absf + '.orig')
+
def printdiff(self, repo, diffopts, node1, node2=None, files=None,
fp=None, changes=None, opts={}):
stat = opts.get('stat')
@@ -668,7 +694,8 @@
return (False, list(files), False)
def apply(self, repo, series, list=False, update_status=True,
- strict=False, patchdir=None, merge=None, all_files=None):
+ strict=False, patchdir=None, merge=None, all_files=None,
+ tobackup=None, check=False):
wlock = lock = tr = None
try:
wlock = repo.wlock()
@@ -676,10 +703,15 @@
tr = repo.transaction("qpush")
try:
ret = self._apply(repo, series, list, update_status,
- strict, patchdir, merge, all_files=all_files)
+ strict, patchdir, merge, all_files=all_files,
+ tobackup=tobackup, check=check)
tr.close()
self.savedirty()
return ret
+ except AbortNoCleanup:
+ tr.close()
+ self.savedirty()
+ return 2, repo.dirstate.p1()
except:
try:
tr.abort()
@@ -693,9 +725,14 @@
self.removeundo(repo)
def _apply(self, repo, series, list=False, update_status=True,
- strict=False, patchdir=None, merge=None, all_files=None):
- '''returns (error, hash)
- error = 1 for unable to read, 2 for patch failed, 3 for patch fuzz'''
+ strict=False, patchdir=None, merge=None, all_files=None,
+ tobackup=None, check=False):
+ """returns (error, hash)
+
+ error = 1 for unable to read, 2 for patch failed, 3 for patch
+ fuzz. tobackup is None or a set of files to backup before they
+ are modified by a patch.
+ """
# TODO unify with commands.py
if not patchdir:
patchdir = self.path
@@ -727,6 +764,14 @@
message = '\n'.join(message)
if ph.haspatch:
+ if tobackup:
+ touched = patchmod.changedfiles(self.ui, repo, pf)
+ touched = set(touched) & tobackup
+ if touched and check:
+ raise AbortNoCleanup(
+ _("local changes found, refresh first"))
+ self.backup(repo, touched, copy=True)
+ tobackup = tobackup - touched
(patcherr, files, fuzz) = self.patch(repo, pf)
if all_files is not None:
all_files.update(files)
@@ -838,7 +883,7 @@
def finish(self, repo, revs):
# Manually trigger phase computation to ensure phasedefaults is
# executed before we remove the patches.
- repo._phaserev
+ repo._phasecache
patches = self._revpatches(repo, sorted(revs))
qfinished = self._cleanup(patches, len(patches))
if qfinished and repo.ui.configbool('mq', 'secret', False):
@@ -935,6 +980,10 @@
else:
raise util.Abort(_('patch "%s" already exists') % name)
+ def checkforcecheck(self, check, force):
+ if force and check:
+ raise util.Abort(_('cannot use both --force and --check'))
+
def new(self, repo, patchfn, *pats, **opts):
"""options:
msg: a string or a no-argument function returning a string
@@ -1132,8 +1181,9 @@
return self.series[i + off]
raise util.Abort(_("patch %s not in series") % patch)
- def push(self, repo, patch=None, force=False, list=False,
- mergeq=None, all=False, move=False, exact=False):
+ def push(self, repo, patch=None, force=False, list=False, mergeq=None,
+ all=False, move=False, exact=False, nobackup=False, check=False):
+ self.checkforcecheck(check, force)
diffopts = self.diffopts()
wlock = repo.wlock()
try:
@@ -1188,10 +1238,13 @@
if start == len(self.series):
self.ui.warn(_('patch series already fully applied\n'))
return 1
- if not force:
+ if not force and not check:
self.checklocalchanges(repo, refresh=self.applied)
if exact:
+ if check:
+ raise util.Abort(
+ _("cannot use --exact and --check together"))
if move:
raise util.Abort(_("cannot use --exact and --move together"))
if self.applied:
@@ -1232,13 +1285,22 @@
else:
end = self.series.index(patch, start) + 1
+ tobackup = set()
+ if (not nobackup and force) or check:
+ m, a, r, d = self.checklocalchanges(repo, force=True)
+ if check:
+ tobackup.update(m + a + r + d)
+ else:
+ tobackup.update(m + a)
+
s = self.series[start:end]
all_files = set()
try:
if mergeq:
ret = self.mergepatch(repo, mergeq, s, diffopts)
else:
- ret = self.apply(repo, s, list, all_files=all_files)
+ ret = self.apply(repo, s, list, all_files=all_files,
+ tobackup=tobackup, check=check)
except:
self.ui.warn(_('cleaning up working directory...'))
node = repo.dirstate.p1()
@@ -1268,7 +1330,9 @@
finally:
wlock.release()
- def pop(self, repo, patch=None, force=False, update=True, all=False):
+ def pop(self, repo, patch=None, force=False, update=True, all=False,
+ nobackup=False, check=False):
+ self.checkforcecheck(check, force)
wlock = repo.wlock()
try:
if patch:
@@ -1313,8 +1377,14 @@
break
update = needupdate
- if not force and update:
- self.checklocalchanges(repo)
+ tobackup = set()
+ if update:
+ m, a, r, d = self.checklocalchanges(repo, force=force or check)
+ if force:
+ if not nobackup:
+ tobackup.update(m + a)
+ elif check:
+ tobackup.update(m + a + r + d)
self.applieddirty = True
end = len(self.applied)
@@ -1344,6 +1414,12 @@
m, a, r, d = repo.status(qp, top)[:4]
if d:
raise util.Abort(_("deletions found between repo revs"))
+
+ tobackup = set(a + m + r) & tobackup
+ if check and tobackup:
+ self.localchangesfound()
+ self.backup(repo, tobackup)
+
for f in a:
try:
util.unlinkpath(repo.wjoin(f))
@@ -1921,6 +1997,14 @@
self.removeundo(repo)
return imported
+def fixcheckopts(ui, opts):
+ if (not ui.configbool('mq', 'check') or opts.get('force')
+ or opts.get('exact')):
+ return opts
+ opts = dict(opts)
+ opts['check'] = True
+ return opts
+
@command("qdelete|qremove|qrm",
[('k', 'keep', None, _('keep patch file')),
('r', 'rev', [],
@@ -2460,18 +2544,25 @@
wlock.release()
@command("qgoto",
- [('f', 'force', None, _('overwrite any local changes'))],
+ [('c', 'check', None, _('tolerate non-conflicting local changes')),
+ ('f', 'force', None, _('overwrite any local changes')),
+ ('', 'no-backup', None, _('do not save backup copies of files'))],
_('hg qgoto [OPTION]... PATCH'))
def goto(ui, repo, patch, **opts):
'''push or pop patches until named patch is at top of stack
Returns 0 on success.'''
+ opts = fixcheckopts(ui, opts)
q = repo.mq
patch = q.lookup(patch)
+ nobackup = opts.get('no_backup')
+ check = opts.get('check')
if q.isapplied(patch):
- ret = q.pop(repo, patch, force=opts.get('force'))
+ ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
+ check=check)
else:
- ret = q.push(repo, patch, force=opts.get('force'))
+ ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
+ check=check)
q.savedirty()
return ret
@@ -2591,26 +2682,32 @@
return newpath
@command("^qpush",
- [('f', 'force', None, _('apply on top of local changes')),
+ [('c', 'check', None, _('tolerate non-conflicting local changes')),
+ ('f', 'force', None, _('apply on top of local changes')),
('e', 'exact', None, _('apply the target patch to its recorded parent')),
('l', 'list', None, _('list patch name in commit text')),
('a', 'all', None, _('apply all patches')),
('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
('n', 'name', '',
_('merge queue name (DEPRECATED)'), _('NAME')),
- ('', 'move', None, _('reorder patch series and apply only the patch'))],
+ ('', 'move', None,
+ _('reorder patch series and apply only the patch')),
+ ('', 'no-backup', None, _('do not save backup copies of files'))],
_('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
def push(ui, repo, patch=None, **opts):
"""push the next patch onto the stack
- When -f/--force is applied, all local changes in patched files
- will be lost.
+ By default, abort if the working directory contains uncommitted
+ changes. With -c/--check, abort only if the uncommitted files
+ overlap with patched files. With -f/--force, backup and patch over
+ uncommitted changes.
Return 0 on success.
"""
q = repo.mq
mergeq = None
+ opts = fixcheckopts(ui, opts)
if opts.get('merge'):
if opts.get('name'):
newpath = repo.join(opts.get('name'))
@@ -2623,24 +2720,33 @@
ui.warn(_("merging with queue at: %s\n") % mergeq.path)
ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
- exact=opts.get('exact'))
+ exact=opts.get('exact'), nobackup=opts.get('no_backup'),
+ check=opts.get('check'))
return ret
@command("^qpop",
[('a', 'all', None, _('pop all patches')),
('n', 'name', '',
_('queue name to pop (DEPRECATED)'), _('NAME')),
- ('f', 'force', None, _('forget any local changes to patched files'))],
+ ('c', 'check', None, _('tolerate non-conflicting local changes')),
+ ('f', 'force', None, _('forget any local changes to patched files')),
+ ('', 'no-backup', None, _('do not save backup copies of files'))],
_('hg qpop [-a] [-f] [PATCH | INDEX]'))
def pop(ui, repo, patch=None, **opts):
"""pop the current patch off the stack
- By default, pops off the top of the patch stack. If given a patch
- name, keeps popping off patches until the named patch is at the
- top of the stack.
+ Without argument, pops off the top of the patch stack. If given a
+ patch name, keeps popping off patches until the named patch is at
+ the top of the stack.
+
+ By default, abort if the working directory contains uncommitted
+ changes. With -c/--check, abort only if the uncommitted files
+ overlap with patched files. With -f/--force, backup and discard
+ changes made to such files.
Return 0 on success.
"""
+ opts = fixcheckopts(ui, opts)
localupdate = True
if opts.get('name'):
q = queue(ui, repo.path, repo.join(opts.get('name')))
@@ -2649,7 +2755,8 @@
else:
q = repo.mq
ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
- all=opts.get('all'))
+ all=opts.get('all'), nobackup=opts.get('no_backup'),
+ check=opts.get('check'))
q.savedirty()
return ret
--- a/hgext/pager.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/pager.py Sat May 12 13:20:26 2012 +0200
@@ -22,12 +22,6 @@
If no pager is set, the pager extensions uses the environment variable
$PAGER. If neither pager.pager, nor $PAGER is set, no pager is used.
-If you notice "BROKEN PIPE" error messages, you can disable them by
-setting::
-
- [pager]
- quiet = True
-
You can disable the pager for certain commands by adding them to the
pager.ignore list::
@@ -53,37 +47,27 @@
normal behavior.
'''
-import sys, os, signal, shlex, errno
+import atexit, sys, os, signal, subprocess
from mercurial import commands, dispatch, util, extensions
from mercurial.i18n import _
def _runpager(p):
- if not util.safehasattr(os, 'fork'):
- sys.stdout = util.popen(p, 'wb')
- if util.isatty(sys.stderr):
- sys.stderr = sys.stdout
- return
- fdin, fdout = os.pipe()
- pid = os.fork()
- if pid == 0:
- os.close(fdin)
- os.dup2(fdout, sys.stdout.fileno())
- if util.isatty(sys.stderr):
- os.dup2(fdout, sys.stderr.fileno())
- os.close(fdout)
- return
- os.dup2(fdin, sys.stdin.fileno())
- os.close(fdin)
- os.close(fdout)
- try:
- os.execvp('/bin/sh', ['/bin/sh', '-c', p])
- except OSError, e:
- if e.errno == errno.ENOENT:
- # no /bin/sh, try executing the pager directly
- args = shlex.split(p)
- os.execvp(args[0], args)
- else:
- raise
+ pager = subprocess.Popen(p, shell=True, bufsize=-1,
+ close_fds=util.closefds, stdin=subprocess.PIPE,
+ stdout=sys.stdout, stderr=sys.stderr)
+
+ stdout = os.dup(sys.stdout.fileno())
+ stderr = os.dup(sys.stderr.fileno())
+ os.dup2(pager.stdin.fileno(), sys.stdout.fileno())
+ if util.isatty(sys.stderr):
+ os.dup2(pager.stdin.fileno(), sys.stderr.fileno())
+
+ @atexit.register
+ def killpager():
+ pager.stdin.close()
+ os.dup2(stdout, sys.stdout.fileno())
+ os.dup2(stderr, sys.stderr.fileno())
+ pager.wait()
def uisetup(ui):
if ui.plain() or '--debugger' in sys.argv or not util.isatty(sys.stdout):
@@ -101,9 +85,9 @@
(cmd not in ui.configlist('pager', 'ignore') and not attend))):
ui.setconfig('ui', 'formatted', ui.formatted())
ui.setconfig('ui', 'interactive', False)
+ if util.safehasattr(signal, "SIGPIPE"):
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
_runpager(p)
- if ui.configbool('pager', 'quiet'):
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
return orig(ui, options, cmd, cmdfunc)
extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
--- a/hgext/transplant.py Wed Apr 25 01:35:39 2012 +0200
+++ b/hgext/transplant.py Sat May 12 13:20:26 2012 +0200
@@ -124,7 +124,7 @@
continue
parents = source.changelog.parents(node)
- if not opts.get('filter'):
+ if not (opts.get('filter') or opts.get('log')):
# If the changeset parent is the same as the
# wdir's parent, just pull it.
if parents[0] == p1:
--- a/mercurial/cmdutil.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/cmdutil.py Sat May 12 13:20:26 2012 +0200
@@ -1311,6 +1311,12 @@
# |
# base o - parent of amending changeset
+ # Update extra dict from amended commit (e.g. to preserve graft source)
+ extra.update(old.extra())
+
+ # Also update it from the intermediate commit or from the wctx
+ extra.update(ctx.extra())
+
files = set(old.files())
# Second, we use either the commit we just did, or if there were no
@@ -1322,7 +1328,6 @@
user = ctx.user()
date = ctx.date()
message = ctx.description()
- extra = ctx.extra()
# Recompute copies (avoid recording a -> b -> a)
copied = copies.pathcopies(base, ctx)
--- a/mercurial/commands.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/commands.py Sat May 12 13:20:26 2012 +0200
@@ -196,6 +196,8 @@
be identical) as its parameter. Detecting renamed files this way
can be expensive. After using this option, :hg:`status -C` can be
used to check which files were identified as moved or renamed.
+ If this option is not specified, only renames of identical files
+ are detected.
Returns 0 if all files are successfully added.
"""
@@ -518,10 +520,12 @@
revision as good or bad without checking it out first.
If you supply a command, it will be used for automatic bisection.
- Its exit status will be used to mark revisions as good or bad:
- status 0 means good, 125 means to skip the revision, 127
- (command not found) will abort the bisection, and any other
- non-zero exit status means the revision is bad.
+ The environment variable HG_NODE will contain the ID of the
+ changeset being tested. The exit status of the command will be
+ used to mark revisions as good or bad: status 0 means good, 125
+ means to skip the revision, 127 (command not found) will abort the
+ bisection, and any other non-zero exit status means the revision
+ is bad.
.. container:: verbose
@@ -561,6 +565,11 @@
hg log -r "bisect(pruned)"
+ - see the changeset currently being bisected (especially useful
+ if running with -U/--noupdate)::
+
+ hg log -r "bisect(current)"
+
- see all changesets that took part in the current bisection::
hg log -r "bisect(range)"
@@ -645,10 +654,22 @@
if command:
changesets = 1
try:
+ node = state['current'][0]
+ except LookupError:
+ if noupdate:
+ raise util.Abort(_('current bisect revision is unknown - '
+ 'start a new bisect to fix'))
+ node, p2 = repo.dirstate.parents()
+ if p2 != nullid:
+ raise util.Abort(_('current bisect revision is a merge'))
+ try:
while changesets:
# update state
+ state['current'] = [node]
hbisect.save_state(repo, state)
- status = util.system(command, out=ui.fout)
+ status = util.system(command,
+ environ={'HG_NODE': hex(node)},
+ out=ui.fout)
if status == 125:
transition = "skip"
elif status == 0:
@@ -660,7 +681,7 @@
raise util.Abort(_("%s killed") % command)
else:
transition = "bad"
- ctx = scmutil.revsingle(repo, rev)
+ ctx = scmutil.revsingle(repo, rev, node)
rev = None # clear for future iterations
state[transition].append(ctx.node())
ui.status(_('Changeset %d:%s: %s\n') % (ctx, ctx, transition))
@@ -668,9 +689,12 @@
# bisect
nodes, changesets, good = hbisect.bisect(repo.changelog, state)
# update to next check
- cmdutil.bailifchanged(repo)
- hg.clean(repo, nodes[0], show_stats=False)
+ node = nodes[0]
+ if not noupdate:
+ cmdutil.bailifchanged(repo)
+ hg.clean(repo, node, show_stats=False)
finally:
+ state['current'] = [node]
hbisect.save_state(repo, state)
print_result(nodes, good)
return
@@ -702,6 +726,8 @@
if extendnode is not None:
ui.write(_("Extending search to changeset %d:%s\n"
% (extendnode.rev(), extendnode)))
+ state['current'] = [extendnode.node()]
+ hbisect.save_state(repo, state)
if noupdate:
return
cmdutil.bailifchanged(repo)
@@ -721,6 +747,8 @@
ui.write(_("Testing changeset %d:%s "
"(%d changesets remaining, ~%d tests)\n")
% (rev, short(node), changesets, tests))
+ state['current'] = [node]
+ hbisect.save_state(repo, state)
if not noupdate:
cmdutil.bailifchanged(repo)
return hg.clean(repo, node)
@@ -2553,6 +2581,7 @@
'graft',
[('c', 'continue', False, _('resume interrupted graft')),
('e', 'edit', False, _('invoke editor on commit messages')),
+ ('', 'log', None, _('append graft info to log message')),
('D', 'currentdate', False,
_('record the current date as commit date')),
('U', 'currentuser', False,
@@ -2571,6 +2600,11 @@
Changesets that are ancestors of the current revision, that have
already been grafted, or that are merges will be skipped.
+ If --log is specified, log messages will have a comment appended
+ of the form::
+
+ (grafted from CHANGESETHASH)
+
If a graft merge results in conflicts, the graft process is
interrupted so that the current merge can be manually resolved.
Once all conflicts are addressed, the graft process can be
@@ -2720,7 +2754,10 @@
date = ctx.date()
if opts.get('date'):
date = opts['date']
- node = repo.commit(text=ctx.description(), user=user,
+ message = ctx.description()
+ if opts.get('log'):
+ message += '\n(grafted from %s)' % ctx.hex()
+ node = repo.commit(text=message, user=user,
date=date, extra=extra, editor=editor)
if node is None:
ui.status(_('graft for revision %s is empty\n') % ctx.rev())
@@ -4347,10 +4384,10 @@
lock = repo.lock()
try:
# set phase
- nodes = [ctx.node() for ctx in repo.set('%ld', revs)]
- if not nodes:
- raise util.Abort(_('empty revision set'))
- olddata = repo._phaserev[:]
+ if not revs:
+ raise util.Abort(_('empty revision set'))
+ nodes = [repo[r].node() for r in revs]
+ olddata = repo._phasecache.getphaserevs(repo)[:]
phases.advanceboundary(repo, targetphase, nodes)
if opts['force']:
phases.retractboundary(repo, targetphase, nodes)
@@ -4358,7 +4395,7 @@
lock.release()
if olddata is not None:
changes = 0
- newdata = repo._phaserev
+ newdata = repo._phasecache.getphaserevs(repo)
changes = sum(o != newdata[i] for i, o in enumerate(olddata))
rejected = [n for n in nodes
if newdata[repo[n].rev()] < targetphase]
--- a/mercurial/context.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/context.py Sat May 12 13:20:26 2012 +0200
@@ -191,12 +191,7 @@
def bookmarks(self):
return self._repo.nodebookmarks(self._node)
def phase(self):
- if self._rev == -1:
- return phases.public
- if self._rev >= len(self._repo._phaserev):
- # outdated cache
- del self._repo._phaserev
- return self._repo._phaserev[self._rev]
+ return self._repo._phasecache.phase(self._repo, self._rev)
def phasestr(self):
return phases.phasenames[self.phase()]
def mutable(self):
--- a/mercurial/diffhelpers.c Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/diffhelpers.c Sat May 12 13:20:26 2012 +0200
@@ -135,7 +135,7 @@
return NULL;
alen = PyList_Size(a);
blen = PyList_Size(b);
- if (alen > blen - bstart) {
+ if (alen > blen - bstart || bstart < 0) {
return Py_BuildValue("l", -1);
}
for (i = 0; i < alen; i++) {
--- a/mercurial/discovery.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/discovery.py Sat May 12 13:20:26 2012 +0200
@@ -105,7 +105,7 @@
og.commonheads, _any, _hds = commoninc
# compute outgoing
- if not repo._phaseroots[phases.secret]:
+ if not repo._phasecache.phaseroots[phases.secret]:
og.missingheads = onlyheads or repo.heads()
elif onlyheads is None:
# use visible heads as it should be cached
--- a/mercurial/hbisect.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/hbisect.py Sat May 12 13:20:26 2012 +0200
@@ -132,7 +132,7 @@
def load_state(repo):
- state = {'good': [], 'bad': [], 'skip': []}
+ state = {'current': [], 'good': [], 'bad': [], 'skip': []}
if os.path.exists(repo.join("bisect.state")):
for l in repo.opener("bisect.state"):
kind, node = l[:-1].split()
@@ -164,10 +164,11 @@
- ``pruned`` : csets that are goods, bads or skipped
- ``untested`` : csets whose fate is yet unknown
- ``ignored`` : csets ignored due to DAG topology
+ - ``current`` : the cset currently being bisected
"""
state = load_state(repo)
- if status in ('good', 'bad', 'skip'):
- return [repo.changelog.rev(n) for n in state[status]]
+ if status in ('good', 'bad', 'skip', 'current'):
+ return map(repo.changelog.rev, state[status])
else:
# In the floowing sets, we do *not* call 'bisect()' with more
# than one level of recusrsion, because that can be very, very
@@ -233,7 +234,7 @@
if rev in get(repo, 'skip'):
# i18n: bisect changeset status
return _('skipped')
- if rev in get(repo, 'untested'):
+ if rev in get(repo, 'untested') or rev in get(repo, 'current'):
# i18n: bisect changeset status
return _('untested')
if rev in get(repo, 'ignored'):
--- a/mercurial/httpclient/__init__.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/__init__.py Sat May 12 13:20:26 2012 +0200
@@ -45,6 +45,7 @@
import select
import socket
+import _readers
import socketutil
logger = logging.getLogger(__name__)
@@ -54,8 +55,6 @@
HTTP_VER_1_0 = 'HTTP/1.0'
HTTP_VER_1_1 = 'HTTP/1.1'
-_LEN_CLOSE_IS_END = -1
-
OUTGOING_BUFFER_SIZE = 1 << 15
INCOMING_BUFFER_SIZE = 1 << 20
@@ -83,23 +82,19 @@
The response will continue to load as available. If you need the
complete response before continuing, check the .complete() method.
"""
- def __init__(self, sock, timeout):
+ def __init__(self, sock, timeout, method):
self.sock = sock
+ self.method = method
self.raw_response = ''
- self._body = None
self._headers_len = 0
- self._content_len = 0
self.headers = None
self.will_close = False
self.status_line = ''
self.status = None
+ self.continued = False
self.http_version = None
self.reason = None
- self._chunked = False
- self._chunked_done = False
- self._chunked_until_next = 0
- self._chunked_skip_bytes = 0
- self._chunked_preloaded_block = None
+ self._reader = None
self._read_location = 0
self._eol = EOL
@@ -117,11 +112,12 @@
socket is closed, this will nearly always return False, even
in cases where all the data has actually been loaded.
"""
- if self._chunked:
- return self._chunked_done
- if self._content_len == _LEN_CLOSE_IS_END:
- return False
- return self._body is not None and len(self._body) >= self._content_len
+ if self._reader:
+ return self._reader.done()
+
+ def _close(self):
+ if self._reader is not None:
+ self._reader._close()
def readline(self):
"""Read a single line from the response body.
@@ -129,30 +125,34 @@
This may block until either a line ending is found or the
response is complete.
"""
- eol = self._body.find('\n', self._read_location)
- while eol == -1 and not self.complete():
+ # TODO: move this into the reader interface where it can be
+ # smarter (and probably avoid copies)
+ bytes = []
+ while not bytes:
+ try:
+ bytes = [self._reader.read(1)]
+ except _readers.ReadNotReady:
+ self._select()
+ while bytes[-1] != '\n' and not self.complete():
self._select()
- eol = self._body.find('\n', self._read_location)
- if eol != -1:
- eol += 1
- else:
- eol = len(self._body)
- data = self._body[self._read_location:eol]
- self._read_location = eol
- return data
+ bytes.append(self._reader.read(1))
+ if bytes[-1] != '\n':
+ next = self._reader.read(1)
+ while next and next != '\n':
+ bytes.append(next)
+ next = self._reader.read(1)
+ bytes.append(next)
+ return ''.join(bytes)
def read(self, length=None):
# if length is None, unbounded read
while (not self.complete() # never select on a finished read
and (not length # unbounded, so we wait for complete()
- or (self._read_location + length) > len(self._body))):
+ or length > self._reader.available_data)):
self._select()
if not length:
- length = len(self._body) - self._read_location
- elif len(self._body) < (self._read_location + length):
- length = len(self._body) - self._read_location
- r = self._body[self._read_location:self._read_location + length]
- self._read_location += len(r)
+ length = self._reader.available_data
+ r = self._reader.read(length)
if self.complete() and self.will_close:
self.sock.close()
return r
@@ -160,93 +160,35 @@
def _select(self):
r, _, _ = select.select([self.sock], [], [], self._timeout)
if not r:
- # socket was not readable. If the response is not complete
- # and we're not a _LEN_CLOSE_IS_END response, raise a timeout.
- # If we are a _LEN_CLOSE_IS_END response and we have no data,
- # raise a timeout.
- if not (self.complete() or
- (self._content_len == _LEN_CLOSE_IS_END and self._body)):
+ # socket was not readable. If the response is not
+ # complete, raise a timeout.
+ if not self.complete():
logger.info('timed out with timeout of %s', self._timeout)
raise HTTPTimeoutException('timeout reading data')
- logger.info('cl: %r body: %r', self._content_len, self._body)
try:
data = self.sock.recv(INCOMING_BUFFER_SIZE)
- # If the socket was readable and no data was read, that
- # means the socket was closed. If this isn't a
- # _CLOSE_IS_END socket, then something is wrong if we're
- # here (we shouldn't enter _select() if the response is
- # complete), so abort.
- if not data and self._content_len != _LEN_CLOSE_IS_END:
- raise HTTPRemoteClosedError(
- 'server appears to have closed the socket mid-response')
except socket.sslerror, e:
if e.args[0] != socket.SSL_ERROR_WANT_READ:
raise
logger.debug('SSL_WANT_READ in _select, should retry later')
return True
logger.debug('response read %d data during _select', len(data))
+ # If the socket was readable and no data was read, that means
+ # the socket was closed. Inform the reader (if any) so it can
+ # raise an exception if this is an invalid situation.
if not data:
- if self.headers and self._content_len == _LEN_CLOSE_IS_END:
- self._content_len = len(self._body)
+ if self._reader:
+ self._reader._close()
return False
else:
self._load_response(data)
return True
- def _chunked_parsedata(self, data):
- if self._chunked_preloaded_block:
- data = self._chunked_preloaded_block + data
- self._chunked_preloaded_block = None
- while data:
- logger.debug('looping with %d data remaining', len(data))
- # Slice out anything we should skip
- if self._chunked_skip_bytes:
- if len(data) <= self._chunked_skip_bytes:
- self._chunked_skip_bytes -= len(data)
- data = ''
- break
- else:
- data = data[self._chunked_skip_bytes:]
- self._chunked_skip_bytes = 0
-
- # determine how much is until the next chunk
- if self._chunked_until_next:
- amt = self._chunked_until_next
- logger.debug('reading remaining %d of existing chunk', amt)
- self._chunked_until_next = 0
- body = data
- else:
- try:
- amt, body = data.split(self._eol, 1)
- except ValueError:
- self._chunked_preloaded_block = data
- logger.debug('saving %r as a preloaded block for chunked',
- self._chunked_preloaded_block)
- return
- amt = int(amt, base=16)
- logger.debug('reading chunk of length %d', amt)
- if amt == 0:
- self._chunked_done = True
-
- # read through end of what we have or the chunk
- self._body += body[:amt]
- if len(body) >= amt:
- data = body[amt:]
- self._chunked_skip_bytes = len(self._eol)
- else:
- self._chunked_until_next = amt - len(body)
- self._chunked_skip_bytes = 0
- data = ''
-
def _load_response(self, data):
- if self._chunked:
- self._chunked_parsedata(data)
- return
- elif self._body is not None:
- self._body += data
- return
-
- # We haven't seen end of headers yet
+ # Being here implies we're not at the end of the headers yet,
+ # since at the end of this method if headers were completely
+ # loaded we replace this method with the load() method of the
+ # reader we created.
self.raw_response += data
# This is a bogus server with bad line endings
if self._eol not in self.raw_response:
@@ -270,6 +212,7 @@
http_ver, status = hdrs.split(' ', 1)
if status.startswith('100'):
self.raw_response = body
+ self.continued = True
logger.debug('continue seen, setting body to %r', body)
return
@@ -289,23 +232,46 @@
if self._eol != EOL:
hdrs = hdrs.replace(self._eol, '\r\n')
headers = rfc822.Message(cStringIO.StringIO(hdrs))
+ content_len = None
if HDR_CONTENT_LENGTH in headers:
- self._content_len = int(headers[HDR_CONTENT_LENGTH])
+ content_len = int(headers[HDR_CONTENT_LENGTH])
if self.http_version == HTTP_VER_1_0:
self.will_close = True
elif HDR_CONNECTION_CTRL in headers:
self.will_close = (
headers[HDR_CONNECTION_CTRL].lower() == CONNECTION_CLOSE)
- if self._content_len == 0:
- self._content_len = _LEN_CLOSE_IS_END
if (HDR_XFER_ENCODING in headers
and headers[HDR_XFER_ENCODING].lower() == XFER_ENCODING_CHUNKED):
- self._body = ''
- self._chunked_parsedata(body)
- self._chunked = True
- if self._body is None:
- self._body = body
+ self._reader = _readers.ChunkedReader(self._eol)
+ logger.debug('using a chunked reader')
+ else:
+ # HEAD responses are forbidden from returning a body, and
+ # it's implausible for a CONNECT response to use
+ # close-is-end logic for an OK response.
+ if (self.method == 'HEAD' or
+ (self.method == 'CONNECT' and content_len is None)):
+ content_len = 0
+ if content_len is not None:
+ logger.debug('using a content-length reader with length %d',
+ content_len)
+ self._reader = _readers.ContentLengthReader(content_len)
+ else:
+ # Response body had no length specified and is not
+ # chunked, so the end of the body will only be
+ # identifiable by the termination of the socket by the
+ # server. My interpretation of the spec means that we
+ # are correct in hitting this case if
+ # transfer-encoding, content-length, and
+ # connection-control were left unspecified.
+ self._reader = _readers.CloseIsEndReader()
+ logger.debug('using a close-is-end reader')
+ self.will_close = True
+
+ if body:
+ self._reader._load(body)
+ logger.debug('headers complete')
self.headers = headers
+ self._load_response = self._reader._load
class HTTPConnection(object):
@@ -382,13 +348,14 @@
{}, HTTP_VER_1_0)
sock.send(data)
sock.setblocking(0)
- r = self.response_class(sock, self.timeout)
+ r = self.response_class(sock, self.timeout, 'CONNECT')
timeout_exc = HTTPTimeoutException(
'Timed out waiting for CONNECT response from proxy')
while not r.complete():
try:
if not r._select():
- raise timeout_exc
+ if not r.complete():
+ raise timeout_exc
except HTTPTimeoutException:
# This raise/except pattern looks goofy, but
# _select can raise the timeout as well as the
@@ -527,7 +494,7 @@
out = outgoing_headers or body
blocking_on_continue = False
if expect_continue and not outgoing_headers and not (
- response and response.headers):
+ response and (response.headers or response.continued)):
logger.info(
'waiting up to %s seconds for'
' continue response from server',
@@ -550,11 +517,6 @@
'server, optimistically sending request body')
else:
raise HTTPTimeoutException('timeout sending data')
- # TODO exceptional conditions with select? (what are those be?)
- # TODO if the response is loading, must we finish sending at all?
- #
- # Certainly not if it's going to close the connection and/or
- # the response is already done...I think.
was_first = first
# incoming data
@@ -572,11 +534,11 @@
logger.info('socket appears closed in read')
self.sock = None
self._current_response = None
+ if response is not None:
+ response._close()
# This if/elif ladder is a bit subtle,
# comments in each branch should help.
- if response is not None and (
- response.complete() or
- response._content_len == _LEN_CLOSE_IS_END):
+ if response is not None and response.complete():
# Server responded completely and then
# closed the socket. We should just shut
# things down and let the caller get their
@@ -605,7 +567,7 @@
'response was missing or incomplete!')
logger.debug('read %d bytes in request()', len(data))
if response is None:
- response = self.response_class(r[0], self.timeout)
+ response = self.response_class(r[0], self.timeout, method)
response._load_response(data)
# Jump to the next select() call so we load more
# data if the server is still sending us content.
@@ -613,10 +575,6 @@
except socket.error, e:
if e[0] != errno.EPIPE and not was_first:
raise
- if (response._content_len
- and response._content_len != _LEN_CLOSE_IS_END):
- outgoing_headers = sent_data + outgoing_headers
- reconnect('read')
# outgoing data
if w and out:
@@ -661,7 +619,7 @@
# close if the server response said to or responded before eating
# the whole request
if response is None:
- response = self.response_class(self.sock, self.timeout)
+ response = self.response_class(self.sock, self.timeout, method)
complete = response.complete()
data_left = bool(outgoing_headers or body)
if data_left:
@@ -679,7 +637,8 @@
raise httplib.ResponseNotReady()
r = self._current_response
while r.headers is None:
- r._select()
+ if not r._select() and not r.complete():
+ raise _readers.HTTPRemoteClosedError()
if r.will_close:
self.sock = None
self._current_response = None
@@ -705,7 +664,7 @@
class HTTPStateError(httplib.HTTPException):
"""Invalid internal state encountered."""
-
-class HTTPRemoteClosedError(httplib.HTTPException):
- """The server closed the remote socket in the middle of a response."""
+# Forward this exception type from _readers since it needs to be part
+# of the public API.
+HTTPRemoteClosedError = _readers.HTTPRemoteClosedError
# no-check-code
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/httpclient/_readers.py Sat May 12 13:20:26 2012 +0200
@@ -0,0 +1,195 @@
+# Copyright 2011, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""Reader objects to abstract out different body response types.
+
+This module is package-private. It is not expected that these will
+have any clients outside of httpplus.
+"""
+
+import httplib
+import itertools
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class ReadNotReady(Exception):
+ """Raised when read() is attempted but not enough data is loaded."""
+
+
+class HTTPRemoteClosedError(httplib.HTTPException):
+ """The server closed the remote socket in the middle of a response."""
+
+
+class AbstractReader(object):
+ """Abstract base class for response readers.
+
+ Subclasses must implement _load, and should implement _close if
+ it's not an error for the server to close their socket without
+ some termination condition being detected during _load.
+ """
+ def __init__(self):
+ self._finished = False
+ self._done_chunks = []
+
+ @property
+ def available_data(self):
+ return sum(map(len, self._done_chunks))
+
+ def done(self):
+ return self._finished
+
+ def read(self, amt):
+ if self.available_data < amt and not self._finished:
+ raise ReadNotReady()
+ need = [amt]
+ def pred(s):
+ needed = need[0] > 0
+ need[0] -= len(s)
+ return needed
+ blocks = list(itertools.takewhile(pred, self._done_chunks))
+ self._done_chunks = self._done_chunks[len(blocks):]
+ over_read = sum(map(len, blocks)) - amt
+ if over_read > 0 and blocks:
+ logger.debug('need to reinsert %d data into done chunks', over_read)
+ last = blocks[-1]
+ blocks[-1], reinsert = last[:-over_read], last[-over_read:]
+ self._done_chunks.insert(0, reinsert)
+ result = ''.join(blocks)
+ assert len(result) == amt or (self._finished and len(result) < amt)
+ return result
+
+ def _load(self, data): # pragma: no cover
+ """Subclasses must implement this.
+
+ As data is available to be read out of this object, it should
+ be placed into the _done_chunks list. Subclasses should not
+ rely on data remaining in _done_chunks forever, as it may be
+ reaped if the client is parsing data as it comes in.
+ """
+ raise NotImplementedError
+
+ def _close(self):
+ """Default implementation of close.
+
+ The default implementation assumes that the reader will mark
+ the response as finished on the _finished attribute once the
+ entire response body has been read. In the event that this is
+ not true, the subclass should override the implementation of
+ close (for example, close-is-end responses have to set
+ self._finished in the close handler.)
+ """
+ if not self._finished:
+ raise HTTPRemoteClosedError(
+ 'server appears to have closed the socket mid-response')
+
+
+class AbstractSimpleReader(AbstractReader):
+ """Abstract base class for simple readers that require no response decoding.
+
+ Examples of such responses are Connection: Close (close-is-end)
+ and responses that specify a content length.
+ """
+ def _load(self, data):
+ if data:
+ assert not self._finished, (
+ 'tried to add data (%r) to a closed reader!' % data)
+ logger.debug('%s read an addtional %d data', self.name, len(data))
+ self._done_chunks.append(data)
+
+
+class CloseIsEndReader(AbstractSimpleReader):
+ """Reader for responses that specify Connection: Close for length."""
+ name = 'close-is-end'
+
+ def _close(self):
+ logger.info('Marking close-is-end reader as closed.')
+ self._finished = True
+
+
+class ContentLengthReader(AbstractSimpleReader):
+ """Reader for responses that specify an exact content length."""
+ name = 'content-length'
+
+ def __init__(self, amount):
+ AbstractReader.__init__(self)
+ self._amount = amount
+ if amount == 0:
+ self._finished = True
+ self._amount_seen = 0
+
+ def _load(self, data):
+ AbstractSimpleReader._load(self, data)
+ self._amount_seen += len(data)
+ if self._amount_seen >= self._amount:
+ self._finished = True
+ logger.debug('content-length read complete')
+
+
+class ChunkedReader(AbstractReader):
+ """Reader for chunked transfer encoding responses."""
+ def __init__(self, eol):
+ AbstractReader.__init__(self)
+ self._eol = eol
+ self._leftover_skip_amt = 0
+ self._leftover_data = ''
+
+ def _load(self, data):
+ assert not self._finished, 'tried to add data to a closed reader!'
+ logger.debug('chunked read an addtional %d data', len(data))
+ position = 0
+ if self._leftover_data:
+ logger.debug('chunked reader trying to finish block from leftover data')
+ # TODO: avoid this string concatenation if possible
+ data = self._leftover_data + data
+ position = self._leftover_skip_amt
+ self._leftover_data = ''
+ self._leftover_skip_amt = 0
+ datalen = len(data)
+ while position < datalen:
+ split = data.find(self._eol, position)
+ if split == -1:
+ self._leftover_data = data
+ self._leftover_skip_amt = position
+ return
+ amt = int(data[position:split], base=16)
+ block_start = split + len(self._eol)
+ # If the whole data chunk plus the eol trailer hasn't
+ # loaded, we'll wait for the next load.
+ if block_start + amt + len(self._eol) > len(data):
+ self._leftover_data = data
+ self._leftover_skip_amt = position
+ return
+ if amt == 0:
+ self._finished = True
+ logger.debug('closing chunked redaer due to chunk of length 0')
+ return
+ self._done_chunks.append(data[block_start:block_start + amt])
+ position = block_start + amt + len(self._eol)
+# no-check-code
--- a/mercurial/httpclient/tests/simple_http_test.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/simple_http_test.py Sat May 12 13:20:26 2012 +0200
@@ -29,7 +29,7 @@
import socket
import unittest
-import http
+import httpplus
# relative import to ease embedding the library
import util
@@ -38,7 +38,7 @@
class SimpleHttpTest(util.HttpTestBase, unittest.TestCase):
def _run_simple_test(self, host, server_data, expected_req, expected_data):
- con = http.HTTPConnection(host)
+ con = httpplus.HTTPConnection(host)
con._connect()
con.sock.data = server_data
con.request('GET', '/')
@@ -47,9 +47,9 @@
self.assertEqual(expected_data, con.getresponse().read())
def test_broken_data_obj(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
- self.assertRaises(http.BadRequestData,
+ self.assertRaises(httpplus.BadRequestData,
con.request, 'POST', '/', body=1)
def test_no_keepalive_http_1_0(self):
@@ -74,7 +74,7 @@
fncache
dotencode
"""
- con = http.HTTPConnection('localhost:9999')
+ con = httpplus.HTTPConnection('localhost:9999')
con._connect()
con.sock.data = [expected_response_headers, expected_response_body]
con.request('GET', '/remote/.hg/requires',
@@ -95,7 +95,7 @@
self.assert_(resp.sock.closed)
def test_multiline_header(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
con.sock.data = ['HTTP/1.1 200 OK\r\n',
'Server: BogusServer 1.0\r\n',
@@ -122,7 +122,7 @@
self.assertEqual(con.sock.closed, False)
def testSimpleRequest(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
con.sock.data = ['HTTP/1.1 200 OK\r\n',
'Server: BogusServer 1.0\r\n',
@@ -149,12 +149,13 @@
resp.headers.getheaders('server'))
def testHeaderlessResponse(self):
- con = http.HTTPConnection('1.2.3.4', use_ssl=False)
+ con = httpplus.HTTPConnection('1.2.3.4', use_ssl=False)
con._connect()
con.sock.data = ['HTTP/1.1 200 OK\r\n',
'\r\n'
'1234567890'
]
+ con.sock.close_on_empty = True
con.request('GET', '/')
expected_req = ('GET / HTTP/1.1\r\n'
@@ -169,7 +170,30 @@
self.assertEqual(resp.status, 200)
def testReadline(self):
- con = http.HTTPConnection('1.2.3.4')
+ con = httpplus.HTTPConnection('1.2.3.4')
+ con._connect()
+ con.sock.data = ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'Connection: Close\r\n',
+ '\r\n'
+ '1\n2\nabcdefg\n4\n5']
+ con.sock.close_on_empty = True
+
+ expected_req = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
+ con.request('GET', '/')
+ self.assertEqual(('1.2.3.4', 80), con.sock.sa)
+ self.assertEqual(expected_req, con.sock.sent)
+ r = con.getresponse()
+ for expected in ['1\n', '2\n', 'abcdefg\n', '4\n', '5']:
+ actual = r.readline()
+ self.assertEqual(expected, actual,
+ 'Expected %r, got %r' % (expected, actual))
+
+ def testReadlineTrickle(self):
+ con = httpplus.HTTPConnection('1.2.3.4')
con._connect()
# make sure it trickles in one byte at a time
# so that we touch all the cases in readline
@@ -179,6 +203,7 @@
'Connection: Close\r\n',
'\r\n'
'1\n2\nabcdefg\n4\n5']))
+ con.sock.close_on_empty = True
expected_req = ('GET / HTTP/1.1\r\n'
'Host: 1.2.3.4\r\n'
@@ -193,6 +218,59 @@
self.assertEqual(expected, actual,
'Expected %r, got %r' % (expected, actual))
+ def testVariousReads(self):
+ con = httpplus.HTTPConnection('1.2.3.4')
+ con._connect()
+ # make sure it trickles in one byte at a time
+ # so that we touch all the cases in readline
+ con.sock.data = list(''.join(
+ ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'Connection: Close\r\n',
+ '\r\n'
+ '1\n2',
+ '\na', 'bc',
+ 'defg\n4\n5']))
+ con.sock.close_on_empty = True
+
+ expected_req = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
+ con.request('GET', '/')
+ self.assertEqual(('1.2.3.4', 80), con.sock.sa)
+ self.assertEqual(expected_req, con.sock.sent)
+ r = con.getresponse()
+ for read_amt, expect in [(1, '1'), (1, '\n'),
+ (4, '2\nab'),
+ ('line', 'cdefg\n'),
+ (None, '4\n5')]:
+ if read_amt == 'line':
+ self.assertEqual(expect, r.readline())
+ else:
+ self.assertEqual(expect, r.read(read_amt))
+
+ def testZeroLengthBody(self):
+ con = httpplus.HTTPConnection('1.2.3.4')
+ con._connect()
+ # make sure it trickles in one byte at a time
+ # so that we touch all the cases in readline
+ con.sock.data = list(''.join(
+ ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'Content-length: 0\r\n',
+ '\r\n']))
+
+ expected_req = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
+ con.request('GET', '/')
+ self.assertEqual(('1.2.3.4', 80), con.sock.sa)
+ self.assertEqual(expected_req, con.sock.sent)
+ r = con.getresponse()
+ self.assertEqual('', r.read())
+
def testIPv6(self):
self._run_simple_test('[::1]:8221',
['HTTP/1.1 200 OK\r\n',
@@ -226,7 +304,7 @@
'1234567890')
def testEarlyContinueResponse(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 403 Forbidden\r\n',
@@ -240,8 +318,23 @@
self.assertEqual("You can't do that.", con.getresponse().read())
self.assertEqual(sock.closed, True)
+ def testEarlyContinueResponseNoContentLength(self):
+ con = httpplus.HTTPConnection('1.2.3.4:80')
+ con._connect()
+ sock = con.sock
+ sock.data = ['HTTP/1.1 403 Forbidden\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ '\r\n'
+ "You can't do that."]
+ sock.close_on_empty = True
+ expected_req = self.doPost(con, expect_body=False)
+ self.assertEqual(('1.2.3.4', 80), sock.sa)
+ self.assertStringEqual(expected_req, sock.sent)
+ self.assertEqual("You can't do that.", con.getresponse().read())
+ self.assertEqual(sock.closed, True)
+
def testDeniedAfterContinueTimeoutExpires(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 403 Forbidden\r\n',
@@ -269,7 +362,7 @@
self.assertEqual(sock.closed, True)
def testPostData(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.read_wait_sentinel = 'POST data'
@@ -286,7 +379,7 @@
self.assertEqual(sock.closed, False)
def testServerWithoutContinue(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.read_wait_sentinel = 'POST data'
@@ -302,7 +395,7 @@
self.assertEqual(sock.closed, False)
def testServerWithSlowContinue(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.read_wait_sentinel = 'POST data'
@@ -321,7 +414,7 @@
self.assertEqual(sock.closed, False)
def testSlowConnection(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
# simulate one byte arriving at a time, to check for various
# corner cases
@@ -340,12 +433,26 @@
self.assertEqual(expected_req, con.sock.sent)
self.assertEqual('1234567890', con.getresponse().read())
+ def testCloseAfterNotAllOfHeaders(self):
+ con = httpplus.HTTPConnection('1.2.3.4:80')
+ con._connect()
+ con.sock.data = ['HTTP/1.1 200 OK\r\n',
+ 'Server: NO CARRIER']
+ con.sock.close_on_empty = True
+ con.request('GET', '/')
+ self.assertRaises(httpplus.HTTPRemoteClosedError,
+ con.getresponse)
+
+ expected_req = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
def testTimeout(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
con.sock.data = []
con.request('GET', '/')
- self.assertRaises(http.HTTPTimeoutException,
+ self.assertRaises(httpplus.HTTPTimeoutException,
con.getresponse)
expected_req = ('GET / HTTP/1.1\r\n'
@@ -370,7 +477,7 @@
return s
socket.socket = closingsocket
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
con.request('GET', '/')
r1 = con.getresponse()
@@ -381,7 +488,7 @@
self.assertEqual(2, len(sockets))
def test_server_closes_before_end_of_body(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
s = con.sock
s.data = ['HTTP/1.1 200 OK\r\n',
@@ -393,9 +500,9 @@
s.close_on_empty = True
con.request('GET', '/')
r1 = con.getresponse()
- self.assertRaises(http.HTTPRemoteClosedError, r1.read)
+ self.assertRaises(httpplus.HTTPRemoteClosedError, r1.read)
def test_no_response_raises_response_not_ready(self):
- con = http.HTTPConnection('foo')
- self.assertRaises(http.httplib.ResponseNotReady, con.getresponse)
+ con = httpplus.HTTPConnection('foo')
+ self.assertRaises(httpplus.httplib.ResponseNotReady, con.getresponse)
# no-check-code
--- a/mercurial/httpclient/tests/test_bogus_responses.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/test_bogus_responses.py Sat May 12 13:20:26 2012 +0200
@@ -34,7 +34,7 @@
"""
import unittest
-import http
+import httpplus
# relative import to ease embedding the library
import util
@@ -43,7 +43,7 @@
class SimpleHttpTest(util.HttpTestBase, unittest.TestCase):
def bogusEOL(self, eol):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
con.sock.data = ['HTTP/1.1 200 OK%s' % eol,
'Server: BogusServer 1.0%s' % eol,
--- a/mercurial/httpclient/tests/test_chunked_transfer.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/test_chunked_transfer.py Sat May 12 13:20:26 2012 +0200
@@ -29,7 +29,7 @@
import cStringIO
import unittest
-import http
+import httpplus
# relative import to ease embedding the library
import util
@@ -50,7 +50,7 @@
class ChunkedTransferTest(util.HttpTestBase, unittest.TestCase):
def testChunkedUpload(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.read_wait_sentinel = '0\r\n\r\n'
@@ -77,7 +77,7 @@
self.assertEqual(sock.closed, False)
def testChunkedDownload(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 200 OK\r\n',
@@ -85,14 +85,31 @@
'transfer-encoding: chunked',
'\r\n\r\n',
chunkedblock('hi '),
- chunkedblock('there'),
+ ] + list(chunkedblock('there')) + [
chunkedblock(''),
]
con.request('GET', '/')
self.assertStringEqual('hi there', con.getresponse().read())
+ def testChunkedDownloadOddReadBoundaries(self):
+ con = httpplus.HTTPConnection('1.2.3.4:80')
+ con._connect()
+ sock = con.sock
+ sock.data = ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'transfer-encoding: chunked',
+ '\r\n\r\n',
+ chunkedblock('hi '),
+ ] + list(chunkedblock('there')) + [
+ chunkedblock(''),
+ ]
+ con.request('GET', '/')
+ resp = con.getresponse()
+ for amt, expect in [(1, 'h'), (5, 'i the'), (100, 're')]:
+ self.assertEqual(expect, resp.read(amt))
+
def testChunkedDownloadBadEOL(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 200 OK\n',
@@ -107,7 +124,7 @@
self.assertStringEqual('hi there', con.getresponse().read())
def testChunkedDownloadPartialChunkBadEOL(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 200 OK\n',
@@ -122,7 +139,7 @@
con.getresponse().read())
def testChunkedDownloadPartialChunk(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
sock.data = ['HTTP/1.1 200 OK\r\n',
@@ -136,7 +153,7 @@
con.getresponse().read())
def testChunkedDownloadEarlyHangup(self):
- con = http.HTTPConnection('1.2.3.4:80')
+ con = httpplus.HTTPConnection('1.2.3.4:80')
con._connect()
sock = con.sock
broken = chunkedblock('hi'*20)[:-1]
@@ -149,5 +166,5 @@
sock.close_on_empty = True
con.request('GET', '/')
resp = con.getresponse()
- self.assertRaises(http.HTTPRemoteClosedError, resp.read)
+ self.assertRaises(httpplus.HTTPRemoteClosedError, resp.read)
# no-check-code
--- a/mercurial/httpclient/tests/test_proxy_support.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/test_proxy_support.py Sat May 12 13:20:26 2012 +0200
@@ -29,13 +29,13 @@
import unittest
import socket
-import http
+import httpplus
# relative import to ease embedding the library
import util
-def make_preloaded_socket(data):
+def make_preloaded_socket(data, close=False):
"""Make a socket pre-loaded with data so it can be read during connect.
Useful for https proxy tests because we have to read from the
@@ -44,6 +44,7 @@
def s(*args, **kwargs):
sock = util.MockSocket(*args, **kwargs)
sock.early_data = data[:]
+ sock.close_on_empty = close
return sock
return s
@@ -51,7 +52,7 @@
class ProxyHttpTest(util.HttpTestBase, unittest.TestCase):
def _run_simple_test(self, host, server_data, expected_req, expected_data):
- con = http.HTTPConnection(host)
+ con = httpplus.HTTPConnection(host)
con._connect()
con.sock.data = server_data
con.request('GET', '/')
@@ -60,7 +61,7 @@
self.assertEqual(expected_data, con.getresponse().read())
def testSimpleRequest(self):
- con = http.HTTPConnection('1.2.3.4:80',
+ con = httpplus.HTTPConnection('1.2.3.4:80',
proxy_hostport=('magicproxy', 4242))
con._connect()
con.sock.data = ['HTTP/1.1 200 OK\r\n',
@@ -88,7 +89,7 @@
resp.headers.getheaders('server'))
def testSSLRequest(self):
- con = http.HTTPConnection('1.2.3.4:443',
+ con = httpplus.HTTPConnection('1.2.3.4:443',
proxy_hostport=('magicproxy', 4242))
socket.socket = make_preloaded_socket(
['HTTP/1.1 200 OK\r\n',
@@ -124,12 +125,47 @@
self.assertEqual(['BogusServer 1.0'],
resp.headers.getheaders('server'))
- def testSSLProxyFailure(self):
- con = http.HTTPConnection('1.2.3.4:443',
+ def testSSLRequestNoConnectBody(self):
+ con = httpplus.HTTPConnection('1.2.3.4:443',
proxy_hostport=('magicproxy', 4242))
socket.socket = make_preloaded_socket(
- ['HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'])
- self.assertRaises(http.HTTPProxyConnectFailedException, con._connect)
- self.assertRaises(http.HTTPProxyConnectFailedException,
+ ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ '\r\n'])
+ con._connect()
+ con.sock.data = ['HTTP/1.1 200 OK\r\n',
+ 'Server: BogusServer 1.0\r\n',
+ 'Content-Length: 10\r\n',
+ '\r\n'
+ '1234567890'
+ ]
+ connect_sent = con.sock.sent
+ con.sock.sent = ''
+ con.request('GET', '/')
+
+ expected_connect = ('CONNECT 1.2.3.4:443 HTTP/1.0\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n'
+ '\r\n')
+ expected_request = ('GET / HTTP/1.1\r\n'
+ 'Host: 1.2.3.4\r\n'
+ 'accept-encoding: identity\r\n\r\n')
+
+ self.assertEqual(('127.0.0.42', 4242), con.sock.sa)
+ self.assertStringEqual(expected_connect, connect_sent)
+ self.assertStringEqual(expected_request, con.sock.sent)
+ resp = con.getresponse()
+ self.assertEqual(resp.status, 200)
+ self.assertEqual('1234567890', resp.read())
+ self.assertEqual(['BogusServer 1.0'],
+ resp.headers.getheaders('server'))
+
+ def testSSLProxyFailure(self):
+ con = httpplus.HTTPConnection('1.2.3.4:443',
+ proxy_hostport=('magicproxy', 4242))
+ socket.socket = make_preloaded_socket(
+ ['HTTP/1.1 407 Proxy Authentication Required\r\n\r\n'], close=True)
+ self.assertRaises(httpplus.HTTPProxyConnectFailedException, con._connect)
+ self.assertRaises(httpplus.HTTPProxyConnectFailedException,
con.request, 'GET', '/')
# no-check-code
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/httpclient/tests/test_readers.py Sat May 12 13:20:26 2012 +0200
@@ -0,0 +1,70 @@
+# Copyright 2010, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import unittest
+
+from httpplus import _readers
+
+def chunkedblock(x, eol='\r\n'):
+ r"""Make a chunked transfer-encoding block.
+
+ >>> chunkedblock('hi')
+ '2\r\nhi\r\n'
+ >>> chunkedblock('hi' * 10)
+ '14\r\nhihihihihihihihihihi\r\n'
+ >>> chunkedblock('hi', eol='\n')
+ '2\nhi\n'
+ """
+ return ''.join((hex(len(x))[2:], eol, x, eol))
+
+corpus = 'foo\r\nbar\r\nbaz\r\n'
+
+
+class ChunkedReaderTest(unittest.TestCase):
+ def test_many_block_boundaries(self):
+ for step in xrange(1, len(corpus)):
+ data = ''.join(chunkedblock(corpus[start:start+step]) for
+ start in xrange(0, len(corpus), step))
+ for istep in xrange(1, len(data)):
+ rdr = _readers.ChunkedReader('\r\n')
+ print 'step', step, 'load', istep
+ for start in xrange(0, len(data), istep):
+ rdr._load(data[start:start+istep])
+ rdr._load(chunkedblock(''))
+ self.assertEqual(corpus, rdr.read(len(corpus) + 1))
+
+ def test_small_chunk_blocks_large_wire_blocks(self):
+ data = ''.join(map(chunkedblock, corpus)) + chunkedblock('')
+ rdr = _readers.ChunkedReader('\r\n')
+ for start in xrange(0, len(data), 4):
+ d = data[start:start + 4]
+ if d:
+ rdr._load(d)
+ self.assertEqual(corpus, rdr.read(len(corpus)+100))
+# no-check-code
--- a/mercurial/httpclient/tests/test_ssl.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/test_ssl.py Sat May 12 13:20:26 2012 +0200
@@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import unittest
-import http
+import httpplus
# relative import to ease embedding the library
import util
@@ -37,7 +37,7 @@
class HttpSslTest(util.HttpTestBase, unittest.TestCase):
def testSslRereadRequired(self):
- con = http.HTTPConnection('1.2.3.4:443')
+ con = httpplus.HTTPConnection('1.2.3.4:443')
con._connect()
# extend the list instead of assign because of how
# MockSSLSocket works.
@@ -66,7 +66,7 @@
resp.headers.getheaders('server'))
def testSslRereadInEarlyResponse(self):
- con = http.HTTPConnection('1.2.3.4:443')
+ con = httpplus.HTTPConnection('1.2.3.4:443')
con._connect()
con.sock.early_data = ['HTTP/1.1 200 OK\r\n',
'Server: BogusServer 1.0\r\n',
--- a/mercurial/httpclient/tests/util.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/httpclient/tests/util.py Sat May 12 13:20:26 2012 +0200
@@ -29,7 +29,7 @@
import difflib
import socket
-import http
+import httpplus
class MockSocket(object):
@@ -57,7 +57,7 @@
self.remote_closed = self.closed = False
self.close_on_empty = False
self.sent = ''
- self.read_wait_sentinel = http._END_HEADERS
+ self.read_wait_sentinel = httpplus._END_HEADERS
def close(self):
self.closed = True
@@ -86,7 +86,7 @@
@property
def ready_for_read(self):
- return ((self.early_data and http._END_HEADERS in self.sent)
+ return ((self.early_data and httpplus._END_HEADERS in self.sent)
or (self.read_wait_sentinel in self.sent and self.data)
or self.closed or self.remote_closed)
@@ -132,7 +132,7 @@
def mocksslwrap(sock, keyfile=None, certfile=None,
- server_side=False, cert_reqs=http.socketutil.CERT_NONE,
+ server_side=False, cert_reqs=httpplus.socketutil.CERT_NONE,
ssl_version=None, ca_certs=None,
do_handshake_on_connect=True,
suppress_ragged_eofs=True):
@@ -156,16 +156,16 @@
self.orig_getaddrinfo = socket.getaddrinfo
socket.getaddrinfo = mockgetaddrinfo
- self.orig_select = http.select.select
- http.select.select = mockselect
+ self.orig_select = httpplus.select.select
+ httpplus.select.select = mockselect
- self.orig_sslwrap = http.socketutil.wrap_socket
- http.socketutil.wrap_socket = mocksslwrap
+ self.orig_sslwrap = httpplus.socketutil.wrap_socket
+ httpplus.socketutil.wrap_socket = mocksslwrap
def tearDown(self):
socket.socket = self.orig_socket
- http.select.select = self.orig_select
- http.socketutil.wrap_socket = self.orig_sslwrap
+ httpplus.select.select = self.orig_select
+ httpplus.socketutil.wrap_socket = self.orig_sslwrap
socket.getaddrinfo = self.orig_getaddrinfo
def assertStringEqual(self, l, r):
--- a/mercurial/localrepo.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/localrepo.py Sat May 12 13:20:26 2012 +0200
@@ -41,7 +41,6 @@
self.wopener = scmutil.opener(self.root)
self.baseui = baseui
self.ui = baseui.copy()
- self._dirtyphases = False
# A list of callback to shape the phase if no data were found.
# Callback are in the form: func(repo, roots) --> processed root.
# This list it to be filled by extension during repo setup
@@ -182,23 +181,8 @@
bookmarks.write(self)
@storecache('phaseroots')
- def _phaseroots(self):
- self._dirtyphases = False
- phaseroots = phases.readroots(self)
- phases.filterunknown(self, phaseroots)
- return phaseroots
-
- @propertycache
- def _phaserev(self):
- cache = [phases.public] * len(self)
- for phase in phases.trackedphases:
- roots = map(self.changelog.rev, self._phaseroots[phase])
- if roots:
- for rev in roots:
- cache[rev] = phase
- for rev in self.changelog.descendants(*roots):
- cache[rev] = phase
- return cache
+ def _phasecache(self):
+ return phases.phasecache(self, self._phasedefaults)
@storecache('00changelog.i')
def changelog(self):
@@ -505,7 +489,7 @@
partial = self._branchcache
self._branchtags(partial, lrev)
- # this private cache holds all heads (not just tips)
+ # this private cache holds all heads (not just the branch tips)
self._branchcache = partial
def branchmap(self):
@@ -585,8 +569,8 @@
latest = newnodes.pop()
if latest not in bheads:
continue
- minbhrev = self[bheads[0]].node()
- reachable = self.changelog.reachable(latest, minbhrev)
+ minbhnode = self[bheads[0]].node()
+ reachable = self.changelog.reachable(latest, minbhnode)
reachable.remove(latest)
if reachable:
bheads = [b for b in bheads if b not in reachable]
@@ -605,10 +589,11 @@
def known(self, nodes):
nm = self.changelog.nodemap
+ pc = self._phasecache
result = []
for n in nodes:
r = nm.get(n)
- resp = not (r is None or self._phaserev[r] >= phases.secret)
+ resp = not (r is None or pc.phase(self, r) >= phases.secret)
result.append(resp)
return result
@@ -864,7 +849,6 @@
pass
delcache('_tagscache')
- delcache('_phaserev')
self._branchcache = None # in UTF-8
self._branchcachetip = None
@@ -932,9 +916,8 @@
def unlock():
self.store.write()
- if self._dirtyphases:
- phases.writeroots(self)
- self._dirtyphases = False
+ if '_phasecache' in vars(self):
+ self._phasecache.write()
for k, ce in self._filecache.items():
if k == 'dirstate':
continue
@@ -1334,6 +1317,8 @@
def mfmatches(ctx):
mf = ctx.manifest().copy()
+ if match.always():
+ return mf
for fn in mf.keys():
if not match(fn):
del mf[fn]
@@ -1419,10 +1404,11 @@
mf2 = mfmatches(ctx2)
modified, added, clean = [], [], []
+ withflags = mf1.withflags() | mf2.withflags()
for fn in mf2:
if fn in mf1:
if (fn not in deleted and
- (mf1.flags(fn) != mf2.flags(fn) or
+ ((fn in withflags and mf1.flags(fn) != mf2.flags(fn)) or
(mf1[fn] != mf2[fn] and
(mf2[fn] or ctx1[fn].cmp(ctx2[fn]))))):
modified.append(fn)
@@ -1694,7 +1680,7 @@
# * missingheads part of comon (::commonheads)
common = set(outgoing.common)
cheads = [node for node in revs if node in common]
- # and
+ # and
# * commonheads parents on missing
revset = self.set('%ln and parents(roots(%ln))',
outgoing.commonheads,
--- a/mercurial/manifest.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/manifest.py Sat May 12 13:20:26 2012 +0200
@@ -19,6 +19,8 @@
self._flags = flags
def flags(self, f):
return self._flags.get(f, "")
+ def withflags(self):
+ return set(self._flags.keys())
def set(self, f, flags):
self._flags[f] = flags
def copy(self):
--- a/mercurial/match.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/match.py Sat May 12 13:20:26 2012 +0200
@@ -118,6 +118,8 @@
return self._files
def anypats(self):
return self._anypats
+ def always(self):
+ return False
class exact(match):
def __init__(self, root, cwd, files):
@@ -126,6 +128,8 @@
class always(match):
def __init__(self, root, cwd):
match.__init__(self, root, cwd, [])
+ def always(self):
+ return True
class narrowmatcher(match):
"""Adapt a matcher to work on a subdirectory only.
--- a/mercurial/parsers.c Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/parsers.c Sat May 12 13:20:26 2012 +0200
@@ -387,7 +387,7 @@
Py_ssize_t length = index_length(self);
const char *data;
- if (pos == length - 1)
+ if (pos == length - 1 || pos == INT_MAX)
return nullid;
if (pos >= length)
@@ -508,13 +508,13 @@
return NULL;
#define istat(__n, __d) \
- if (PyDict_SetItemString(obj, __d, PyInt_FromLong(self->__n)) == -1) \
+ if (PyDict_SetItemString(obj, __d, PyInt_FromSsize_t(self->__n)) == -1) \
goto bail;
if (self->added) {
Py_ssize_t len = PyList_GET_SIZE(self->added);
if (PyDict_SetItemString(obj, "index entries added",
- PyInt_FromLong(len)) == -1)
+ PyInt_FromSsize_t(len)) == -1)
goto bail;
}
@@ -553,9 +553,11 @@
* -2: not found
* rest: valid rev
*/
-static int nt_find(indexObject *self, const char *node, Py_ssize_t nodelen)
+static int nt_find(indexObject *self, const char *node, Py_ssize_t nodelen,
+ int hex)
{
- int level, off;
+ int (*getnybble)(const char *, Py_ssize_t) = hex ? hexdigit : nt_level;
+ int level, maxlevel, off;
if (nodelen == 20 && node[0] == '\0' && memcmp(node, nullid, 20) == 0)
return -1;
@@ -563,19 +565,28 @@
if (self->nt == NULL)
return -2;
- for (level = off = 0; level < nodelen; level++) {
- int k = nt_level(node, level);
+ if (hex)
+ maxlevel = nodelen > 40 ? 40 : (int)nodelen;
+ else
+ maxlevel = nodelen > 20 ? 40 : ((int)nodelen * 2);
+
+ for (level = off = 0; level < maxlevel; level++) {
+ int k = getnybble(node, level);
nodetree *n = &self->nt[off];
int v = n->children[k];
if (v < 0) {
const char *n;
+ Py_ssize_t i;
+
v = -v - 1;
n = index_node(self, v);
if (n == NULL)
return -2;
- return memcmp(node, n, nodelen > 20 ? 20 : nodelen)
- ? -2 : v;
+ for (i = level; i < maxlevel; i++)
+ if (getnybble(node, i) != nt_level(n, i))
+ return -2;
+ return v;
}
if (v == 0)
return -2;
@@ -606,7 +617,7 @@
int level = 0;
int off = 0;
- while (level < 20) {
+ while (level < 40) {
int k = nt_level(node, level);
nodetree *n;
int v;
@@ -660,6 +671,8 @@
self->ntrev = (int)index_length(self) - 1;
self->ntlookups = 1;
self->ntmisses = 0;
+ if (nt_insert(self, nullid, INT_MAX) == -1)
+ return -1;
}
return 0;
}
@@ -677,7 +690,7 @@
int rev;
self->ntlookups++;
- rev = nt_find(self, node, nodelen);
+ rev = nt_find(self, node, nodelen, 0);
if (rev >= -1)
return rev;
@@ -782,6 +795,77 @@
return NULL;
}
+static int nt_partialmatch(indexObject *self, const char *node,
+ Py_ssize_t nodelen)
+{
+ int rev;
+
+ if (nt_init(self) == -1)
+ return -3;
+
+ if (self->ntrev > 0) {
+ /* ensure that the radix tree is fully populated */
+ for (rev = self->ntrev - 1; rev >= 0; rev--) {
+ const char *n = index_node(self, rev);
+ if (n == NULL)
+ return -2;
+ if (nt_insert(self, n, rev) == -1)
+ return -3;
+ }
+ self->ntrev = rev;
+ }
+
+ return nt_find(self, node, nodelen, 1);
+}
+
+static PyObject *index_partialmatch(indexObject *self, PyObject *args)
+{
+ const char *fullnode;
+ int nodelen;
+ char *node;
+ int rev, i;
+
+ if (!PyArg_ParseTuple(args, "s#", &node, &nodelen))
+ return NULL;
+
+ if (nodelen < 4) {
+ PyErr_SetString(PyExc_ValueError, "key too short");
+ return NULL;
+ }
+
+ if (nodelen > 40)
+ nodelen = 40;
+
+ for (i = 0; i < nodelen; i++)
+ hexdigit(node, i);
+ if (PyErr_Occurred()) {
+ /* input contains non-hex characters */
+ PyErr_Clear();
+ Py_RETURN_NONE;
+ }
+
+ rev = nt_partialmatch(self, node, nodelen);
+
+ switch (rev) {
+ case -4:
+ raise_revlog_error();
+ case -3:
+ return NULL;
+ case -2:
+ Py_RETURN_NONE;
+ case -1:
+ return PyString_FromStringAndSize(nullid, 20);
+ }
+
+ fullnode = index_node(self, rev);
+ if (fullnode == NULL) {
+ PyErr_Format(PyExc_IndexError,
+ "could not access rev %d", rev);
+ return NULL;
+ }
+ return PyString_FromStringAndSize(fullnode, 20);
+}
+
static PyObject *index_m_get(indexObject *self, PyObject *args)
{
char *node;
@@ -1064,6 +1148,8 @@
"get an index entry"},
{"insert", (PyCFunction)index_insert, METH_VARARGS,
"insert an index entry"},
+ {"partialmatch", (PyCFunction)index_partialmatch, METH_VARARGS,
+ "match a potentially ambiguous node ID"},
{"stats", (PyCFunction)index_stats, METH_NOARGS,
"stats for the index"},
{NULL} /* Sentinel */
--- a/mercurial/patch.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/patch.py Sat May 12 13:20:26 2012 +0200
@@ -1014,9 +1014,9 @@
oldstart = self.starta + top
newstart = self.startb + top
# zero length hunk ranges already have their start decremented
- if self.lena:
+ if self.lena and oldstart > 0:
oldstart -= 1
- if self.lenb:
+ if self.lenb and newstart > 0:
newstart -= 1
return old, oldstart, new, newstart
--- a/mercurial/phases.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/phases.py Sat May 12 13:20:26 2012 +0200
@@ -99,15 +99,44 @@
"""
import errno
-from node import nullid, bin, hex, short
+from node import nullid, nullrev, bin, hex, short
from i18n import _
+import util
allphases = public, draft, secret = range(3)
trackedphases = allphases[1:]
phasenames = ['public', 'draft', 'secret']
-def readroots(repo):
- """Read phase roots from disk"""
+def _filterunknown(ui, changelog, phaseroots):
+ """remove unknown nodes from the phase boundary
+
+ Nothing is lost as unknown nodes only hold data for their descendants
+ """
+ updated = False
+ nodemap = changelog.nodemap # to filter unknown nodes
+ for phase, nodes in enumerate(phaseroots):
+ missing = [node for node in nodes if node not in nodemap]
+ if missing:
+ for mnode in missing:
+ ui.debug(
+ 'removing unknown node %s from %i-phase boundary\n'
+ % (short(mnode), phase))
+ nodes.symmetric_difference_update(missing)
+ updated = True
+ return updated
+
+def _readroots(repo, phasedefaults=None):
+ """Read phase roots from disk
+
+ phasedefaults is a list of fn(repo, roots) callable, which are
+ executed if the phase roots file does not exist. When phases are
+ being initialized on an existing repository, this could be used to
+ set selected changesets phase to something else than public.
+
+ Return (roots, dirty) where dirty is true if roots differ from
+ what is being stored.
+ """
+ dirty = False
roots = [set() for i in allphases]
try:
f = repo.sopener('phaseroots')
@@ -120,39 +149,115 @@
except IOError, inst:
if inst.errno != errno.ENOENT:
raise
- for f in repo._phasedefaults:
- roots = f(repo, roots)
- repo._dirtyphases = True
- return roots
+ if phasedefaults:
+ for f in phasedefaults:
+ roots = f(repo, roots)
+ dirty = True
+ if _filterunknown(repo.ui, repo.changelog, roots):
+ dirty = True
+ return roots, dirty
+
+class phasecache(object):
+ def __init__(self, repo, phasedefaults, _load=True):
+ if _load:
+ # Cheap trick to allow shallow-copy without copy module
+ self.phaseroots, self.dirty = _readroots(repo, phasedefaults)
+ self.opener = repo.sopener
+ self._phaserevs = None
+
+ def copy(self):
+ # Shallow copy meant to ensure isolation in
+ # advance/retractboundary(), nothing more.
+ ph = phasecache(None, None, _load=False)
+ ph.phaseroots = self.phaseroots[:]
+ ph.dirty = self.dirty
+ ph.opener = self.opener
+ ph._phaserevs = self._phaserevs
+ return ph
-def writeroots(repo):
- """Write phase roots from disk"""
- f = repo.sopener('phaseroots', 'w', atomictemp=True)
- try:
- for phase, roots in enumerate(repo._phaseroots):
- for h in roots:
- f.write('%i %s\n' % (phase, hex(h)))
- repo._dirtyphases = False
- finally:
- f.close()
+ def replace(self, phcache):
+ for a in 'phaseroots dirty opener _phaserevs'.split():
+ setattr(self, a, getattr(phcache, a))
+
+ def getphaserevs(self, repo, rebuild=False):
+ if rebuild or self._phaserevs is None:
+ revs = [public] * len(repo.changelog)
+ for phase in trackedphases:
+ roots = map(repo.changelog.rev, self.phaseroots[phase])
+ if roots:
+ for rev in roots:
+ revs[rev] = phase
+ for rev in repo.changelog.descendants(*roots):
+ revs[rev] = phase
+ self._phaserevs = revs
+ return self._phaserevs
+
+ def phase(self, repo, rev):
+ # We need a repo argument here to be able to build _phaserev
+ # if necessary. The repository instance is not stored in
+ # phasecache to avoid reference cycles. The changelog instance
+ # is not stored because it is a filecache() property and can
+ # be replaced without us being notified.
+ if rev == nullrev:
+ return public
+ if self._phaserevs is None or rev >= len(self._phaserevs):
+ self._phaserevs = self.getphaserevs(repo, rebuild=True)
+ return self._phaserevs[rev]
-def filterunknown(repo, phaseroots=None):
- """remove unknown nodes from the phase boundary
+ def write(self):
+ if not self.dirty:
+ return
+ f = self.opener('phaseroots', 'w', atomictemp=True)
+ try:
+ for phase, roots in enumerate(self.phaseroots):
+ for h in roots:
+ f.write('%i %s\n' % (phase, hex(h)))
+ finally:
+ f.close()
+ self.dirty = False
+
+ def _updateroots(self, phase, newroots):
+ self.phaseroots[phase] = newroots
+ self._phaserevs = None
+ self.dirty = True
+
+ def advanceboundary(self, repo, targetphase, nodes):
+ # Be careful to preserve shallow-copied values: do not update
+ # phaseroots values, replace them.
- no data is lost as unknown node only old data for their descentants
- """
- if phaseroots is None:
- phaseroots = repo._phaseroots
- nodemap = repo.changelog.nodemap # to filter unknown nodes
- for phase, nodes in enumerate(phaseroots):
- missing = [node for node in nodes if node not in nodemap]
- if missing:
- for mnode in missing:
- repo.ui.debug(
- 'removing unknown node %s from %i-phase boundary\n'
- % (short(mnode), phase))
- nodes.symmetric_difference_update(missing)
- repo._dirtyphases = True
+ delroots = [] # set of root deleted by this path
+ for phase in xrange(targetphase + 1, len(allphases)):
+ # filter nodes that are not in a compatible phase already
+ nodes = [n for n in nodes
+ if self.phase(repo, repo[n].rev()) >= phase]
+ if not nodes:
+ break # no roots to move anymore
+ olds = self.phaseroots[phase]
+ roots = set(ctx.node() for ctx in repo.set(
+ 'roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
+ if olds != roots:
+ self._updateroots(phase, roots)
+ # some roots may need to be declared for lower phases
+ delroots.extend(olds - roots)
+ # declare deleted root in the target phase
+ if targetphase != 0:
+ self.retractboundary(repo, targetphase, delroots)
+
+ def retractboundary(self, repo, targetphase, nodes):
+ # Be careful to preserve shallow-copied values: do not update
+ # phaseroots values, replace them.
+
+ currentroots = self.phaseroots[targetphase]
+ newroots = [n for n in nodes
+ if self.phase(repo, repo[n].rev()) < targetphase]
+ if newroots:
+ if nullid in newroots:
+ raise util.Abort(_('cannot change null revision phase'))
+ currentroots = currentroots.copy()
+ currentroots.update(newroots)
+ ctxs = repo.set('roots(%ln::)', currentroots)
+ currentroots.intersection_update(ctx.node() for ctx in ctxs)
+ self._updateroots(targetphase, currentroots)
def advanceboundary(repo, targetphase, nodes):
"""Add nodes to a phase changing other nodes phases if necessary.
@@ -161,30 +266,9 @@
in the target phase or kept in a *lower* phase.
Simplify boundary to contains phase roots only."""
- delroots = [] # set of root deleted by this path
- for phase in xrange(targetphase + 1, len(allphases)):
- # filter nodes that are not in a compatible phase already
- # XXX rev phase cache might have been invalidated by a previous loop
- # XXX we need to be smarter here
- nodes = [n for n in nodes if repo[n].phase() >= phase]
- if not nodes:
- break # no roots to move anymore
- roots = repo._phaseroots[phase]
- olds = roots.copy()
- ctxs = list(repo.set('roots((%ln::) - (%ln::%ln))', olds, olds, nodes))
- roots.clear()
- roots.update(ctx.node() for ctx in ctxs)
- if olds != roots:
- # invalidate cache (we probably could be smarter here
- if '_phaserev' in vars(repo):
- del repo._phaserev
- repo._dirtyphases = True
- # some roots may need to be declared for lower phases
- delroots.extend(olds - roots)
- # declare deleted root in the target phase
- if targetphase != 0:
- retractboundary(repo, targetphase, delroots)
-
+ phcache = repo._phasecache.copy()
+ phcache.advanceboundary(repo, targetphase, nodes)
+ repo._phasecache.replace(phcache)
def retractboundary(repo, targetphase, nodes):
"""Set nodes back to a phase changing other nodes phases if necessary.
@@ -193,22 +277,15 @@
in the target phase or kept in a *higher* phase.
Simplify boundary to contains phase roots only."""
- currentroots = repo._phaseroots[targetphase]
- newroots = [n for n in nodes if repo[n].phase() < targetphase]
- if newroots:
- currentroots.update(newroots)
- ctxs = repo.set('roots(%ln::)', currentroots)
- currentroots.intersection_update(ctx.node() for ctx in ctxs)
- if '_phaserev' in vars(repo):
- del repo._phaserev
- repo._dirtyphases = True
-
+ phcache = repo._phasecache.copy()
+ phcache.retractboundary(repo, targetphase, nodes)
+ repo._phasecache.replace(phcache)
def listphases(repo):
"""List phases root for serialisation over pushkey"""
keys = {}
value = '%i' % draft
- for root in repo._phaseroots[draft]:
+ for root in repo._phasecache.phaseroots[draft]:
keys[hex(root)] = value
if repo.ui.configbool('phases', 'publish', True):
@@ -251,7 +328,7 @@
def visibleheads(repo):
"""return the set of visible head of this repo"""
# XXX we want a cache on this
- sroots = repo._phaseroots[secret]
+ sroots = repo._phasecache.phaseroots[secret]
if sroots:
# XXX very slow revset. storing heads or secret "boundary" would help.
revset = repo.set('heads(not (%ln::))', sroots)
@@ -267,7 +344,7 @@
"""return a branchmap for the visible set"""
# XXX Recomputing this data on the fly is very slow. We should build a
# XXX cached version while computin the standard branchmap version.
- sroots = repo._phaseroots[secret]
+ sroots = repo._phasecache.phaseroots[secret]
if sroots:
vbranchmap = {}
for branch, nodes in repo.branchmap().iteritems():
--- a/mercurial/repair.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/repair.py Sat May 12 13:20:26 2012 +0200
@@ -6,7 +6,7 @@
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
-from mercurial import changegroup, bookmarks, phases
+from mercurial import changegroup, bookmarks
from mercurial.node import short
from mercurial.i18n import _
import os
@@ -38,14 +38,14 @@
"""return the changesets which will be broken by the truncation"""
s = set()
def collectone(revlog):
- links = (revlog.linkrev(i) for i in revlog)
+ linkgen = (revlog.linkrev(i) for i in revlog)
# find the truncation point of the revlog
- for lrev in links:
+ for lrev in linkgen:
if lrev >= striprev:
break
# see if any revision after this point has a linkrev
# less than striprev (those will be broken by strip)
- for lrev in links:
+ for lrev in linkgen:
if lrev < striprev:
s.add(lrev)
@@ -170,7 +170,3 @@
raise
repo.destroyed()
-
- # remove potential unknown phase
- # XXX using to_strip data would be faster
- phases.filterunknown(repo)
--- a/mercurial/revlog.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/revlog.py Sat May 12 13:20:26 2012 +0200
@@ -756,6 +756,15 @@
pass
def _partialmatch(self, id):
+ try:
+ return self.index.partialmatch(id)
+ except RevlogError:
+ # parsers.c radix tree lookup gave multiple matches
+ raise LookupError(id, self.indexfile, _("ambiguous identifier"))
+ except (AttributeError, ValueError):
+ # we are pure python, or key was too short to search radix tree
+ pass
+
if id in self._pcache:
return self._pcache[id]
--- a/mercurial/revset.py Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/revset.py Sat May 12 13:20:26 2012 +0200
@@ -289,6 +289,7 @@
- ``pruned`` : csets that are goods, bads or skipped
- ``untested`` : csets whose fate is yet unknown
- ``ignored`` : csets ignored due to DAG topology
+ - ``current`` : the cset currently being bisected
"""
status = getstring(x, _("bisect requires a string")).lower()
state = set(hbisect.get(repo, status))
@@ -462,7 +463,26 @@
"""``draft()``
Changeset in draft phase."""
getargs(x, 0, 0, _("draft takes no arguments"))
- return [r for r in subset if repo._phaserev[r] == phases.draft]
+ pc = repo._phasecache
+ return [r for r in subset if pc.phase(repo, r) == phases.draft]
+
+def extra(repo, subset, x):
+ """``extra(label, [value])``
+ Changesets with the given label in the extra metadata, with the given
+ optional value."""
+
+ l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
+ label = getstring(l[0], _('first argument to extra must be a string'))
+ value = None
+
+ if len(l) > 1:
+ value = getstring(l[1], _('second argument to extra must be a string'))
+
+ def _matchvalue(r):
+ extra = repo[r].extra()
+ return label in extra and (value is None or value == extra[label])
+
+ return [r for r in subset if _matchvalue(r)]
def filelog(repo, subset, x):
"""``filelog(pattern)``
@@ -851,7 +871,8 @@
"""``public()``
Changeset in public phase."""
getargs(x, 0, 0, _("public takes no arguments"))
- return [r for r in subset if repo._phaserev[r] == phases.public]
+ pc = repo._phasecache
+ return [r for r in subset if pc.phase(repo, r) == phases.public]
def remote(repo, subset, x):
"""``remote([id [,path]])``
@@ -926,7 +947,7 @@
Special fields are ``summary`` and ``metadata``:
``summary`` matches the first line of the description.
- ``metatadata`` is equivalent to matching ``description user date``
+ ``metadata`` is equivalent to matching ``description user date``
(i.e. it matches the main metadata fields).
``metadata`` is the default field which is used when no fields are
@@ -996,7 +1017,7 @@
# is only one field to match)
getinfo = lambda r: [f(r) for f in getfieldfuncs]
- matches = []
+ matches = set()
for rev in revs:
target = getinfo(rev)
for r in subset:
@@ -1006,10 +1027,8 @@
match = False
break
if match:
- matches.append(r)
- if len(revs) > 1:
- matches = sorted(set(matches))
- return matches
+ matches.add(r)
+ return [r for r in subset if r in matches]
def reverse(repo, subset, x):
"""``reverse(set)``
@@ -1032,7 +1051,8 @@
"""``secret()``
Changeset in secret phase."""
getargs(x, 0, 0, _("secret takes no arguments"))
- return [r for r in subset if repo._phaserev[r] == phases.secret]
+ pc = repo._phasecache
+ return [r for r in subset if pc.phase(repo, r) == phases.secret]
def sort(repo, subset, x):
"""``sort(set[, [-]key...])``
@@ -1145,6 +1165,7 @@
"descendants": descendants,
"_firstdescendants": _firstdescendants,
"draft": draft,
+ "extra": extra,
"file": hasfile,
"filelog": filelog,
"first": first,
--- a/mercurial/util.h Wed Apr 25 01:35:39 2012 +0200
+++ b/mercurial/util.h Sat May 12 13:20:26 2012 +0200
@@ -109,6 +109,7 @@
typedef int Py_ssize_t;
typedef Py_ssize_t (*lenfunc)(PyObject *);
typedef PyObject *(*ssizeargfunc)(PyObject *, Py_ssize_t);
+#define PyInt_FromSsize_t PyInt_FromLong
#if !defined(PY_SSIZE_T_MIN)
#define PY_SSIZE_T_MAX INT_MAX
--- a/tests/test-bisect.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-bisect.t Sat May 12 13:20:26 2012 +0200
@@ -224,6 +224,7 @@
Testing changeset 12:1941b52820a5 (23 changesets remaining, ~4 tests)
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ cat .hg/bisect.state
+ current 1941b52820a544549596820a8ae006842b0e2c64
skip 9d7d07bc967ca98ad0600c24953fd289ad5fa991
skip ce8f0998e922c179e80819d5066fbe46e2998784
skip e7fa0811edb063f6319531f0d0a865882138e180
@@ -396,6 +397,12 @@
date: Thu Jan 01 00:00:06 1970 +0000
summary: msg 6
+ $ hg log -r "bisect(current)"
+ changeset: 5:7874a09ea728
+ user: test
+ date: Thu Jan 01 00:00:05 1970 +0000
+ summary: msg 5
+
$ hg log -r "bisect(skip)"
changeset: 1:5cd978ea5149
user: test
@@ -466,3 +473,40 @@
date: Thu Jan 01 00:00:06 1970 +0000
summary: msg 6
+
+
+test bisecting via a command without updating the working dir, and
+ensure that the bisect state file is updated before running a test
+command
+
+ $ hg update null
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ cat > script.sh <<'EOF'
+ > #!/bin/sh
+ > test -n "$HG_NODE" || (echo HG_NODE missing; exit 127)
+ > current="`hg log -r \"bisect(current)\" --template {node}`"
+ > test "$current" = "$HG_NODE" || (echo current is bad: $current; exit 127)
+ > rev="`hg log -r $HG_NODE --template {rev}`"
+ > test "$rev" -ge 6
+ > EOF
+ $ chmod +x script.sh
+ $ hg bisect -r
+ $ hg bisect --good tip --noupdate
+ $ hg bisect --bad 0 --noupdate
+ Testing changeset 15:e7fa0811edb0 (31 changesets remaining, ~4 tests)
+ $ hg bisect --command "'`pwd`/script.sh' and some params" --noupdate
+ Changeset 15:e7fa0811edb0: good
+ Changeset 7:03750880c6b5: good
+ Changeset 3:b53bea5e2fcb: bad
+ Changeset 5:7874a09ea728: bad
+ Changeset 6:a3d5c6fdf0d3: good
+ The first good revision is:
+ changeset: 6:a3d5c6fdf0d3
+ user: test
+ date: Thu Jan 01 00:00:06 1970 +0000
+ summary: msg 6
+
+
+ensure that we still don't have a working dir
+
+ $ hg parents
--- a/tests/test-check-code-hg.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-check-code-hg.t Sat May 12 13:20:26 2012 +0200
@@ -220,9 +220,6 @@
> raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
warning: line over 80 characters
hgext/mq.py:0:
- > ('', 'move', None, _('reorder patch series and apply only the patch'))],
- warning: line over 80 characters
- hgext/mq.py:0:
> ('U', 'noupdate', None, _('do not update the new working directories')),
warning: line over 80 characters
hgext/mq.py:0:
--- a/tests/test-commit-amend.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-commit-amend.t Sat May 12 13:20:26 2012 +0200
@@ -316,3 +316,37 @@
$ hg rollback
no rollback information available
[1]
+
+Preserve extra dict (issue3430):
+
+ $ hg branch a
+ marked working directory as branch a
+ (branches are permanent and global, did you want a bookmark?)
+ $ echo a >> a
+ $ hg ci -ma
+ $ hg ci --amend -m "a'"
+ saved backup bundle to $TESTTMP/.hg/strip-backup/167f8e3031df-amend-backup.hg
+ $ hg log -r . --template "{branch}\n"
+ a
+ $ hg ci --amend -m "a''"
+ saved backup bundle to $TESTTMP/.hg/strip-backup/ceac1a44c806-amend-backup.hg
+ $ hg log -r . --template "{branch}\n"
+ a
+
+Also preserve other entries in the dict that are in the old commit,
+first graft something so there's an additional entry:
+
+ $ hg up 0 -q
+ $ echo z > z
+ $ hg ci -Am 'fork'
+ adding z
+ created new head
+ $ hg up 11
+ 5 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ $ hg graft 12
+ grafting revision 12
+ $ hg ci --amend -m 'graft amend'
+ saved backup bundle to $TESTTMP/.hg/strip-backup/18a5124daf7a-amend-backup.hg
+ $ hg log -r . --debug | grep extra
+ extra: branch=a
+ extra: source=2647734878ef0236dda712fae9c1651cf694ea8a
--- a/tests/test-debugcomplete.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-debugcomplete.t Sat May 12 13:20:26 2012 +0200
@@ -247,7 +247,7 @@
debugsub: rev
debugwalk: include, exclude
debugwireargs: three, four, five, ssh, remotecmd, insecure
- graft: continue, edit, currentdate, currentuser, date, user, tool, dry-run
+ graft: continue, edit, log, currentdate, currentuser, date, user, tool, dry-run
grep: print0, all, text, follow, ignore-case, files-with-matches, line-number, rev, user, date, include, exclude
heads: rev, topo, active, closed, style, template
help: extension, command
--- a/tests/test-graft.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-graft.t Sat May 12 13:20:26 2012 +0200
@@ -277,3 +277,13 @@
$ hg graft tip
skipping already grafted revision 13 (same origin 2)
[255]
+
+Graft with --log
+
+ $ hg up -Cq 1
+ $ hg graft 3 --log -u foo
+ grafting revision 3
+ warning: can't find ancestor for 'c' copied from 'b'!
+ $ hg log --template '{rev} {parents} {desc}\n' -r tip
+ 14 1:5d205f8b35b6 3
+ (grafted from 4c60f11aa304a54ae1c199feb94e7fc771e51ed8)
--- a/tests/test-import.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-import.t Sat May 12 13:20:26 2012 +0200
@@ -997,6 +997,26 @@
c3
c4
+no segfault while importing a unified diff which start line is zero but chunk
+size is non-zero
+
+ $ hg init startlinezero
+ $ cd startlinezero
+ $ echo foo > foo
+ $ hg commit -Amfoo
+ adding foo
+
+ $ hg import --no-commit - << EOF
+ > diff a/foo b/foo
+ > --- a/foo
+ > +++ b/foo
+ > @@ -0,1 +0,1 @@
+ > foo
+ > EOF
+ applying patch from stdin
+
+ $ cd ..
+
Test corner case involving fuzz and skew
$ hg init morecornercases
--- a/tests/test-keyword.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-keyword.t Sat May 12 13:20:26 2012 +0200
@@ -558,6 +558,7 @@
$ hg --debug commit -ma2c -d '1 0' -u 'User Name <user@example.com>'
c
c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
+ removing unknown node 40a904bbbe4c from 1-phase boundary
overwriting c expanding keywords
committed changeset 2:25736cf2f5cbe41f6be4e6784ef6ecf9f3bbcc7d
$ cat a c
@@ -722,6 +723,7 @@
$ hg --debug commit -l log -d '2 0' -u 'User Name <user@example.com>'
a
+ removing unknown node 40a904bbbe4c from 1-phase boundary
overwriting a expanding keywords
committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83
$ rm log
--- a/tests/test-largefiles.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-largefiles.t Sat May 12 13:20:26 2012 +0200
@@ -432,11 +432,21 @@
large11
$ cat sub/large2
large22
+ $ cd ..
+
+Test cloning with --all-largefiles flag
+
+ $ rm -Rf ${USERCACHE}/*
+ $ hg clone --all-largefiles a a-backup
+ updating to branch default
+ 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ getting changed largefiles
+ 3 largefiles updated, 0 removed
+ 8 additional largefiles cached
Rebasing between two repositories does not revert largefiles to old
revisions (this was a very bad bug that took a lot of work to fix).
- $ cd ..
$ hg clone a d
updating to branch default
5 files updated, 0 files merged, 0 files removed, 0 files unresolved
--- a/tests/test-mq-qpush-fail.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-mq-qpush-fail.t Sat May 12 13:20:26 2012 +0200
@@ -150,3 +150,275 @@
abort: cannot push to a previous patch: a
[255]
+test qpop --force and backup files
+
+ $ hg qpop -a
+ popping b
+ patch queue now empty
+ $ hg qq --create force
+ $ echo a > a
+ $ echo b > b
+ $ echo c > c
+ $ hg ci -Am add a b c
+ $ echo a >> a
+ $ hg rm b
+ $ hg rm c
+ $ hg qnew p1
+ $ echo a >> a
+ $ echo bb > b
+ $ hg add b
+ $ echo cc > c
+ $ hg add c
+ $ hg qpop --force --verbose
+ saving current version of a as a.orig
+ saving current version of b as b.orig
+ saving current version of c as c.orig
+ popping p1
+ patch queue now empty
+ $ hg st
+ ? a.orig
+ ? b.orig
+ ? c.orig
+ ? untracked-file
+ $ cat a.orig
+ a
+ a
+ a
+ $ cat b.orig
+ bb
+ $ cat c.orig
+ cc
+
+test qpop --force --no-backup
+
+ $ hg qpush
+ applying p1
+ now at: p1
+ $ rm a.orig
+ $ echo a >> a
+ $ hg qpop --force --no-backup --verbose
+ popping p1
+ patch queue now empty
+ $ test -f a.orig && echo 'error: backup with --no-backup'
+ [1]
+
+test qpop --check
+
+ $ hg qpush
+ applying p1
+ now at: p1
+ $ hg qpop --check --force
+ abort: cannot use both --force and --check
+ [255]
+ $ echo a >> a
+ $ hg qpop --check
+ abort: local changes found, refresh first
+ [255]
+ $ hg revert -qa a
+ $ rm a
+ $ hg qpop --check
+ abort: local changes found, refresh first
+ [255]
+ $ hg rm -A a
+ $ hg qpop --check
+ abort: local changes found, refresh first
+ [255]
+ $ hg revert -qa a
+ $ echo b > b
+ $ hg add b
+ $ hg qpop --check
+ abort: local changes found, refresh first
+ [255]
+ $ hg forget b
+ $ echo d > d
+ $ hg add d
+ $ hg qpop --check
+ popping p1
+ patch queue now empty
+ $ hg forget d
+ $ rm d
+
+test qpush --force and backup files
+
+ $ echo a >> a
+ $ hg qnew p2
+ $ echo b >> b
+ $ echo d > d
+ $ echo e > e
+ $ hg add d e
+ $ hg rm c
+ $ hg qnew p3
+ $ hg qpop -a
+ popping p3
+ popping p2
+ patch queue now empty
+ $ echo a >> a
+ $ echo b1 >> b
+ $ echo d1 > d
+ $ hg add d
+ $ echo e1 > e
+ $ hg qpush -a --force --verbose
+ applying p2
+ saving current version of a as a.orig
+ patching file a
+ a
+ applying p3
+ saving current version of b as b.orig
+ saving current version of d as d.orig
+ patching file b
+ patching file c
+ patching file d
+ file d already exists
+ 1 out of 1 hunks FAILED -- saving rejects to file d.rej
+ patching file e
+ file e already exists
+ 1 out of 1 hunks FAILED -- saving rejects to file e.rej
+ patch failed to apply
+ b
+ patch failed, rejects left in working dir
+ errors during apply, please fix and refresh p3
+ [2]
+ $ cat a.orig
+ a
+ a
+ $ cat b.orig
+ b
+ b1
+ $ cat d.orig
+ d1
+
+test qpush --force --no-backup
+
+ $ hg revert -qa
+ $ hg qpop -a
+ popping p3
+ popping p2
+ patch queue now empty
+ $ echo a >> a
+ $ rm a.orig
+ $ hg qpush --force --no-backup --verbose
+ applying p2
+ patching file a
+ a
+ now at: p2
+ $ test -f a.orig && echo 'error: backup with --no-backup'
+ [1]
+
+test qgoto --force --no-backup
+
+ $ hg qpop
+ popping p2
+ patch queue now empty
+ $ echo a >> a
+ $ hg qgoto --force --no-backup p2 --verbose
+ applying p2
+ patching file a
+ a
+ now at: p2
+ $ test -f a.orig && echo 'error: backup with --no-backup'
+ [1]
+
+test qpush --check
+
+ $ hg qpush --check --force
+ abort: cannot use both --force and --check
+ [255]
+ $ hg qpush --check --exact
+ abort: cannot use --exact and --check together
+ [255]
+ $ echo b >> b
+ $ hg qpush --check
+ applying p3
+ errors during apply, please fix and refresh p2
+ [2]
+ $ rm b
+ $ hg qpush --check
+ applying p3
+ errors during apply, please fix and refresh p2
+ [2]
+ $ hg rm -A b
+ $ hg qpush --check
+ applying p3
+ errors during apply, please fix and refresh p2
+ [2]
+ $ hg revert -aq b
+ $ echo d > d
+ $ hg add d
+ $ hg qpush --check
+ applying p3
+ errors during apply, please fix and refresh p2
+ [2]
+ $ hg forget d
+ $ rm d
+ $ hg qpop
+ popping p2
+ patch queue now empty
+ $ echo b >> b
+ $ hg qpush -a --check
+ applying p2
+ applying p3
+ errors during apply, please fix and refresh p2
+ [2]
+ $ hg qtop
+ p2
+ $ hg parents --template "{rev} {desc}\n"
+ 2 imported patch p2
+ $ hg st b
+ M b
+ $ cat b
+ b
+ b
+
+test qgoto --check
+
+ $ hg revert -aq b
+ $ rm e
+ $ hg qgoto --check --force p3
+ abort: cannot use both --force and --check
+ [255]
+ $ echo a >> a
+ $ hg qgoto --check p3
+ applying p3
+ now at: p3
+ $ hg st a
+ M a
+ $ hg qgoto --check p2
+ popping p3
+ now at: p2
+ $ hg st a
+ M a
+
+test mq.check setting
+
+ $ hg --config mq.check=1 qpush
+ applying p3
+ now at: p3
+ $ hg st a
+ M a
+ $ hg --config mq.check=1 qpop
+ popping p3
+ now at: p2
+ $ hg st a
+ M a
+ $ hg --config mq.check=1 qgoto p3
+ applying p3
+ now at: p3
+ $ hg st a
+ M a
+ $ echo b >> b
+ $ hg --config mq.check=1 qpop --force
+ popping p3
+ now at: p2
+ $ hg st b
+ $ hg --config mq.check=1 qpush --exact
+ abort: local changes found, refresh first
+ [255]
+ $ hg revert -qa a
+ $ hg qpop
+ popping p2
+ patch queue now empty
+ $ echo a >> a
+ $ hg --config mq.check=1 qpush --force
+ applying p2
+ now at: p2
+ $ hg st a
--- a/tests/test-mq.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-mq.t Sat May 12 13:20:26 2012 +0200
@@ -59,6 +59,15 @@
You will by default be managing a patch queue named "patches". You can create
other, independent patch queues with the "hg qqueue" command.
+ If the working directory contains uncommitted files, qpush, qpop and qgoto
+ abort immediately. If -f/--force is used, the changes are discarded. Setting:
+
+ [mq] check = True
+
+ make them behave as if -c/--check were passed, and non-conflicting local
+ changes will be tolerated and preserved. If incompatible options such as
+ -f/--force or --exact are passed, this setting is ignored.
+
list of commands:
qapplied print the patches already applied
@@ -1356,11 +1365,15 @@
apply force, should discard changes in hello, but not bye
- $ hg qpush -f
+ $ hg qpush -f --verbose
applying empty
+ saving current version of hello.txt as hello.txt.orig
+ patching file hello.txt
+ hello.txt
now at: empty
$ hg st
M bye.txt
+ ? hello.txt.orig
$ hg diff --config diff.nodates=True
diff -r ba252371dbc1 bye.txt
--- a/bye.txt
--- a/tests/test-phases.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-phases.t Sat May 12 13:20:26 2012 +0200
@@ -9,6 +9,15 @@
$ hg init initialrepo
$ cd initialrepo
+
+Cannot change null revision phase
+
+ $ hg phase --force --secret null
+ abort: cannot change null revision phase
+ [255]
+ $ hg phase null
+ -1: public
+
$ mkcommit A
New commit are draft by default
--- a/tests/test-revset.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-revset.t Sat May 12 13:20:26 2012 +0200
@@ -32,6 +32,13 @@
(branches are permanent and global, did you want a bookmark?)
$ hg ci -Aqm2 -u Bob
+ $ hg log -r "extra('branch', 'a-b-c-')" --template '{rev}\n'
+ 2
+ $ hg log -r "extra('branch')" --template '{rev}\n'
+ 0
+ 1
+ 2
+
$ hg co 1
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg branch +a+b+c+
@@ -410,6 +417,10 @@
0
$ log '4::8 - 8'
4
+ $ log 'matching(1 or 2 or 3) and (2 or 3 or 1)'
+ 2
+ 3
+ 1
issue2437
--- a/tests/test-transplant.t Wed Apr 25 01:35:39 2012 +0200
+++ b/tests/test-transplant.t Sat May 12 13:20:26 2012 +0200
@@ -120,7 +120,25 @@
1 r2
0 r1
+test same-parent transplant with --log
+ $ hg clone -r 1 ../t ../sameparent
+ adding changesets
+ adding manifests
+ adding file changes
+ added 2 changesets with 2 changes to 2 files
+ updating to branch default
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cd ../sameparent
+ $ hg transplant --log -s ../prune 5
+ searching for changes
+ applying e234d668f844
+ e234d668f844 transplanted to e07aea8ecf9c
+ $ hg log --template '{rev} {parents} {desc}\n'
+ 2 b1
+ (transplanted from e234d668f844e1b1a765f01db83a32c0c7bfa170)
+ 1 r2
+ 0 r1
remote transplant
$ hg clone -r 1 ../t ../remote