# HG changeset patch # User Matt Mackall # Date 1429235871 18000 # Node ID e89f909edffad558b56f4affa8239e4832f88de0 # Parent 612ed41ae359f458dbdc74f03ce9542d3816859d# Parent 2ee10789d66b9ca500a7bfb1b3e031e04cdc4fb7 merge default into stable for 3.4 freeze diff -r 612ed41ae359 -r e89f909edffa Makefile --- a/Makefile Thu Apr 16 22:33:53 2015 +0900 +++ b/Makefile Thu Apr 16 20:57:51 2015 -0500 @@ -7,11 +7,14 @@ PREFIX=/usr/local export PREFIX PYTHON=python +$(eval HGROOT := $(shell pwd)) +HGPYTHONS ?= $(HGROOT)/build/pythons PURE= PYFILES:=$(shell find mercurial hgext doc -name '*.py') DOCFILES=mercurial/help/*.txt export LANGUAGE=C export LC_ALL=C +TESTFLAGS ?= $(shell echo $$HGTESTFLAGS) # Set this to e.g. "mingw32" to use a non-default compiler. COMPILER= @@ -98,6 +101,13 @@ test-%: cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@ +testpy-%: + @echo Looking for Python $* in $(HGPYTHONS) + [ -e $(HGPYTHONS)/$*/bin/python ] || ( \ + cd $$(mktemp --directory --tmpdir) && \ + $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python ) + cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS) + check-code: hg manifest | xargs python contrib/check-code.py @@ -108,6 +118,7 @@ hgext/*.py hgext/*/__init__.py \ mercurial/fileset.py mercurial/revset.py \ mercurial/templatefilters.py mercurial/templatekw.py \ + mercurial/templater.py \ mercurial/filemerge.py \ $(DOCFILES) > i18n/hg.pot.tmp # All strings marked for translation in Mercurial contain diff -r 612ed41ae359 -r e89f909edffa contrib/buildrpm --- a/contrib/buildrpm Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/buildrpm Thu Apr 16 20:57:51 2015 -0500 @@ -74,6 +74,7 @@ $HG archive -t tgz $RPMBUILDDIR/SOURCES/mercurial-$version-$release.tar.gz if [ "$PYTHONVER" ]; then ( + mkdir -p build cd build PYTHON_SRCFILE=Python-$PYTHONVER.tgz [ -f $PYTHON_SRCFILE ] || curl -Lo $PYTHON_SRCFILE http://www.python.org/ftp/python/$PYTHONVER/$PYTHON_SRCFILE diff -r 612ed41ae359 -r e89f909edffa contrib/check-code.py --- a/contrib/check-code.py Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/check-code.py Thu Apr 16 20:57:51 2015 -0500 @@ -122,6 +122,7 @@ (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)', "put a backslash-escaped newline after sed 'i' command"), (r'^diff *-\w*u.*$\n(^ \$ |^$)', "prefix diff -u with cmp"), + (r'seq ', "don't use 'seq', use $TESTDIR/seq.py") ], # warnings [ @@ -153,7 +154,7 @@ (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"), (uprefix + r'.*:\.\S*/', "x:.y in a path does not work on msys, rewrite " "as x://.y, or see `hg log -k msys` for alternatives", r'-\S+:\.|' #-Rxxx - 'hg pull -q file:../test'), # in test-pull.t which is skipped on windows + '# no-msys'), # in test-pull.t which is skipped on windows (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg), (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$', winglobmsg), @@ -334,6 +335,7 @@ (r'(while|if|do|for)\(', "use space after while/if/do/for"), (r'return\(', "return is not a function"), (r' ;', "no space before ;"), + (r'[^;] \)', "no space before )"), (r'[)][{]', "space between ) and {"), (r'\w+\* \w+', "use int *foo, not int* foo"), (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"), diff -r 612ed41ae359 -r e89f909edffa contrib/check-commit --- a/contrib/check-commit Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/check-commit Thu Apr 16 20:57:51 2015 -0500 @@ -20,11 +20,12 @@ errors = [ (r"[(]bc[)]", "(BC) needs to be uppercase"), (r"[(]issue \d\d\d", "no space allowed between issue and number"), - (r"[(]bug", "use (issueDDDD) instead of bug"), + (r"[(]bug(\d|\s)", "use (issueDDDD) instead of bug"), (r"^# User [^@\n]+$", "username is not an email address"), (r"^# .*\n(?!merge with )[^#]\S+[^:] ", "summary line doesn't start with 'topic: '"), (r"^# .*\n[A-Z][a-z]\S+", "don't capitalize summary lines"), + (r"^# .*\n[^\n]*: *[A-Z][a-z]\S+", "don't capitalize summary lines"), (r"^# .*\n.*\.\s+$", "don't add trailing period on summary line"), (r"^# .*\n.{78,}", "summary line too long"), (r"^\+\n \n", "adds double empty line"), diff -r 612ed41ae359 -r e89f909edffa contrib/hgk --- a/contrib/hgk Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/hgk Thu Apr 16 20:57:51 2015 -0500 @@ -177,18 +177,21 @@ set ncmupdate 1 set limit 0 set revargs {} + set showhidden no for {set i 0} {$i < [llength $rargs]} {incr i} { set opt [lindex $rargs $i] - if {$opt == "--limit"} { + switch -- $opt --limit { incr i set limit [lindex $rargs $i] - } else { + } --hidden { + set showhidden yes + } default { lappend revargs $opt } } if [catch { - set parse_args [concat --default HEAD $revargs] - set parse_temp [eval exec {$env(HG)} --config ui.report_untrusted=false debug-rev-parse $parse_args] + set parse_args [concat tip $revargs] + set parse_temp [eval exec {$env(HG)} --config ui.report_untrusted=false log --template '{node}\n' $parse_args] regsub -all "\r\n" $parse_temp "\n" parse_temp set parsed_args [split $parse_temp "\n"] } err] { @@ -201,6 +204,9 @@ if {$limit > 0} { set parsed_args [concat -n $limit $parsed_args] } + if {$showhidden} { + append parsed_args --hidden + } if [catch { set commfd [open "|{$env(HG)} --config ui.report_untrusted=false debug-rev-list --header --topo-order --parents $parsed_args" r] } err] { @@ -331,7 +337,7 @@ proc parsecommit {id contents listed olds} { global commitinfo children nchildren parents nparents cdate ncleft - global firstparents + global firstparents obsolete set inhdr 1 set comment {} @@ -369,21 +375,25 @@ set inhdr 0 } else { set tag [lindex $line 0] - if {$tag == "author"} { + switch -- $tag "author" { set x [expr {[llength $line] - 2}] set audate [lindex $line $x] set auname [join [lrange $line 1 [expr {$x - 1}]]] - } elseif {$tag == "committer"} { + } "committer" { set x [expr {[llength $line] - 2}] set comdate [lindex $line $x] set comname [join [lrange $line 1 [expr {$x - 1}]]] - } elseif {$tag == "revision"} { + } "revision" { set rev [lindex $line 1] - } elseif {$tag == "branch"} { + } "branch" { set branch [join [lrange $line 1 end]] - } elseif {$tag == "bookmark"} { + } "bookmark" { set bookmark [join [lrange $line 1 end]] - } + } "obsolete" { + set obsolete($id) "" + } "phase" { + set phase [lindex $line 1 end] + } } } else { if {$comment == {}} { @@ -407,7 +417,7 @@ set comdate [clock format $comdate] } set commitinfo($id) [list $headline $auname $audate \ - $comname $comdate $comment $rev $branch $bookmark] + $comname $comdate $comment $rev $branch $bookmark $phase] if {[info exists firstparents]} { set i [lsearch $firstparents $id] @@ -1133,7 +1143,7 @@ global lineno lthickness mainline mainlinearrow sidelines global commitlisted rowtextx idpos lastuse displist global oldnlines olddlevel olddisplist - global aucolormap curid curidfont + global aucolormap curid curidfont obsolete incr numcommits incr lineno @@ -1141,13 +1151,26 @@ set lastuse($id) $lineno set lineid($lineno) $id set idline($id) $lineno - set ofill [expr {[info exists commitlisted($id)]? "blue": "white"}] + set shape oval + set outline #000080 + set ofill [expr {[info exists commitlisted($id)]? "#7f7fff": "white"}] if {![info exists commitinfo($id)]} { readcommit $id if {![info exists commitinfo($id)]} { set commitinfo($id) {"No commit information available"} set nparents($id) 0 } + } else { + switch [lindex $commitinfo($id) 9] secret { + set shape rect + } public { + set outline black + set ofill blue + } + } + if {[info exists obsolete($id)]} { + set outline darkgrey + set ofill lightgrey } assigncolor $id set currentparents {} @@ -1175,9 +1198,9 @@ } drawlines $id 0 set orad [expr {$linespc / 3}] - set t [$canv create oval [expr $x - $orad] [expr $y1 - $orad] \ + set t [$canv create $shape [expr $x - $orad] [expr $y1 - $orad] \ [expr $x + $orad - 1] [expr $y1 + $orad - 1] \ - -fill $ofill -outline black -width 1] + -fill $ofill -outline $outline -width 1] $canv raise $t $canv bind $t <1> {selcanvline {} %x %y} set xt [xcoord [llength $displist] $level $lineno] @@ -2493,6 +2516,9 @@ } $ctext insert end "User: [lindex $info 1]\n" $ctext insert end "Date: [lindex $info 2]\n" + if {[lindex $info 3] ne ""} { + $ctext insert end "Committer: [lindex $info 3]\n" + } if {[info exists idbookmarks($id)]} { $ctext insert end "Bookmarks:" foreach bookmark $idbookmarks($id) { @@ -2520,6 +2546,12 @@ append comment "Child: [commit_descriptor $c]\n" } } + + if {[lindex $info 9] eq "secret"} { + # for now, display phase for secret changesets only + append comment "Phase: [lindex $info 9]\n" + } + append comment "\n" append comment [lindex $info 5] @@ -4040,13 +4072,15 @@ proc getconfig {} { global env - - set lines [exec $env(HG) debug-config] - regsub -all "\r\n" $lines "\n" config set config {} - foreach line [split $lines "\n"] { - regsub "^(k|v)=" $line "" line - lappend config $line + + set lines [exec $env(HG) debugconfig] + foreach line [split $lines \n] { + set line [string trimright $line \r] + if {[string match hgk.* $line]} { + regexp {(.*)=(.*)} $line - k v + lappend config $k $v + } } return $config } @@ -4110,8 +4144,9 @@ set stuffsaved 0 set patchnum 0 +set config(hgk.vdiff) "" array set config [getconfig] -set hgvdiff $config(vdiff) +set hgvdiff $config(hgk.vdiff) setcoords makewindow readrefs diff -r 612ed41ae359 -r e89f909edffa contrib/import-checker.py --- a/contrib/import-checker.py Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/import-checker.py Thu Apr 16 20:57:51 2015 -0500 @@ -61,6 +61,8 @@ for m in 'ctypes', 'email': yield m yield 'builtins' # python3 only + for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only + yield m stdlib_prefixes = set([sys.prefix, sys.exec_prefix]) # We need to supplement the list of prefixes for the search to work # when run from within a virtualenv. @@ -90,7 +92,8 @@ for name in files: if name == '__init__.py': continue - if not (name.endswith('.py') or name.endswith('.so')): + if not (name.endswith('.py') or name.endswith('.so') + or name.endswith('.pyd')): continue full_path = os.path.join(top, name) if 'site-packages' in full_path: @@ -162,36 +165,31 @@ class CircularImport(Exception): pass - -def cyclekey(names): - return tuple(sorted(set(names))) - -def check_one_mod(mod, imports, path=None, ignore=None): - if path is None: - path = [] - if ignore is None: - ignore = [] - path = path + [mod] - for i in sorted(imports.get(mod, [])): - if i not in stdlib_modules and not i.startswith('mercurial.'): - i = mod.rsplit('.', 1)[0] + '.' + i - if i in path: - firstspot = path.index(i) - cycle = path[firstspot:] + [i] - if cyclekey(cycle) not in ignore: - raise CircularImport(cycle) - continue - check_one_mod(i, imports, path=path, ignore=ignore) +def checkmod(mod, imports): + shortest = {} + visit = [[mod]] + while visit: + path = visit.pop(0) + for i in sorted(imports.get(path[-1], [])): + if i not in stdlib_modules and not i.startswith('mercurial.'): + i = mod.rsplit('.', 1)[0] + '.' + i + if len(path) < shortest.get(i, 1000): + shortest[i] = len(path) + if i in path: + if i == path[0]: + raise CircularImport(path) + continue + visit.append(path + [i]) def rotatecycle(cycle): """arrange a cycle so that the lexicographically first module listed first - >>> rotatecycle(['foo', 'bar', 'foo']) + >>> rotatecycle(['foo', 'bar']) ['bar', 'foo', 'bar'] """ lowest = min(cycle) idx = cycle.index(lowest) - return cycle[idx:] + cycle[1:idx] + [lowest] + return cycle[idx:] + cycle[:idx] + [lowest] def find_cycles(imports): """Find cycles in an already-loaded import graph. @@ -201,17 +199,17 @@ ... 'top.baz': ['foo'], ... 'top.qux': ['foo']} >>> print '\\n'.join(sorted(find_cycles(imports))) - top.bar -> top.baz -> top.foo -> top.bar -> top.bar - top.foo -> top.qux -> top.foo -> top.foo + top.bar -> top.baz -> top.foo -> top.bar + top.foo -> top.qux -> top.foo """ - cycles = {} + cycles = set() for mod in sorted(imports.iterkeys()): try: - check_one_mod(mod, imports, ignore=cycles) + checkmod(mod, imports) except CircularImport, e: cycle = e.args[0] - cycles[cyclekey(cycle)] = ' -> '.join(rotatecycle(cycle)) - return cycles.values() + cycles.add(" -> ".join(rotatecycle(cycle))) + return cycles def _cycle_sortkey(c): return len(c), c diff -r 612ed41ae359 -r e89f909edffa contrib/lock-checker.py --- a/contrib/lock-checker.py Thu Apr 16 22:33:53 2015 +0900 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -"""Extension to verify locks are obtained in the required places. - -This works by wrapping functions that should be surrounded by a lock -and asserting the lock is held. Missing locks are called out with a -traceback printed to stderr. - -This currently only checks store locks, not working copy locks. -""" -import os -from mercurial import util - -def _checklock(repo): - l = repo._lockref and repo._lockref() - if l is None or not l.held: - util.debugstacktrace('missing lock', skip=1) - -def reposetup(ui, repo): - orig = repo.__class__ - class lockcheckrepo(repo.__class__): - def _writejournal(self, *args, **kwargs): - _checklock(self) - return orig._writejournal(self, *args, **kwargs) - - def transaction(self, *args, **kwargs): - _checklock(self) - return orig.transaction(self, *args, **kwargs) - - # TODO(durin42): kiilerix had a commented-out lock check in - # _writebranchcache and _writerequirements - - def _tag(self, *args, **kwargs): - _checklock(self) - return orig._tag(self, *args, **kwargs) - - def write(self, *args, **kwargs): - assert os.path.lexists(self._join('.hg/wlock')) - return orig.write(self, *args, **kwargs) - - repo.__class__ = lockcheckrepo diff -r 612ed41ae359 -r e89f909edffa contrib/mercurial.spec --- a/contrib/mercurial.spec Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/mercurial.spec Thu Apr 16 20:57:51 2015 -0500 @@ -9,7 +9,7 @@ %global docutilsname docutils-0.12 %global docutilsmd5 4622263b62c5c771c03502afa3157768 %global pythonhg python-hg -%global hgpyprefix /usr/%{pythonhg} +%global hgpyprefix /opt/%{pythonhg} # byte compilation will fail on some some Python /test/ files %global _python_bytecompile_errors_terminate_build 0 diff -r 612ed41ae359 -r e89f909edffa contrib/perf.py --- a/contrib/perf.py Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/perf.py Thu Apr 16 20:57:51 2015 -0500 @@ -189,14 +189,25 @@ timer(d) fm.end() -@command('perfdirstatefoldmap') -def perffoldmap(ui, repo): +@command('perffilefoldmap') +def perffilefoldmap(ui, repo): timer, fm = gettimer(ui) dirstate = repo.dirstate 'a' in dirstate def d(): - dirstate._foldmap.get('a') - del dirstate._foldmap + dirstate._filefoldmap.get('a') + del dirstate._filefoldmap + timer(d) + fm.end() + +@command('perfdirfoldmap') +def perfdirfoldmap(ui, repo): + timer, fm = gettimer(ui) + dirstate = repo.dirstate + 'a' in dirstate + def d(): + dirstate._dirfoldmap.get('a') + del dirstate._dirfoldmap del dirstate._dirs timer(d) fm.end() @@ -293,6 +304,25 @@ timer(d) fm.end() +@command('perfctxfiles') +def perfparents(ui, repo, x): + x = int(x) + timer, fm = gettimer(ui) + def d(): + len(repo[x].files()) + timer(d) + fm.end() + +@command('perfrawfiles') +def perfparents(ui, repo, x): + x = int(x) + timer, fm = gettimer(ui) + cl = repo.changelog + def d(): + len(cl.read(x)[3]) + timer(d) + fm.end() + @command('perflookup') def perflookup(ui, repo, rev): timer, fm = gettimer(ui) diff -r 612ed41ae359 -r e89f909edffa contrib/synthrepo.py --- a/contrib/synthrepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/synthrepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -359,7 +359,10 @@ files.iterkeys(), filectxfn, ui.username(), '%d %d' % util.makedate()) initnode = mc.commit() - hexfn = ui.debugflag and hex or short + if ui.debugflag: + hexfn = hex + else: + hexfn = short ui.status(_('added commit %s with %d files\n') % (hexfn(initnode), len(files))) @@ -475,7 +478,10 @@ if dirpath in replacements: return replacements[dirpath] head, _ = os.path.split(dirpath) - head = head and rename(head) or '' + if head: + head = rename(head) + else: + head = '' renamed = os.path.join(head, wordgen.next()) replacements[dirpath] = renamed return renamed diff -r 612ed41ae359 -r e89f909edffa contrib/win32/ReadMe.html --- a/contrib/win32/ReadMe.html Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/win32/ReadMe.html Thu Apr 16 20:57:51 2015 -0500 @@ -140,7 +140,7 @@

- Mercurial is Copyright 2005-2014 Matt Mackall and others. See + Mercurial is Copyright 2005-2015 Matt Mackall and others. See the Contributors.txt file for a list of contributors.

diff -r 612ed41ae359 -r e89f909edffa contrib/win32/mercurial.iss --- a/contrib/win32/mercurial.iss Thu Apr 16 22:33:53 2015 +0900 +++ b/contrib/win32/mercurial.iss Thu Apr 16 20:57:51 2015 -0500 @@ -21,7 +21,7 @@ #endif [Setup] -AppCopyright=Copyright 2005-2010 Matt Mackall and others +AppCopyright=Copyright 2005-2015 Matt Mackall and others AppName=Mercurial #if ARCH == "x64" AppVerName=Mercurial {#VERSION} (64-bit) @@ -44,7 +44,7 @@ DefaultDirName={pf}\Mercurial SourceDir=..\.. VersionInfoDescription=Mercurial distributed SCM (version {#VERSION}) -VersionInfoCopyright=Copyright 2005-2010 Matt Mackall and others +VersionInfoCopyright=Copyright 2005-2015 Matt Mackall and others VersionInfoCompany=Matt Mackall and others InternalCompressLevel=max SolidCompression=true diff -r 612ed41ae359 -r e89f909edffa contrib/wix/COPYING.rtf Binary file contrib/wix/COPYING.rtf has changed diff -r 612ed41ae359 -r e89f909edffa doc/hgmanpage.py --- a/doc/hgmanpage.py Thu Apr 16 22:33:53 2015 +0900 +++ b/doc/hgmanpage.py Thu Apr 16 20:57:51 2015 -0500 @@ -427,7 +427,7 @@ pass def visit_block_quote(self, node): - # BUG/HACK: indent alway uses the _last_ indention, + # BUG/HACK: indent always uses the _last_ indention, # thus we need two of them. self.indent(BLOCKQOUTE_INDENT) self.indent(0) diff -r 612ed41ae359 -r e89f909edffa hgext/censor.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/censor.py Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,168 @@ +# Copyright (C) 2015 - Mike Edgar +# +# This extension enables removal of file content at a given revision, +# rewriting the data/metadata of successive revisions to preserve revision log +# integrity. + +"""erase file content at a given revision + +The censor command instructs Mercurial to erase all content of a file at a given +revision *without updating the changeset hash.* This allows existing history to +remain valid while preventing future clones/pulls from receiving the erased +data. + +Typical uses for censor are due to security or legal requirements, including:: + + * Passwords, private keys, crytographic material + * Licensed data/code/libraries for which the license has expired + * Personally Identifiable Information or other private data + +Censored file revisions are listed in a tracked file called .hgcensored stored +in the repository root. The censor command adds an entry to the .hgcensored file +in the working directory and commits it (much like ``hg tag`` and .hgtags). The +censored file data is then replaced with a pointer to the new commit, enabling +verification. + +Censored nodes can interrupt mercurial's typical operation whenever the excised +data needs to be materialized. Some commands, like ``hg cat``/``hg revert``, +simply fail when asked to produce censored data. Others, like ``hg verify`` and +``hg update``, must be capable of tolerating censored data to continue to +function in a meaningful way. Such commands only tolerate censored file +revisions if they are allowed by the policy specified by the "censor.allow" +config option. +""" + +from mercurial.node import short +from mercurial import cmdutil, error, filelog, revlog, scmutil, util +from mercurial.i18n import _ + +cmdtable = {} +command = cmdutil.command(cmdtable) +testedwith = 'internal' + +@command('censor', + [('r', 'rev', '', _('censor file from specified revision'), _('REV')), + ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))], + _('-r REV [-t TEXT] [FILE]')) +def censor(ui, repo, path, rev='', tombstone='', **opts): + if not path: + raise util.Abort(_('must specify file path to censor')) + if not rev: + raise util.Abort(_('must specify revision to censor')) + + flog = repo.file(path) + if not len(flog): + raise util.Abort(_('cannot censor file with no history')) + + rev = scmutil.revsingle(repo, rev, rev).rev() + try: + ctx = repo[rev] + except KeyError: + raise util.Abort(_('invalid revision identifier %s') % rev) + + try: + fctx = ctx.filectx(path) + except error.LookupError: + raise util.Abort(_('file does not exist at revision %s') % rev) + + fnode = fctx.filenode() + headctxs = [repo[c] for c in repo.heads()] + heads = [c for c in headctxs if path in c and c.filenode(path) == fnode] + if heads: + headlist = ', '.join([short(c.node()) for c in heads]) + raise util.Abort(_('cannot censor file in heads (%s)') % headlist, + hint=_('clean/delete and commit first')) + + wctx = repo[None] + wp = wctx.parents() + if ctx.node() in [p.node() for p in wp]: + raise util.Abort(_('cannot censor working directory'), + hint=_('clean/delete/update first')) + + flogv = flog.version & 0xFFFF + if flogv != revlog.REVLOGNG: + raise util.Abort( + _('censor does not support revlog version %d') % (flogv,)) + + tombstone = filelog.packmeta({"censored": tombstone}, "") + + crev = fctx.filerev() + + if len(tombstone) > flog.rawsize(crev): + raise util.Abort(_( + 'censor tombstone must be no longer than censored data')) + + # Using two files instead of one makes it easy to rewrite entry-by-entry + idxread = repo.svfs(flog.indexfile, 'r') + idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True) + if flog.version & revlog.REVLOGNGINLINEDATA: + dataread, datawrite = idxread, idxwrite + else: + dataread = repo.svfs(flog.datafile, 'r') + datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True) + + # Copy all revlog data up to the entry to be censored. + rio = revlog.revlogio() + offset = flog.start(crev) + + for chunk in util.filechunkiter(idxread, limit=crev * rio.size): + idxwrite.write(chunk) + for chunk in util.filechunkiter(dataread, limit=offset): + datawrite.write(chunk) + + def rewriteindex(r, newoffs, newdata=None): + """Rewrite the index entry with a new data offset and optional new data. + + The newdata argument, if given, is a tuple of three positive integers: + (new compressed, new uncompressed, added flag bits). + """ + offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r] + flags = revlog.gettype(offlags) + if newdata: + comp, uncomp, nflags = newdata + flags |= nflags + offlags = revlog.offset_type(newoffs, flags) + e = (offlags, comp, uncomp, r, link, p1, p2, nodeid) + idxwrite.write(rio.packentry(e, None, flog.version, r)) + idxread.seek(rio.size, 1) + + def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS): + """Write the given full text to the filelog with the given data offset. + + Returns: + The integer number of data bytes written, for tracking data offsets. + """ + flag, compdata = flog.compress(data) + newcomp = len(flag) + len(compdata) + rewriteindex(r, offs, (newcomp, len(data), nflags)) + datawrite.write(flag) + datawrite.write(compdata) + dataread.seek(flog.length(r), 1) + return newcomp + + # Rewrite censored revlog entry with (padded) tombstone data. + pad = ' ' * (flog.rawsize(crev) - len(tombstone)) + offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED) + + # Rewrite all following filelog revisions fixing up offsets and deltas. + for srev in xrange(crev + 1, len(flog)): + if crev in flog.parentrevs(srev): + # Immediate children of censored node must be re-added as fulltext. + try: + revdata = flog.revision(srev) + except error.CensoredNodeError, e: + revdata = e.tombstone + dlen = rewrite(srev, offset, revdata) + else: + # Copy any other revision data verbatim after fixing up the offset. + rewriteindex(srev, offset) + dlen = flog.length(srev) + for chunk in util.filechunkiter(dataread, limit=dlen): + datawrite.write(chunk) + offset += dlen + + idxread.close() + idxwrite.close() + if dataread is not idxread: + dataread.close() + datawrite.close() diff -r 612ed41ae359 -r e89f909edffa hgext/children.py --- a/hgext/children.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/children.py Thu Apr 16 20:57:51 2015 -0500 @@ -39,11 +39,13 @@ """ rev = opts.get('rev') if file_: - ctx = repo.filectx(file_, changeid=rev) + fctx = repo.filectx(file_, changeid=rev) + childctxs = [fcctx.changectx() for fcctx in fctx.children()] else: ctx = repo[rev] + childctxs = ctx.children() displayer = cmdutil.show_changeset(ui, repo, opts) - for cctx in ctx.children(): + for cctx in childctxs: displayer.show(cctx) displayer.close() diff -r 612ed41ae359 -r e89f909edffa hgext/churn.py --- a/hgext/churn.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/churn.py Thu Apr 16 20:57:51 2015 -0500 @@ -46,7 +46,7 @@ date = datetime.datetime(*time.gmtime(float(t) - tz)[:6]) return date.strftime(opts['dateformat']) else: - tmpl = opts.get('template', '{author|email}') + tmpl = opts.get('oldtemplate') or opts.get('template') tmpl = maketemplater(ui, repo, tmpl) def getkey(ctx): ui.pushbuffer() @@ -95,7 +95,9 @@ _('count rate for the specified revision or revset'), _('REV')), ('d', 'date', '', _('count rate for revisions matching date spec'), _('DATE')), - ('t', 'template', '{author|email}', + ('t', 'oldtemplate', '', + _('template to group changesets (DEPRECATED)'), _('TEMPLATE')), + ('T', 'template', '{author|email}', _('template to group changesets'), _('TEMPLATE')), ('f', 'dateformat', '', _('strftime-compatible format for grouping by date'), _('FORMAT')), diff -r 612ed41ae359 -r e89f909edffa hgext/color.py --- a/hgext/color.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/color.py Thu Apr 16 20:57:51 2015 -0500 @@ -140,6 +140,17 @@ either using ansi mode (or auto mode), or by using less -r (which will pass through all terminal control codes, not just color control codes). + +On some systems (such as MSYS in Windows), the terminal may support +a different color mode than the pager (activated via the "pager" +extension). It is possible to define separate modes depending on whether +the pager is active:: + + [color] + mode = auto + pagermode = ansi + +If ``pagermode`` is not defined, the ``mode`` will be used. ''' import os @@ -213,20 +224,41 @@ formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted()) mode = ui.config('color', 'mode', 'auto') + + # If pager is active, color.pagermode overrides color.mode. + if getattr(ui, 'pageractive', False): + mode = ui.config('color', 'pagermode', mode) + realmode = mode if mode == 'auto': - if os.name == 'nt' and 'TERM' not in os.environ: - # looks line a cmd.exe console, use win32 API or nothing - realmode = 'win32' + if os.name == 'nt': + term = os.environ.get('TERM') + # TERM won't be defined in a vanilla cmd.exe environment. + + # UNIX-like environments on Windows such as Cygwin and MSYS will + # set TERM. They appear to make a best effort attempt at setting it + # to something appropriate. However, not all environments with TERM + # defined support ANSI. Since "ansi" could result in terminal + # gibberish, we error on the side of selecting "win32". However, if + # w32effects is not defined, we almost certainly don't support + # "win32", so don't even try. + if (term and 'xterm' in term) or not w32effects: + realmode = 'ansi' + else: + realmode = 'win32' else: realmode = 'ansi' + def modewarn(): + # only warn if color.mode was explicitly set and we're in + # an interactive terminal + if mode == realmode and ui.interactive(): + ui.warn(_('warning: failed to set color mode to %s\n') % mode) + if realmode == 'win32': _terminfo_params = {} if not w32effects: - if mode == 'win32': - # only warn if color.mode is explicitly set to win32 - ui.warn(_('warning: failed to set color mode to %s\n') % mode) + modewarn() return None _effects.update(w32effects) elif realmode == 'ansi': @@ -234,10 +266,8 @@ elif realmode == 'terminfo': _terminfosetup(ui, mode) if not _terminfo_params: - if mode == 'terminfo': - ## FIXME Shouldn't we return None in this case too? - # only warn if color.mode is explicitly set to win32 - ui.warn(_('warning: failed to set color mode to %s\n') % mode) + ## FIXME Shouldn't we return None in this case too? + modewarn() realmode = 'ansi' else: return None diff -r 612ed41ae359 -r e89f909edffa hgext/convert/bzr.py --- a/hgext/convert/bzr.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/bzr.py Thu Apr 16 20:57:51 2015 -0500 @@ -143,7 +143,8 @@ parentids = self._parentids.pop(version) # only diff against first parent id prevtree = self.sourcerepo.revision_tree(parentids[0]) - return self._gettreechanges(self._revtree, prevtree) + files, changes = self._gettreechanges(self._revtree, prevtree) + return files, changes, set() def getcommit(self, version): rev = self.sourcerepo.get_revision(version) diff -r 612ed41ae359 -r e89f909edffa hgext/convert/common.py --- a/hgext/convert/common.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/common.py Thu Apr 16 20:57:51 2015 -0500 @@ -31,7 +31,10 @@ def checktool(exe, name=None, abort=True): name = name or exe if not util.findexe(exe): - exc = abort and util.Abort or MissingTool + if abort: + exc = util.Abort + else: + exc = MissingTool raise exc(_('cannot find required "%s" tool') % name) class NoRepo(Exception): @@ -94,7 +97,7 @@ raise NotImplementedError def getchanges(self, version, full): - """Returns a tuple of (files, copies). + """Returns a tuple of (files, copies, cleanp2). files is a sorted list of (filename, id) tuples for all files changed between version and its first parent returned by @@ -102,6 +105,10 @@ id is the source revision id of the file. copies is a dictionary of dest: source + + cleanp2 is the set of files filenames that are clean against p2. + (Files that are clean against p1 are already not in files (unless + full). This makes it possible to handle p2 clean files similarly.) """ raise NotImplementedError @@ -212,7 +219,8 @@ mapping equivalent authors identifiers for each system.""" return None - def putcommit(self, files, copies, parents, commit, source, revmap, full): + def putcommit(self, files, copies, parents, commit, source, revmap, full, + cleanp2): """Create a revision with all changed files listed in 'files' and having listed parents. 'commit' is a commit object containing at a minimum the author, date, and message for this @@ -222,6 +230,8 @@ of source revisions to converted revisions. Only getfile() and lookuprev() should be called on 'source'. 'full' means that 'files' is complete and all other files should be removed. + 'cleanp2' is a set of the filenames that are unchanged from p2 + (only in the common merge case where there two parents). Note that the sink repository is not told to update itself to a particular revision (or even what that revision would be) diff -r 612ed41ae359 -r e89f909edffa hgext/convert/convcmd.py --- a/hgext/convert/convcmd.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/convcmd.py Thu Apr 16 20:57:51 2015 -0500 @@ -397,7 +397,7 @@ dest = self.map[changes] self.map[rev] = dest return - files, copies = changes + files, copies, cleanp2 = changes pbranches = [] if commit.parents: for prev in commit.parents: @@ -413,9 +413,19 @@ parents = [self.map.get(p, p) for p in parents] except KeyError: parents = [b[0] for b in pbranches] - source = progresssource(self.ui, self.source, len(files)) + if len(pbranches) != 2: + cleanp2 = set() + if len(parents) < 3: + source = progresssource(self.ui, self.source, len(files)) + else: + # For an octopus merge, we end up traversing the list of + # changed files N-1 times. This tweak to the number of + # files makes it so the progress bar doesn't overflow + # itself. + source = progresssource(self.ui, self.source, + len(files) * (len(parents) - 1)) newnode = self.dest.putcommit(files, copies, parents, commit, - source, self.map, full) + source, self.map, full, cleanp2) source.close() self.source.converted(rev, newnode) self.map[rev] = newnode @@ -515,7 +525,11 @@ sortmode = [m for m in sortmodes if opts.get(m)] if len(sortmode) > 1: raise util.Abort(_('more than one sort mode specified')) - sortmode = sortmode and sortmode[0] or defaultsort + if sortmode: + sortmode = sortmode[0] + else: + sortmode = defaultsort + if sortmode == 'sourcesort' and not srcc.hasnativeorder(): raise util.Abort(_('--sourcesort is not supported by this data source')) if sortmode == 'closesort' and not srcc.hasnativeclose(): @@ -531,4 +545,3 @@ c = converter(ui, srcc, destc, revmapfile, opts) c.convert(sortmode) - diff -r 612ed41ae359 -r e89f909edffa hgext/convert/cvs.py --- a/hgext/convert/cvs.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/cvs.py Thu Apr 16 20:57:51 2015 -0500 @@ -262,7 +262,7 @@ if full: raise util.Abort(_("convert from cvs do not support --full")) self._parse() - return sorted(self.files[rev].iteritems()), {} + return sorted(self.files[rev].iteritems()), {}, set() def getcommit(self, rev): self._parse() diff -r 612ed41ae359 -r e89f909edffa hgext/convert/cvsps.py --- a/hgext/convert/cvsps.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/cvsps.py Thu Apr 16 20:57:51 2015 -0500 @@ -634,14 +634,21 @@ # By this point, the changesets are sufficiently compared that # we don't really care about ordering. However, this leaves # some race conditions in the tests, so we compare on the - # number of files modified and the number of branchpoints in - # each changeset to ensure test output remains stable. + # number of files modified, the files contained in each + # changeset, and the branchpoints in the change to ensure test + # output remains stable. # recommended replacement for cmp from # https://docs.python.org/3.0/whatsnew/3.0.html c = lambda x, y: (x > y) - (x < y) + # Sort bigger changes first. if not d: d = c(len(l.entries), len(r.entries)) + # Try sorting by filename in the change. + if not d: + d = c([e.file for e in l.entries], [e.file for e in r.entries]) + # Try and put changes without a branch point before ones with + # a branch point. if not d: d = c(len(l.branchpoints), len(r.branchpoints)) return d diff -r 612ed41ae359 -r e89f909edffa hgext/convert/darcs.py --- a/hgext/convert/darcs.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/darcs.py Thu Apr 16 20:57:51 2015 -0500 @@ -188,7 +188,7 @@ changes.append((elt.text.strip(), rev)) self.pull(rev) self.lastrev = rev - return sorted(changes), copies + return sorted(changes), copies, set() def getfile(self, name, rev): if rev != self.lastrev: diff -r 612ed41ae359 -r e89f909edffa hgext/convert/filemap.py --- a/hgext/convert/filemap.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/filemap.py Thu Apr 16 20:57:51 2015 -0500 @@ -384,12 +384,15 @@ # Get the real changes and do the filtering/mapping. To be # able to get the files later on in getfile, we hide the # original filename in the rev part of the return value. - changes, copies = self.base.getchanges(rev, full) + changes, copies, cleanp2 = self.base.getchanges(rev, full) files = {} + ncleanp2 = set(cleanp2) for f, r in changes: newf = self.filemapper(f) if newf and (newf != f or newf not in files): files[newf] = (f, r) + if newf != f: + ncleanp2.discard(f) files = sorted(files.items()) ncopies = {} @@ -400,7 +403,7 @@ if newsource: ncopies[newc] = newsource - return files, ncopies + return files, ncopies, ncleanp2 def getfile(self, name, rev): realname, realrev = rev diff -r 612ed41ae359 -r e89f909edffa hgext/convert/git.py --- a/hgext/convert/git.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/git.py Thu Apr 16 20:57:51 2015 -0500 @@ -264,7 +264,7 @@ else: self.retrievegitmodules(version) changes.append(('.hgsubstate', '')) - return (changes, copies) + return (changes, copies, set()) def getcommit(self, version): c = self.catfile(version, "commit") # read the commit hash diff -r 612ed41ae359 -r e89f909edffa hgext/convert/gnuarch.py --- a/hgext/convert/gnuarch.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/gnuarch.py Thu Apr 16 20:57:51 2015 -0500 @@ -171,7 +171,7 @@ copies.update(cps) self.lastrev = rev - return sorted(set(changes)), copies + return sorted(set(changes)), copies, set() def getcommit(self, rev): changes = self.changes[rev] @@ -209,7 +209,10 @@ mode = os.lstat(os.path.join(self.tmppath, name)).st_mode if stat.S_ISLNK(mode): data = os.readlink(os.path.join(self.tmppath, name)) - mode = mode and 'l' or '' + if mode: + mode = 'l' + else: + mode = '' else: data = open(os.path.join(self.tmppath, name), 'rb').read() mode = (mode & 0111) and 'x' or '' diff -r 612ed41ae359 -r e89f909edffa hgext/convert/hg.py --- a/hgext/convert/hg.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/hg.py Thu Apr 16 20:57:51 2015 -0500 @@ -87,7 +87,10 @@ if not branch: branch = 'default' pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches] - pbranch = pbranches and pbranches[0][1] or 'default' + if pbranches: + pbranch = pbranches[0][1] + else: + pbranch = 'default' branchpath = os.path.join(self.path, branch) if setbranch: @@ -129,9 +132,14 @@ fp.write('%s %s\n' % (revid, s[1])) return fp.getvalue() - def putcommit(self, files, copies, parents, commit, source, revmap, full): + def putcommit(self, files, copies, parents, commit, source, revmap, full, + cleanp2): files = dict(files) + def getfilectx(repo, memctx, f): + if p2ctx and f in cleanp2 and f not in copies: + self.ui.debug('reusing %s from p2\n' % f) + return p2ctx[f] try: v = files[f] except KeyError: @@ -196,6 +204,9 @@ while parents: p1 = p2 p2 = parents.pop(0) + p2ctx = None + if p2 != nullid: + p2ctx = self.repo[p2] fileset = set(files) if full: fileset.update(self.repo[p1]) @@ -379,9 +390,13 @@ # getcopies() is also run for roots and before filtering so missing # revlogs are detected early copies = self.getcopies(ctx, parents, copyfiles) + cleanp2 = set() + if len(parents) == 2: + cleanp2.update(self.repo.status(parents[1].node(), ctx.node(), + clean=True).clean) changes = [(f, rev) for f in files if f not in self.ignored] changes.sort() - return changes, copies + return changes, copies, cleanp2 def getcopies(self, ctx, parents, files): copies = {} diff -r 612ed41ae359 -r e89f909edffa hgext/convert/monotone.py --- a/hgext/convert/monotone.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/monotone.py Thu Apr 16 20:57:51 2015 -0500 @@ -280,7 +280,7 @@ for fromfile in renamed.values(): files[fromfile] = rev - return (files.items(), copies) + return (files.items(), copies, set()) def getfile(self, name, rev): if not self.mtnisfile(name, rev): @@ -297,7 +297,7 @@ extra = {} certs = self.mtngetcerts(rev) if certs.get('suspend') == certs["branch"]: - extra['close'] = '1' + extra['close'] = 1 return commit( author=certs["author"], date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")), diff -r 612ed41ae359 -r e89f909edffa hgext/convert/p4.py --- a/hgext/convert/p4.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/p4.py Thu Apr 16 20:57:51 2015 -0500 @@ -195,7 +195,7 @@ def getchanges(self, rev, full): if full: raise util.Abort(_("convert from p4 do not support --full")) - return self.files[rev], {} + return self.files[rev], {}, set() def getcommit(self, rev): return self.changeset[rev] diff -r 612ed41ae359 -r e89f909edffa hgext/convert/subversion.py --- a/hgext/convert/subversion.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/convert/subversion.py Thu Apr 16 20:57:51 2015 -0500 @@ -474,7 +474,7 @@ (files, copies) = self._getchanges(rev, full) # caller caches the result, so free it here to release memory del self.paths[rev] - return (files, copies) + return (files, copies, set()) def getchangedfiles(self, rev, i): # called from filemap - cache computed values for reuse in getchanges @@ -871,8 +871,16 @@ if self.ui.configbool('convert', 'localtimezone'): date = makedatetimestamp(date[0]) - log = message and self.recode(message) or '' - author = author and self.recode(author) or '' + if message: + log = self.recode(message) + else: + log = '' + + if author: + author = self.recode(author) + else: + author = '' + try: branch = self.module.split("/")[-1] if branch == self.trunkname: @@ -1118,7 +1126,10 @@ self.opener = scmutil.opener(self.wc) self.wopener = scmutil.opener(self.wc) self.childmap = mapfile(ui, self.join('hg-childmap')) - self.is_exec = util.checkexec(self.wc) and util.isexec or None + if util.checkexec(self.wc): + self.is_exec = util.isexec + else: + self.is_exec = None if created: hook = os.path.join(created, 'hooks', 'pre-revprop-change') @@ -1229,7 +1240,8 @@ def revid(self, rev): return u"svn:%s@%s" % (self.uuid, rev) - def putcommit(self, files, copies, parents, commit, source, revmap, full): + def putcommit(self, files, copies, parents, commit, source, revmap, full, + cleanp2): for parent in parents: try: return self.revid(self.childmap[parent]) diff -r 612ed41ae359 -r e89f909edffa hgext/eol.py --- a/hgext/eol.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/eol.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,13 +6,13 @@ Unix/Mac, thereby letting everybody use their OS native line endings. The extension reads its configuration from a versioned ``.hgeol`` -configuration file found in the root of the working copy. The +configuration file found in the root of the working directory. The ``.hgeol`` file use the same syntax as all other Mercurial configuration files. It uses two sections, ``[patterns]`` and ``[repository]``. The ``[patterns]`` section specifies how line endings should be -converted between the working copy and the repository. The format is +converted between the working directory and the repository. The format is specified by a file pattern. The first match is used, so put more specific patterns first. The available line endings are ``LF``, ``CRLF``, and ``BIN``. @@ -51,7 +51,7 @@ .. note:: The rules will first apply when files are touched in the working - copy, e.g. by updating to null and back to tip to touch all files. + directory, e.g. by updating to null and back to tip to touch all files. The extension uses an optional ``[eol]`` section read from both the normal Mercurial configuration files and the ``.hgeol`` file, with the diff -r 612ed41ae359 -r e89f909edffa hgext/extdiff.py --- a/hgext/extdiff.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/extdiff.py Thu Apr 16 20:57:51 2015 -0500 @@ -276,6 +276,7 @@ def uisetup(ui): for cmd, path in ui.configitems('extdiff'): + path = util.expandpath(path) if cmd.startswith('cmd.'): cmd = cmd[4:] if not path: diff -r 612ed41ae359 -r e89f909edffa hgext/fetch.py --- a/hgext/fetch.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/fetch.py Thu Apr 16 20:57:51 2015 -0500 @@ -56,8 +56,8 @@ except error.RepoLookupError: branchnode = None if parent != branchnode: - raise util.Abort(_('working dir not at branch tip ' - '(use "hg update" to check out branch tip)')) + raise util.Abort(_('working directory not at branch tip'), + hint=_('use "hg update" to check out branch tip')) wlock = lock = None try: diff -r 612ed41ae359 -r e89f909edffa hgext/graphlog.py --- a/hgext/graphlog.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/graphlog.py Thu Apr 16 20:57:51 2015 -0500 @@ -54,4 +54,5 @@ Nodes printed as an @ character are parents of the working directory. """ - return cmdutil.graphlog(ui, repo, *pats, **opts) + opts['graph'] = True + return commands.log(ui, repo, *pats, **opts) diff -r 612ed41ae359 -r e89f909edffa hgext/hgcia.py --- a/hgext/hgcia.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/hgcia.py Thu Apr 16 20:57:51 2015 -0500 @@ -121,7 +121,10 @@ return patch.diffstat(pbuf.lines) or '' def logmsg(self): - diffstat = self.cia.diffstat and self.diffstat() or '' + if self.cia.diffstat: + diffstat = self.diffstat() + else: + diffstat = '' self.cia.ui.pushbuffer() self.cia.templater.show(self.ctx, changes=self.ctx.changeset(), baseurl=self.cia.ui.config('web', 'baseurl'), @@ -199,7 +202,10 @@ style = self.ui.config('cia', 'style') template = self.ui.config('cia', 'template') if not template: - template = self.diffstat and self.dstemplate or self.deftemplate + if self.diffstat: + template = self.dstemplate + else: + template = self.deftemplate template = templater.parsestring(template, quoted=False) t = cmdutil.changeset_templater(self.ui, self.repo, False, None, template, style, False) diff -r 612ed41ae359 -r e89f909edffa hgext/hgk.py --- a/hgext/hgk.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/hgk.py Thu Apr 16 20:57:51 2015 -0500 @@ -35,7 +35,7 @@ ''' import os -from mercurial import cmdutil, commands, patch, revlog, scmutil +from mercurial import cmdutil, commands, patch, scmutil, obsolete from mercurial.node import nullid, nullrev, short from mercurial.i18n import _ @@ -50,7 +50,7 @@ ('s', 'stdin', None, _('stdin')), ('C', 'copy', None, _('detect copies')), ('S', 'search', "", _('search'))], - ('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...'), + ('[OPTION]... NODE1 NODE2 [FILE]...'), inferrepo=True) def difftree(ui, repo, node1=None, node2=None, *files, **opts): """diff trees from two commits""" @@ -117,17 +117,16 @@ date = ctx.date() description = ctx.description().replace("\0", "") - lines = description.splitlines() - if lines and lines[-1].startswith('committer:'): - committer = lines[-1].split(': ')[1].rstrip() - else: - committer = "" + ui.write(("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))) - ui.write(("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))) - if committer != '': - ui.write(("committer %s %s %s\n" % (committer, int(date[0]), date[1]))) + if 'committer' in ctx.extra(): + ui.write(("committer %s\n" % ctx.extra()['committer'])) + ui.write(("revision %d\n" % ctx.rev())) ui.write(("branch %s\n" % ctx.branch())) + if obsolete.isenabled(repo, obsolete.createmarkersopt): + if ctx.obsolete(): + ui.write(("obsolete\n")) ui.write(("phase %s\n\n" % ctx.phasestr())) if prefix != "": @@ -138,7 +137,7 @@ if prefix: ui.write('\0') -@command('debug-merge-base', [], _('hg debug-merge-base REV REV')) +@command('debug-merge-base', [], _('REV REV')) def base(ui, repo, node1, node2): """output common ancestor information""" node1 = repo.lookup(node1) @@ -148,7 +147,7 @@ @command('debug-cat-file', [('s', 'stdin', None, _('stdin'))], - _('hg debug-cat-file [OPTION]... TYPE FILE'), + _('[OPTION]... TYPE FILE'), inferrepo=True) def catfile(ui, repo, type=None, r=None, **opts): """cat a specific revision""" @@ -298,22 +297,6 @@ break count += 1 -@command('debug-rev-parse', - [('', 'default', '', _('ignored'))], - _('hg debug-rev-parse REV')) -def revparse(ui, repo, *revs, **opts): - """parse given revisions""" - def revstr(rev): - if rev == 'HEAD': - rev = 'tip' - return revlog.hex(repo.lookup(rev)) - - for r in revs: - revrange = r.split(':', 1) - ui.write('%s\n' % revstr(revrange[0])) - if len(revrange) == 2: - ui.write('^%s\n' % revstr(revrange[1])) - # git rev-list tries to order things by date, and has the ability to stop # at a given commit without walking the whole repo. TODO add the stop # parameter @@ -322,7 +305,7 @@ ('t', 'topo-order', None, _('topo-order')), ('p', 'parents', None, _('parents')), ('n', 'max-count', 0, _('max-count'))], - ('hg debug-rev-list [OPTION]... REV...')) + ('[OPTION]... REV...')) def revlist(ui, repo, *revs, **opts): """print revisions""" if opts['header']: @@ -332,23 +315,17 @@ copy = [x for x in revs] revtree(ui, copy, repo, full, opts['max_count'], opts['parents']) -@command('debug-config', [], _('hg debug-config')) -def config(ui, repo, **opts): - """print extension options""" - def writeopt(name, value): - ui.write(('k=%s\nv=%s\n' % (name, value))) - - writeopt('vdiff', ui.config('hgk', 'vdiff', '')) - - @command('view', [('l', 'limit', '', _('limit number of changes displayed'), _('NUM'))], - _('hg view [-l LIMIT] [REVRANGE]')) + _('[-l LIMIT] [REVRANGE]')) def view(ui, repo, *etc, **opts): "start interactive history viewer" os.chdir(repo.root) optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v]) + if repo.filtername is None: + optstr += '--hidden' + cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc)) ui.debug("running %s\n" % cmd) ui.system(cmd) diff -r 612ed41ae359 -r e89f909edffa hgext/histedit.py --- a/hgext/histedit.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/histedit.py Thu Apr 16 20:57:51 2015 -0500 @@ -142,6 +142,13 @@ as running ``hg histedit 836302820282``. If you need plan to push to a repository that Mercurial does not detect to be related to the source repo, you can add a ``--force`` option. + +Histedit rule lines are truncated to 80 characters by default. You +can customise this behaviour by setting a different length in your +configuration file: + +[histedit] +linelen = 120 # truncate rule lines at 120 characters """ try: @@ -156,8 +163,11 @@ from mercurial import cmdutil from mercurial import discovery from mercurial import error +from mercurial import changegroup from mercurial import copies from mercurial import context +from mercurial import exchange +from mercurial import extensions from mercurial import hg from mercurial import node from mercurial import repair @@ -189,15 +199,16 @@ """) class histeditstate(object): - def __init__(self, repo, parentctx=None, rules=None, keep=None, + def __init__(self, repo, parentctxnode=None, rules=None, keep=None, topmost=None, replacements=None, lock=None, wlock=None): self.repo = repo self.rules = rules self.keep = keep self.topmost = topmost - self.parentctx = parentctx + self.parentctxnode = parentctxnode self.lock = lock self.wlock = wlock + self.backupfile = None if replacements is None: self.replacements = [] else: @@ -212,23 +223,153 @@ raise raise util.Abort(_('no histedit in progress')) - parentctxnode, rules, keep, topmost, replacements = pickle.load(fp) + try: + data = pickle.load(fp) + parentctxnode, rules, keep, topmost, replacements = data + backupfile = None + except pickle.UnpicklingError: + data = self._load() + parentctxnode, rules, keep, topmost, replacements, backupfile = data - self.parentctx = self.repo[parentctxnode] + self.parentctxnode = parentctxnode self.rules = rules self.keep = keep self.topmost = topmost self.replacements = replacements + self.backupfile = backupfile def write(self): fp = self.repo.vfs('histedit-state', 'w') - pickle.dump((self.parentctx.node(), self.rules, self.keep, - self.topmost, self.replacements), fp) + fp.write('v1\n') + fp.write('%s\n' % node.hex(self.parentctxnode)) + fp.write('%s\n' % node.hex(self.topmost)) + fp.write('%s\n' % self.keep) + fp.write('%d\n' % len(self.rules)) + for rule in self.rules: + fp.write('%s%s\n' % (rule[1], rule[0])) + fp.write('%d\n' % len(self.replacements)) + for replacement in self.replacements: + fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r) + for r in replacement[1]))) + fp.write('%s\n' % self.backupfile) fp.close() + def _load(self): + fp = self.repo.vfs('histedit-state', 'r') + lines = [l[:-1] for l in fp.readlines()] + + index = 0 + lines[index] # version number + index += 1 + + parentctxnode = node.bin(lines[index]) + index += 1 + + topmost = node.bin(lines[index]) + index += 1 + + keep = lines[index] == 'True' + index += 1 + + # Rules + rules = [] + rulelen = int(lines[index]) + index += 1 + for i in xrange(rulelen): + rule = lines[index] + rulehash = rule[:40] + ruleaction = rule[40:] + rules.append((ruleaction, rulehash)) + index += 1 + + # Replacements + replacements = [] + replacementlen = int(lines[index]) + index += 1 + for i in xrange(replacementlen): + replacement = lines[index] + original = node.bin(replacement[:40]) + succ = [node.bin(replacement[i:i + 40]) for i in + range(40, len(replacement), 40)] + replacements.append((original, succ)) + index += 1 + + backupfile = lines[index] + index += 1 + + fp.close() + + return parentctxnode, rules, keep, topmost, replacements, backupfile + def clear(self): self.repo.vfs.unlink('histedit-state') +class histeditaction(object): + def __init__(self, state, node): + self.state = state + self.repo = state.repo + self.node = node + + @classmethod + def fromrule(cls, state, rule): + """Parses the given rule, returning an instance of the histeditaction. + """ + repo = state.repo + rulehash = rule.strip().split(' ', 1)[0] + try: + node = repo[rulehash].node() + except error.RepoError: + raise util.Abort(_('unknown changeset %s listed') % rulehash[:12]) + return cls(state, node) + + def run(self): + """Runs the action. The default behavior is simply apply the action's + rulectx onto the current parentctx.""" + self.applychange() + self.continuedirty() + return self.continueclean() + + def applychange(self): + """Applies the changes from this action's rulectx onto the current + parentctx, but does not commit them.""" + repo = self.repo + rulectx = repo[self.node] + hg.update(repo, self.state.parentctxnode) + stats = applychanges(repo.ui, repo, rulectx, {}) + if stats and stats[3] > 0: + raise error.InterventionRequired(_('Fix up the change and run ' + 'hg histedit --continue')) + + def continuedirty(self): + """Continues the action when changes have been applied to the working + copy. The default behavior is to commit the dirty changes.""" + repo = self.repo + rulectx = repo[self.node] + + editor = self.commiteditor() + commit = commitfuncfor(repo, rulectx) + + commit(text=rulectx.description(), user=rulectx.user(), + date=rulectx.date(), extra=rulectx.extra(), editor=editor) + + def commiteditor(self): + """The editor to be used to edit the commit message.""" + return False + + def continueclean(self): + """Continues the action when the working copy is clean. The default + behavior is to accept the current commit as the new version of the + rulectx.""" + ctx = self.repo['.'] + if ctx.node() == self.state.parentctxnode: + self.repo.ui.warn(_('%s: empty changeset\n') % + node.short(self.node)) + return ctx, [(self.node, tuple())] + if ctx.node() == self.node: + # Nothing changed + return ctx, [] + return ctx, [(self.node, (ctx.node(),))] + def commitfuncfor(repo, src): """Build a commit function for the replacement of @@ -345,121 +486,120 @@ editor=editor) return repo.commitctx(new) -def pick(ui, state, ha, opts): - repo, ctx = state.repo, state.parentctx - oldctx = repo[ha] - if oldctx.parents()[0] == ctx: - ui.debug('node %s unchanged\n' % ha) - return oldctx, [] - hg.update(repo, ctx.node()) - stats = applychanges(ui, repo, oldctx, opts) - if stats and stats[3] > 0: - raise error.InterventionRequired(_('Fix up the change and run ' - 'hg histedit --continue')) - # drop the second merge parent - commit = commitfuncfor(repo, oldctx) - n = commit(text=oldctx.description(), user=oldctx.user(), - date=oldctx.date(), extra=oldctx.extra()) - if n is None: - ui.warn(_('%s: empty changeset\n') % node.hex(ha)) - return ctx, [] - new = repo[n] - return new, [(oldctx.node(), (n,))] +class pick(histeditaction): + def run(self): + rulectx = self.repo[self.node] + if rulectx.parents()[0].node() == self.state.parentctxnode: + self.repo.ui.debug('node %s unchanged\n' % node.short(self.node)) + return rulectx, [] + + return super(pick, self).run() +class edit(histeditaction): + def run(self): + repo = self.repo + rulectx = repo[self.node] + hg.update(repo, self.state.parentctxnode) + applychanges(repo.ui, repo, rulectx, {}) + raise error.InterventionRequired( + _('Make changes as needed, you may commit or record as needed ' + 'now.\nWhen you are finished, run hg histedit --continue to ' + 'resume.')) + + def commiteditor(self): + return cmdutil.getcommiteditor(edit=True, editform='histedit.edit') -def edit(ui, state, ha, opts): - repo, ctx = state.repo, state.parentctx - oldctx = repo[ha] - hg.update(repo, ctx.node()) - applychanges(ui, repo, oldctx, opts) - raise error.InterventionRequired( - _('Make changes as needed, you may commit or record as needed now.\n' - 'When you are finished, run hg histedit --continue to resume.')) +class fold(histeditaction): + def continuedirty(self): + repo = self.repo + rulectx = repo[self.node] -def rollup(ui, state, ha, opts): - rollupopts = opts.copy() - rollupopts['rollup'] = True - return fold(ui, state, ha, rollupopts) + commit = commitfuncfor(repo, rulectx) + commit(text='fold-temp-revision %s' % node.short(self.node), + user=rulectx.user(), date=rulectx.date(), + extra=rulectx.extra()) -def fold(ui, state, ha, opts): - repo, ctx = state.repo, state.parentctx - oldctx = repo[ha] - hg.update(repo, ctx.node()) - stats = applychanges(ui, repo, oldctx, opts) - if stats and stats[3] > 0: - raise error.InterventionRequired( - _('Fix up the change and run hg histedit --continue')) - n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(), - date=oldctx.date(), extra=oldctx.extra()) - if n is None: - ui.warn(_('%s: empty changeset') % node.hex(ha)) - return ctx, [] - return finishfold(ui, repo, ctx, oldctx, n, opts, []) + def continueclean(self): + repo = self.repo + ctx = repo['.'] + rulectx = repo[self.node] + parentctxnode = self.state.parentctxnode + if ctx.node() == parentctxnode: + repo.ui.warn(_('%s: empty changeset\n') % + node.short(self.node)) + return ctx, [(self.node, (parentctxnode,))] + + parentctx = repo[parentctxnode] + newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx, + parentctx)) + if not newcommits: + repo.ui.warn(_('%s: cannot fold - working copy is not a ' + 'descendant of previous commit %s\n') % + (node.short(self.node), node.short(parentctxnode))) + return ctx, [(self.node, (ctx.node(),))] + + middlecommits = newcommits.copy() + middlecommits.discard(ctx.node()) -def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges): - parent = ctx.parents()[0].node() - hg.update(repo, parent) - ### prepare new commit data - commitopts = opts.copy() - commitopts['user'] = ctx.user() - # commit message - if opts.get('rollup'): - newmessage = ctx.description() - else: - newmessage = '\n***\n'.join( - [ctx.description()] + - [repo[r].description() for r in internalchanges] + - [oldctx.description()]) + '\n' - commitopts['message'] = newmessage - # date - commitopts['date'] = max(ctx.date(), oldctx.date()) - extra = ctx.extra().copy() - # histedit_source - # note: ctx is likely a temporary commit but that the best we can do here - # This is sufficient to solve issue3681 anyway - extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex()) - commitopts['extra'] = extra - phasebackup = repo.ui.backupconfig('phases', 'new-commit') - try: - phasemin = max(ctx.phase(), oldctx.phase()) - repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit') - n = collapse(repo, ctx, repo[newnode], commitopts) - finally: - repo.ui.restoreconfig(phasebackup) - if n is None: - return ctx, [] - hg.update(repo, n) - replacements = [(oldctx.node(), (newnode,)), - (ctx.node(), (n,)), - (newnode, (n,)), - ] - for ich in internalchanges: - replacements.append((ich, (n,))) - return repo[n], replacements + return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(), + middlecommits) + + def skipprompt(self): + return False -def drop(ui, state, ha, opts): - repo, ctx = state.repo, state.parentctx - return ctx, [(repo[ha].node(), ())] - + def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges): + parent = ctx.parents()[0].node() + hg.update(repo, parent) + ### prepare new commit data + commitopts = {} + commitopts['user'] = ctx.user() + # commit message + if self.skipprompt(): + newmessage = ctx.description() + else: + newmessage = '\n***\n'.join( + [ctx.description()] + + [repo[r].description() for r in internalchanges] + + [oldctx.description()]) + '\n' + commitopts['message'] = newmessage + # date + commitopts['date'] = max(ctx.date(), oldctx.date()) + extra = ctx.extra().copy() + # histedit_source + # note: ctx is likely a temporary commit but that the best we can do + # here. This is sufficient to solve issue3681 anyway. + extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex()) + commitopts['extra'] = extra + phasebackup = repo.ui.backupconfig('phases', 'new-commit') + try: + phasemin = max(ctx.phase(), oldctx.phase()) + repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit') + n = collapse(repo, ctx, repo[newnode], commitopts) + finally: + repo.ui.restoreconfig(phasebackup) + if n is None: + return ctx, [] + hg.update(repo, n) + replacements = [(oldctx.node(), (newnode,)), + (ctx.node(), (n,)), + (newnode, (n,)), + ] + for ich in internalchanges: + replacements.append((ich, (n,))) + return repo[n], replacements -def message(ui, state, ha, opts): - repo, ctx = state.repo, state.parentctx - oldctx = repo[ha] - hg.update(repo, ctx.node()) - stats = applychanges(ui, repo, oldctx, opts) - if stats and stats[3] > 0: - raise error.InterventionRequired( - _('Fix up the change and run hg histedit --continue')) - message = oldctx.description() - commit = commitfuncfor(repo, oldctx) - editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess') - new = commit(text=message, user=oldctx.user(), date=oldctx.date(), - extra=oldctx.extra(), editor=editor) - newctx = repo[new] - if oldctx.node() != newctx.node(): - return newctx, [(oldctx.node(), (new,))] - # We didn't make an edit, so just indicate no replaced nodes - return newctx, [] +class rollup(fold): + def skipprompt(self): + return True + +class drop(histeditaction): + def run(self): + parentctx = self.repo[self.state.parentctxnode] + return parentctx, [(self.node, tuple())] + +class message(histeditaction): + def commiteditor(self): + return cmdutil.getcommiteditor(edit=True, editform='histedit.mess') def findoutgoing(ui, repo, remote=None, force=False, opts={}): """utility function to find the first outgoing changeset @@ -501,15 +641,16 @@ @command('histedit', [('', 'commands', '', - _('Read history edits from the specified file.')), + _('read history edits from the specified file'), _('FILE')), ('c', 'continue', False, _('continue an edit already in progress')), + ('', 'edit-plan', False, _('edit remaining actions list')), ('k', 'keep', False, _("don't strip old nodes after edit is complete")), ('', 'abort', False, _('abort an edit in progress')), ('o', 'outgoing', False, _('changesets not found in destination')), ('f', 'force', False, _('force outgoing even for unrelated repositories')), - ('r', 'rev', [], _('first revision to be edited'))], + ('r', 'rev', [], _('first revision to be edited'), _('REV'))], _("ANCESTOR | --outgoing [URL]")) def histedit(ui, repo, *freeargs, **opts): """interactively edit changeset history @@ -552,6 +693,7 @@ # basic argument incompatibility processing outg = opts.get('outgoing') cont = opts.get('continue') + editplan = opts.get('edit_plan') abort = opts.get('abort') force = opts.get('force') rules = opts.get('commands', '') @@ -560,13 +702,18 @@ if force and not outg: raise util.Abort(_('--force only allowed with --outgoing')) if cont: - if util.any((outg, abort, revs, freeargs, rules)): + if util.any((outg, abort, revs, freeargs, rules, editplan)): raise util.Abort(_('no arguments allowed with --continue')) goal = 'continue' elif abort: - if util.any((outg, revs, freeargs, rules)): + if util.any((outg, revs, freeargs, rules, editplan)): raise util.Abort(_('no arguments allowed with --abort')) goal = 'abort' + elif editplan: + if util.any((outg, revs, freeargs)): + raise util.Abort(_('only --commands argument allowed with' + '--edit-plan')) + goal = 'edit-plan' else: if os.path.exists(os.path.join(repo.path, 'histedit-state')): raise util.Abort(_('history edit already in progress, try ' @@ -579,6 +726,10 @@ _('only one repo argument allowed with --outgoing')) else: revs.extend(freeargs) + if len(revs) == 0: + histeditdefault = ui.config('histedit', 'defaultrev') + if histeditdefault: + revs.append(histeditdefault) if len(revs) != 1: raise util.Abort( _('histedit requires exactly one ancestor revision')) @@ -589,17 +740,43 @@ # rebuild state if goal == 'continue': - state = histeditstate(repo) state.read() state = bootstrapcontinue(ui, state, opts) + elif goal == 'edit-plan': + state.read() + if not rules: + comment = editcomment % (state.parentctx, node.short(state.topmost)) + rules = ruleeditor(repo, ui, state.rules, comment) + else: + if rules == '-': + f = sys.stdin + else: + f = open(rules) + rules = f.read() + f.close() + rules = [l for l in (r.strip() for r in rules.splitlines()) + if l and not l.startswith('#')] + rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules]) + state.rules = rules + state.write() + return elif goal == 'abort': - state = histeditstate(repo) state.read() mapping, tmpnodes, leafs, _ntm = processreplacement(state) ui.debug('restore wc to old parent %s\n' % node.short(state.topmost)) + + # Recover our old commits if necessary + if not state.topmost in repo and state.backupfile: + backupfile = repo.join(state.backupfile) + f = hg.openpath(ui, backupfile) + gen = exchange.readbundle(ui, f, backupfile) + changegroup.addchangegroup(repo, gen, 'histedit', + 'bundle:' + backupfile) + os.remove(backupfile) + # check whether we should update away parentnodes = [c.node() for c in repo[None].parents()] - for n in leafs | set([state.parentctx.node()]): + for n in leafs | set([state.parentctxnode]): if n in parentnodes: hg.clean(repo, state.topmost) break @@ -634,16 +811,8 @@ ctxs = [repo[r] for r in revs] if not rules: - rules = '\n'.join([makedesc(c) for c in ctxs]) - rules += '\n\n' - rules += editcomment % (node.short(root), node.short(topmost)) - rules = ui.edit(rules, ui.username()) - # Save edit rules in .hg/histedit-last-edit.txt in case - # the user needs to ask for help after something - # surprising happens. - f = open(repo.join('histedit-last-edit.txt'), 'w') - f.write(rules) - f.close() + comment = editcomment % (node.short(root), node.short(topmost)) + rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment) else: if rules == '-': f = sys.stdin @@ -655,23 +824,32 @@ if l and not l.startswith('#')] rules = verifyrules(rules, repo, ctxs) - parentctx = repo[root].parents()[0] + parentctxnode = repo[root].parents()[0].node() - state.parentctx = parentctx + state.parentctxnode = parentctxnode state.rules = rules state.keep = keep state.topmost = topmost state.replacements = replacements + # Create a backup so we can always abort completely. + backupfile = None + if not obsolete.isenabled(repo, obsolete.createmarkersopt): + backupfile = repair._bundle(repo, [parentctxnode], [topmost], root, + 'histedit') + state.backupfile = backupfile + while state.rules: state.write() action, ha = state.rules.pop(0) - ui.debug('histedit: processing %s %s\n' % (action, ha)) - actfunc = actiontable[action] - state.parentctx, replacement_ = actfunc(ui, state, ha, opts) + ui.debug('histedit: processing %s %s\n' % (action, ha[:12])) + actobj = actiontable[action].fromrule(state, ha) + parentctx, replacement_ = actobj.run() + state.parentctxnode = parentctx.node() state.replacements.extend(replacement_) + state.write() - hg.update(repo, state.parentctx.node()) + hg.update(repo, state.parentctxnode) mapping, tmpnodes, created, ntm = processreplacement(state) if mapping: @@ -707,80 +885,22 @@ if os.path.exists(repo.sjoin('undo')): os.unlink(repo.sjoin('undo')) -def gatherchildren(repo, ctx): - # is there any new commit between the expected parent and "." - # - # note: does not take non linear new change in account (but previous - # implementation didn't used them anyway (issue3655) - newchildren = [c.node() for c in repo.set('(%d::.)', ctx)] - if ctx.node() != node.nullid: - if not newchildren: - # `ctx` should match but no result. This means that - # currentnode is not a descendant from ctx. - msg = _('%s is not an ancestor of working directory') - hint = _('use "histedit --abort" to clear broken state') - raise util.Abort(msg % ctx, hint=hint) - newchildren.pop(0) # remove ctx - return newchildren +def bootstrapcontinue(ui, state, opts): + repo = state.repo + action, currentnode = state.rules.pop(0) -def bootstrapcontinue(ui, state, opts): - repo, parentctx = state.repo, state.parentctx - action, currentnode = state.rules.pop(0) - ctx = repo[currentnode] + actobj = actiontable[action].fromrule(state, currentnode) - newchildren = gatherchildren(repo, parentctx) - - # Commit dirty working directory if necessary - new = None s = repo.status() if s.modified or s.added or s.removed or s.deleted: - # prepare the message for the commit to comes - if action in ('f', 'fold', 'r', 'roll'): - message = 'fold-temp-revision %s' % currentnode - else: - message = ctx.description() - editopt = action in ('e', 'edit', 'm', 'mess') - canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'} - editform = 'histedit.%s' % canonaction.get(action, action) - editor = cmdutil.getcommiteditor(edit=editopt, editform=editform) - commit = commitfuncfor(repo, ctx) - new = commit(text=message, user=ctx.user(), date=ctx.date(), - extra=ctx.extra(), editor=editor) - if new is not None: - newchildren.append(new) - - replacements = [] - # track replacements - if ctx.node() not in newchildren: - # note: new children may be empty when the changeset is dropped. - # this happen e.g during conflicting pick where we revert content - # to parent. - replacements.append((ctx.node(), tuple(newchildren))) + actobj.continuedirty() + s = repo.status() + if s.modified or s.added or s.removed or s.deleted: + raise util.Abort(_("working copy still dirty")) - if action in ('f', 'fold', 'r', 'roll'): - if newchildren: - # finalize fold operation if applicable - if new is None: - new = newchildren[-1] - else: - newchildren.pop() # remove new from internal changes - foldopts = opts - if action in ('r', 'roll'): - foldopts = foldopts.copy() - foldopts['rollup'] = True - parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, - foldopts, newchildren) - replacements.extend(repl) - else: - # newchildren is empty if the fold did not result in any commit - # this happen when all folded change are discarded during the - # merge. - replacements.append((ctx.node(), (parentctx.node(),))) - elif newchildren: - # otherwise update "parentctx" before proceeding to further operation - parentctx = repo[newchildren[-1]] + parentctx, replacements = actobj.continueclean() - state.parentctx = parentctx + state.parentctxnode = parentctx.node() state.replacements.extend(replacements) return state @@ -801,19 +921,41 @@ raise util.Abort(_('cannot edit immutable changeset: %s') % root) return [c.node() for c in ctxs] -def makedesc(c): - """build a initial action line for a ctx `c` +def makedesc(repo, action, rev): + """build a initial action line for a ctx line are in the form: - pick + """ + ctx = repo[rev] summary = '' - if c.description(): - summary = c.description().splitlines()[0] - line = 'pick %s %d %s' % (c, c.rev(), summary) + if ctx.description(): + summary = ctx.description().splitlines()[0] + line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary) # trim to 80 columns so it's not stupidly wide in my editor - return util.ellipsis(line, 80) + maxlen = repo.ui.configint('histedit', 'linelen', default=80) + maxlen = max(maxlen, 22) # avoid truncating hash + return util.ellipsis(line, maxlen) + +def ruleeditor(repo, ui, rules, editcomment=""): + """open an editor to edit rules + + rules are in the format [ [act, ctx], ...] like in state.rules + """ + rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules]) + rules += '\n\n' + rules += editcomment + rules = ui.edit(rules, ui.username()) + + # Save edit rules in .hg/histedit-last-edit.txt in case + # the user needs to ask for help after something + # surprising happens. + f = open(repo.join('histedit-last-edit.txt'), 'w') + f.write(rules) + f.close() + + return rules def verifyrules(rules, repo, ctxs): """Verify that there exists exactly one edit rule per given changeset. @@ -822,7 +964,7 @@ or a rule on a changeset outside of the user-given range. """ parsed = [] - expected = set(str(c) for c in ctxs) + expected = set(c.hex() for c in ctxs) seen = set() for r in rules: if ' ' not in r: @@ -830,22 +972,24 @@ action, rest = r.split(' ', 1) ha = rest.strip().split(' ', 1)[0] try: - ha = str(repo[ha]) # ensure its a short hash + ha = repo[ha].hex() except error.RepoError: - raise util.Abort(_('unknown changeset %s listed') % ha) + raise util.Abort(_('unknown changeset %s listed') % ha[:12]) if ha not in expected: raise util.Abort( _('may not use changesets other than the ones listed')) if ha in seen: - raise util.Abort(_('duplicated command for changeset %s') % ha) + raise util.Abort(_('duplicated command for changeset %s') % + ha[:12]) seen.add(ha) if action not in actiontable: raise util.Abort(_('unknown action "%s"') % action) parsed.append([action, ha]) missing = sorted(expected - seen) # sort to stabilize output if missing: - raise util.Abort(_('missing rules for changeset %s') % missing[0], - hint=_('do you want to use the drop action?')) + raise util.Abort(_('missing rules for changeset %s') % + missing[0][:12], + hint=_('do you want to use the drop action?')) return parsed def processreplacement(state): @@ -965,6 +1109,23 @@ finally: release(lock) +def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs): + if isinstance(nodelist, str): + nodelist = [nodelist] + if os.path.exists(os.path.join(repo.path, 'histedit-state')): + state = histeditstate(repo) + state.read() + histedit_nodes = set([repo[rulehash].node() for (action, rulehash) + in state.rules if rulehash in repo]) + strip_nodes = set([repo[n].node() for n in nodelist]) + common_nodes = histedit_nodes & strip_nodes + if common_nodes: + raise util.Abort(_("histedit in progress, can't strip %s") + % ', '.join(node.short(x) for x in common_nodes)) + return orig(ui, repo, nodelist, *args, **kwargs) + +extensions.wrapfunction(repair, 'strip', stripwrapper) + def summaryhook(ui, repo): if not os.path.exists(repo.join('histedit-state')): return diff -r 612ed41ae359 -r e89f909edffa hgext/keyword.py --- a/hgext/keyword.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/keyword.py Thu Apr 16 20:57:51 2015 -0500 @@ -506,7 +506,10 @@ kwt = kwtools['templater'] wctx = repo[None] status = _status(ui, repo, wctx, kwt, *pats, **opts) - cwd = pats and repo.getcwd() or '' + if pats: + cwd = repo.getcwd() + else: + cwd = '' files = [] if not opts.get('unknown') or opts.get('all'): files = sorted(status.modified + status.added + status.clean) @@ -640,11 +643,10 @@ # shrink keywords read from working dir self.lines = kwt.shrinklines(self.fname, self.lines) - def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None, - opts=None, prefix=''): + def kwdiff(orig, *args, **kwargs): '''Monkeypatch patch.diff to avoid expansion.''' kwt.restrict = True - return orig(repo, node1, node2, match, changes, opts, prefix) + return orig(*args, **kwargs) def kwweb_skip(orig, web, req, tmpl): '''Wraps webcommands.x turning off keyword expansion.''' @@ -734,16 +736,10 @@ extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp) extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) - extensions.wrapfunction(patch, 'diff', kw_diff) + extensions.wrapfunction(patch, 'diff', kwdiff) extensions.wrapfunction(cmdutil, 'amend', kw_amend) extensions.wrapfunction(cmdutil, 'copy', kw_copy) + extensions.wrapfunction(cmdutil, 'dorecord', kw_dorecord) for c in 'annotate changeset rev filediff diff'.split(): extensions.wrapfunction(webcommands, c, kwweb_skip) - for name in recordextensions.split(): - try: - record = extensions.find(name) - extensions.wrapfunction(record, 'dorecord', kw_dorecord) - except KeyError: - pass - repo.__class__ = kwrepo diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/lfcommands.py --- a/hgext/largefiles/lfcommands.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/lfcommands.py Thu Apr 16 20:57:51 2015 -0500 @@ -437,7 +437,7 @@ return totalsuccess, totalmissing def updatelfiles(ui, repo, filelist=None, printmessage=None, - normallookup=False, checked=False): + normallookup=False): '''Update largefiles according to standins in the working directory If ``printmessage`` is other than ``None``, it means "print (or @@ -464,16 +464,12 @@ shutil.copyfile(abslfile, abslfile + '.orig') util.unlinkpath(absstandin + '.orig') expecthash = lfutil.readstandin(repo, lfile) - if (expecthash != '' and - (checked or - not os.path.exists(abslfile) or - expecthash != lfutil.hashfile(abslfile))): + if expecthash != '': if lfile not in repo[None]: # not switched to normal file util.unlinkpath(abslfile, ignoremissing=True) # use normallookup() to allocate an entry in largefiles - # dirstate, because lack of it misleads - # lfilesrepo.status() into recognition that such cache - # missing files are removed. + # dirstate to prevent lfilesrepo.status() from reporting + # missing files as removed. lfdirstate.normallookup(lfile) update[lfile] = expecthash else: diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/lfutil.py --- a/hgext/largefiles/lfutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/lfutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -82,9 +82,10 @@ return path and os.path.exists(path) def findfile(repo, hash): - if instore(repo, hash): + path, exists = findstorepath(repo, hash) + if exists: repo.ui.note(_('found %s in store\n') % hash) - return storepath(repo, hash) + return path elif inusercache(repo.ui, hash): repo.ui.note(_('found %s in system cache\n') % hash) path = storepath(repo, hash) @@ -164,11 +165,30 @@ for f in repo[rev].walk(matcher) if rev is not None or repo.dirstate[f] != '?'] -def instore(repo, hash): - return os.path.exists(storepath(repo, hash)) +def instore(repo, hash, forcelocal=False): + return os.path.exists(storepath(repo, hash, forcelocal)) + +def storepath(repo, hash, forcelocal=False): + if not forcelocal and repo.shared(): + return repo.vfs.reljoin(repo.sharedpath, longname, hash) + return repo.join(longname, hash) -def storepath(repo, hash): - return repo.join(os.path.join(longname, hash)) +def findstorepath(repo, hash): + '''Search through the local store path(s) to find the file for the given + hash. If the file is not found, its path in the primary store is returned. + The return value is a tuple of (path, exists(path)). + ''' + # For shared repos, the primary store is in the share source. But for + # backward compatibility, force a lookup in the local store if it wasn't + # found in the share source. + path = storepath(repo, hash, False) + + if instore(repo, hash): + return (path, True) + elif repo.shared() and instore(repo, hash, True): + return storepath(repo, hash, True) + + return (path, False) def copyfromcache(repo, hash, filename): '''Copy the specified largefile from the repo or system cache to @@ -388,7 +408,7 @@ lfdirstate.drop(lfile) def markcommitted(orig, ctx, node): - repo = ctx._repo + repo = ctx.repo() orig(node) diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/localstore.py --- a/hgext/largefiles/localstore.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/localstore.py Thu Apr 16 20:57:51 2015 -0500 @@ -55,9 +55,9 @@ return False expecthash = fctx.data()[0:40] - storepath = lfutil.storepath(self.remote, expecthash) + storepath, exists = lfutil.findstorepath(self.remote, expecthash) verified.add(key) - if not lfutil.instore(self.remote, expecthash): + if not exists: self.ui.warn( _('changeset %s: %s references missing %s\n') % (cset, filename, storepath)) diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/overrides.py --- a/hgext/largefiles/overrides.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/overrides.py Thu Apr 16 20:57:51 2015 -0500 @@ -14,7 +14,6 @@ from mercurial import hg, util, cmdutil, scmutil, match as match_, \ archival, pathutil, revset from mercurial.i18n import _ -from mercurial.node import hex import lfutil import lfcommands @@ -304,17 +303,47 @@ return matchandpats pats = set(p) - # TODO: handling of patterns in both cases below + + def fixpats(pat, tostandin=lfutil.standin): + kindpat = match_._patsplit(pat, None) + + if kindpat[0] is not None: + return kindpat[0] + ':' + tostandin(kindpat[1]) + return tostandin(kindpat[1]) + if m._cwd: - if os.path.isabs(m._cwd): - # TODO: handle largefile magic when invoked from other cwd - return matchandpats - back = (m._cwd.count('/') + 1) * '../' - pats.update(back + lfutil.standin(m._cwd + '/' + f) for f in p) + hglf = lfutil.shortname + back = util.pconvert(m.rel(hglf)[:-len(hglf)]) + + def tostandin(f): + # The file may already be a standin, so trucate the back + # prefix and test before mangling it. This avoids turning + # 'glob:../.hglf/foo*' into 'glob:../.hglf/../.hglf/foo*'. + if f.startswith(back) and lfutil.splitstandin(f[len(back):]): + return f + + # An absolute path is from outside the repo, so truncate the + # path to the root before building the standin. Otherwise cwd + # is somewhere in the repo, relative to root, and needs to be + # prepended before building the standin. + if os.path.isabs(m._cwd): + f = f[len(back):] + else: + f = m._cwd + '/' + f + return back + lfutil.standin(f) + + pats.update(fixpats(f, tostandin) for f in p) else: - pats.update(lfutil.standin(f) for f in p) + def tostandin(f): + if lfutil.splitstandin(f): + return f + return lfutil.standin(f) + pats.update(fixpats(f, tostandin) for f in p) for i in range(0, len(m._files)): + # Don't add '.hglf' to m.files, since that is already covered by '.' + if m._files[i] == '.': + continue standin = lfutil.standin(m._files[i]) # If the "standin" is a directory, append instead of replace to # support naming a directory on the command line with only @@ -325,7 +354,6 @@ elif m._files[i] not in repo[ctx.node()] \ and repo.wvfs.isdir(standin): m._files.append(standin) - pats.add(standin) m._fmap = set(m._files) m._always = False @@ -338,6 +366,7 @@ return r m.matchfn = lfmatchfn + ui.debug('updated patterns: %s\n' % sorted(pats)) return m, pats # For hg log --patch, the match object is used in two different senses: @@ -346,8 +375,8 @@ # The magic matchandpats override should be used for case (1) but not for # case (2). def overridemakelogfilematcher(repo, pats, opts): - pctx = repo[None] - match, pats = oldmatchandpats(pctx, pats, opts) + wctx = repo[None] + match, pats = oldmatchandpats(wctx, pats, opts) return lambda rev: match oldmatchandpats = installmatchandpatsfn(overridematchandpats) @@ -379,36 +408,6 @@ else: orig(ui, repo, *pats, **opts) -# Override needs to refresh standins so that update's normal merge -# will go through properly. Then the other update hook (overriding repo.update) -# will get the new files. Filemerge is also overridden so that the merge -# will merge standins correctly. -def overrideupdate(orig, ui, repo, *pats, **opts): - # Need to lock between the standins getting updated and their - # largefiles getting updated - wlock = repo.wlock() - try: - if opts['check']: - lfdirstate = lfutil.openlfdirstate(ui, repo) - unsure, s = lfdirstate.status( - match_.always(repo.root, repo.getcwd()), - [], False, False, False) - - mod = len(s.modified) > 0 - for lfile in unsure: - standin = lfutil.standin(lfile) - if repo['.'][standin].data().strip() != \ - lfutil.hashfile(repo.wjoin(lfile)): - mod = True - else: - lfdirstate.normal(lfile) - lfdirstate.write() - if mod: - raise util.Abort(_('uncommitted changes')) - return orig(ui, repo, *pats, **opts) - finally: - wlock.release() - # Before starting the manifest merge, merge.updates will call # _checkunknownfile to check if there are any files in the merged-in # changeset that collide with unknown files in the working copy. @@ -548,6 +547,15 @@ repo.wwrite(fcd.path(), fco.data(), fco.flags()) return 0 +def copiespathcopies(orig, ctx1, ctx2, match=None): + copies = orig(ctx1, ctx2, match=match) + updated = {} + + for k, v in copies.iteritems(): + updated[lfutil.splitstandin(k) or k] = lfutil.splitstandin(v) or v + + return updated + # Copy first changes the matchers to match standins instead of # largefiles. Then it overrides util.copyfile in that function it # checks if the destination largefile already exists. It also keeps a @@ -559,16 +567,6 @@ # this isn't legal, let the original function deal with it return orig(ui, repo, pats, opts, rename) - def makestandin(relpath): - path = pathutil.canonpath(repo.root, repo.getcwd(), relpath) - return os.path.join(repo.wjoin(lfutil.standin(path))) - - fullpats = scmutil.expandpats(pats) - dest = fullpats[-1] - - if os.path.isdir(dest): - if not os.path.isdir(makestandin(dest)): - os.makedirs(makestandin(dest)) # This could copy both lfiles and normal files in one command, # but we don't want to do that. First replace their matcher to # only match normal files and run it, then replace it to just @@ -595,6 +593,17 @@ except OSError: return result + def makestandin(relpath): + path = pathutil.canonpath(repo.root, repo.getcwd(), relpath) + return os.path.join(repo.wjoin(lfutil.standin(path))) + + fullpats = scmutil.expandpats(pats) + dest = fullpats[-1] + + if os.path.isdir(dest): + if not os.path.isdir(makestandin(dest)): + os.makedirs(makestandin(dest)) + try: try: # When we call orig below it creates the standins but we don't add @@ -694,7 +703,7 @@ # commits. Update the standins then run the original revert, changing # the matcher to hit standins instead of largefiles. Based on the # resulting standins update the largefiles. -def overriderevert(orig, ui, repo, *pats, **opts): +def overriderevert(orig, ui, repo, ctx, parents, *pats, **opts): # Because we put the standins in a bad state (by updating them) # and then return them to a correct state we need to lock to # prevent others from changing them in their incorrect state. @@ -711,14 +720,23 @@ oldstandins = lfutil.getstandinsstate(repo) - def overridematch(ctx, pats=[], opts={}, globbed=False, + def overridematch(mctx, pats=[], opts={}, globbed=False, default='relpath'): - match = oldmatch(ctx, pats, opts, globbed, default) + match = oldmatch(mctx, pats, opts, globbed, default) m = copy.copy(match) + + # revert supports recursing into subrepos, and though largefiles + # currently doesn't work correctly in that case, this match is + # called, so the lfdirstate above may not be the correct one for + # this invocation of match. + lfdirstate = lfutil.openlfdirstate(mctx.repo().ui, mctx.repo(), + False) + def tostandin(f): - if lfutil.standin(f) in ctx: - return lfutil.standin(f) - elif lfutil.standin(f) in repo[None]: + standin = lfutil.standin(f) + if standin in ctx or standin in mctx: + return standin + elif standin in repo[None] or lfdirstate[f] == 'r': return None return f m._files = [tostandin(f) for f in m._files] @@ -728,13 +746,13 @@ def matchfn(f): if lfutil.isstandin(f): return (origmatchfn(lfutil.splitstandin(f)) and - (f in repo[None] or f in ctx)) + (f in ctx or f in mctx)) return origmatchfn(f) m.matchfn = matchfn return m oldmatch = installmatchfn(overridematch) try: - orig(ui, repo, *pats, **opts) + orig(ui, repo, ctx, parents, *pats, **opts) finally: restorematchfn() @@ -820,6 +838,14 @@ sourcerepo, destrepo = result repo = destrepo.local() + # If largefiles is required for this repo, permanently enable it locally + if 'largefiles' in repo.requirements: + fp = repo.vfs('hgrc', 'a', text=True) + try: + fp.write('\n[extensions]\nlargefiles=\n') + finally: + fp.close() + # Caching is implicitly limited to 'rev' option, since the dest repo was # truncated at that point. The user may expect a download count with # this option, so attempt whether or not this is a largefile repo. @@ -845,7 +871,7 @@ repo._lfcommithooks.pop() def overridearchive(orig, repo, dest, node, kind, decode=True, matchfn=None, - prefix=None, mtime=None, subrepos=None): + prefix='', mtime=None, subrepos=None): # No need to lock because we are only reading history and # largefile caches, neither of which are modified. lfcommands.cachelfiles(repo.ui, repo, node) @@ -873,24 +899,8 @@ archiver = archival.archivers[kind](dest, mtime or ctx.date()[0]) if repo.ui.configbool("ui", "archivemeta", True): - def metadata(): - base = 'repo: %s\nnode: %s\nbranch: %s\n' % ( - hex(repo.changelog.node(0)), hex(node), ctx.branch()) - - tags = ''.join('tag: %s\n' % t for t in ctx.tags() - if repo.tagtype(t) == 'global') - if not tags: - repo.ui.pushbuffer() - opts = {'template': '{latesttag}\n{latesttagdistance}', - 'style': '', 'patch': None, 'git': None} - cmdutil.show_changeset(repo.ui, repo, opts).show(ctx) - ltags, dist = repo.ui.popbuffer().split('\n') - tags = ''.join('latesttag: %s\n' % t for t in ltags.split(':')) - tags += 'latesttagdistance: %s\n' % dist - - return base + tags - - write('.hg_archival.txt', 0644, False, metadata) + write('.hg_archival.txt', 0644, False, + lambda: archival.buildmetadata(ctx)) for f in ctx: ff = ctx.flags(f) @@ -972,8 +982,8 @@ # standin until a commit. cmdutil.bailifchanged() raises an exception # if the repo has uncommitted changes. Wrap it to also check if # largefiles were changed. This is used by bisect, backout and fetch. -def overridebailifchanged(orig, repo): - orig(repo) +def overridebailifchanged(orig, repo, *args, **kwargs): + orig(repo, *args, **kwargs) repo.lfstatus = True s = repo.status() repo.lfstatus = False @@ -1247,6 +1257,20 @@ if not f in notbad: origbadfn(f, msg) m.bad = lfbadfn + + origvisitdirfn = m.visitdir + def lfvisitdirfn(dir): + if dir == lfutil.shortname: + return True + ret = origvisitdirfn(dir) + if ret: + return ret + lf = lfutil.splitstandin(dir) + if lf is None: + return False + return origvisitdirfn(lf) + m.visitdir = lfvisitdirfn + for f in ctx.walk(m): fp = cmdutil.makefileobj(repo, opts.get('output'), ctx.node(), pathname=f) @@ -1294,45 +1318,37 @@ # (*) don't care # (*1) deprecated, but used internally (e.g: "rebase --collapse") - linearmerge = not branchmerge and not force and not partial + lfdirstate = lfutil.openlfdirstate(repo.ui, repo) + unsure, s = lfdirstate.status(match_.always(repo.root, + repo.getcwd()), + [], False, False, False) + pctx = repo['.'] + for lfile in unsure + s.modified: + lfileabs = repo.wvfs.join(lfile) + if not os.path.exists(lfileabs): + continue + lfhash = lfutil.hashrepofile(repo, lfile) + standin = lfutil.standin(lfile) + lfutil.writestandin(repo, standin, lfhash, + lfutil.getexecutable(lfileabs)) + if (standin in pctx and + lfhash == lfutil.readstandin(repo, lfile, '.')): + lfdirstate.normal(lfile) + for lfile in s.added: + lfutil.updatestandin(repo, lfutil.standin(lfile)) + lfdirstate.write() - if linearmerge or (branchmerge and force and not partial): - # update standins for linear-merge or force-branch-merge, - # because largefiles in the working directory may be modified - lfdirstate = lfutil.openlfdirstate(repo.ui, repo) - unsure, s = lfdirstate.status(match_.always(repo.root, - repo.getcwd()), - [], False, False, False) - pctx = repo['.'] - for lfile in unsure + s.modified: - lfileabs = repo.wvfs.join(lfile) - if not os.path.exists(lfileabs): - continue - lfhash = lfutil.hashrepofile(repo, lfile) - standin = lfutil.standin(lfile) - lfutil.writestandin(repo, standin, lfhash, - lfutil.getexecutable(lfileabs)) - if (standin in pctx and - lfhash == lfutil.readstandin(repo, lfile, '.')): - lfdirstate.normal(lfile) - for lfile in s.added: - lfutil.updatestandin(repo, lfutil.standin(lfile)) - lfdirstate.write() - - if linearmerge: - # Only call updatelfiles on the standins that have changed - # to save time - oldstandins = lfutil.getstandinsstate(repo) + oldstandins = lfutil.getstandinsstate(repo) result = orig(repo, node, branchmerge, force, partial, *args, **kwargs) - filelist = None - if linearmerge: - newstandins = lfutil.getstandinsstate(repo) - filelist = lfutil.getlfilestoupdate(oldstandins, newstandins) + newstandins = lfutil.getstandinsstate(repo) + filelist = lfutil.getlfilestoupdate(oldstandins, newstandins) + if branchmerge or force or partial: + filelist.extend(s.deleted + s.removed) lfcommands.updatelfiles(repo.ui, repo, filelist=filelist, - normallookup=partial, checked=linearmerge) + normallookup=partial) return result finally: diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/reposetup.py --- a/hgext/largefiles/reposetup.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/reposetup.py Thu Apr 16 20:57:51 2015 -0500 @@ -10,7 +10,7 @@ import copy import os -from mercurial import error, manifest, match as match_, util +from mercurial import error, match as match_, util from mercurial.i18n import _ from mercurial import scmutil, localrepo @@ -38,17 +38,18 @@ def __getitem__(self, changeid): ctx = super(lfilesrepo, self).__getitem__(changeid) if self.lfstatus: - class lfilesmanifestdict(manifest.manifestdict): - def __contains__(self, filename): - orig = super(lfilesmanifestdict, self).__contains__ - return orig(filename) or orig(lfutil.standin(filename)) class lfilesctx(ctx.__class__): def files(self): filenames = super(lfilesctx, self).files() return [lfutil.splitstandin(f) or f for f in filenames] def manifest(self): man1 = super(lfilesctx, self).manifest() - man1.__class__ = lfilesmanifestdict + class lfilesmanifest(man1.__class__): + def __contains__(self, filename): + orig = super(lfilesmanifest, self).__contains__ + return (orig(filename) or + orig(lfutil.standin(filename))) + man1.__class__ = lfilesmanifest return man1 def filectx(self, path, fileid=None, filelog=None): orig = super(lfilesctx, self).filectx @@ -329,10 +330,10 @@ actualfiles.append(lf) if not matcheddir: # There may still be normal files in the dir, so - # make sure a directory is in the list, which - # forces status to walk and call the match - # function on the matcher. Windows does NOT - # require this. + # add a directory to the list, which + # forces status/dirstate to walk all files and + # call the match function on the matcher, even + # on case sensitive filesystems. actualfiles.append('.') matcheddir = True # Nothing in dir, so readd it diff -r 612ed41ae359 -r e89f909edffa hgext/largefiles/uisetup.py --- a/hgext/largefiles/uisetup.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/largefiles/uisetup.py Thu Apr 16 20:57:51 2015 -0500 @@ -9,7 +9,7 @@ '''setup for largefiles extension: uisetup''' from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \ - httppeer, merge, scmutil, sshpeer, wireproto, revset, subrepo + httppeer, merge, scmutil, sshpeer, wireproto, revset, subrepo, copies from mercurial.i18n import _ from mercurial.hgweb import hgweb_mod, webcommands @@ -37,6 +37,8 @@ extensions.wrapfunction(cmdutil, 'remove', overrides.cmdutilremove) extensions.wrapfunction(cmdutil, 'forget', overrides.cmdutilforget) + extensions.wrapfunction(copies, 'pathcopies', overrides.copiespathcopies) + # Subrepos call status function entry = extensions.wrapcommand(commands.table, 'status', overrides.overridestatus) @@ -74,8 +76,6 @@ entry[1].extend(summaryopt) cmdutil.summaryremotehooks.add('largefiles', overrides.summaryremotehook) - entry = extensions.wrapcommand(commands.table, 'update', - overrides.overrideupdate) entry = extensions.wrapcommand(commands.table, 'pull', overrides.overridepull) pullopt = [('', 'all-largefiles', None, @@ -111,11 +111,7 @@ entry = extensions.wrapfunction(subrepo.hgsubrepo, 'dirty', overrides.overridedirty) - # Backout calls revert so we need to override both the command and the - # function - entry = extensions.wrapcommand(commands.table, 'revert', - overrides.overriderevert) - entry = extensions.wrapfunction(commands, 'revert', + entry = extensions.wrapfunction(cmdutil, 'revert', overrides.overriderevert) extensions.wrapfunction(archival, 'archive', overrides.overridearchive) diff -r 612ed41ae359 -r e89f909edffa hgext/mq.py --- a/hgext/mq.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/mq.py Thu Apr 16 20:57:51 2015 -0500 @@ -418,7 +418,10 @@ gitmode = ui.configbool('mq', 'git', None) if gitmode is None: raise error.ConfigError - self.gitmode = gitmode and 'yes' or 'no' + if gitmode: + self.gitmode = 'yes' + else: + self.gitmode = 'no' except error.ConfigError: self.gitmode = ui.config('mq', 'git', 'auto').lower() self.plainmode = ui.configbool('mq', 'plain', False) @@ -610,7 +613,11 @@ return True, '' def explainpushable(self, idx, all_patches=False): - write = all_patches and self.ui.write or self.ui.warn + if all_patches: + write = self.ui.write + else: + write = self.ui.warn + if all_patches or self.ui.verbose: if isinstance(idx, str): idx = self.series.index(idx) @@ -923,7 +930,8 @@ self.applied.append(statusentry(n, patchname)) if patcherr: - self.ui.warn(_("patch failed, rejects left in working dir\n")) + self.ui.warn(_("patch failed, rejects left in working " + "directory\n")) err = 2 break @@ -1825,7 +1833,11 @@ self.ui.write(pfx) if summary: ph = patchheader(self.join(patchname), self.plainmode) - msg = ph.message and ph.message[0] or '' + if ph.message: + msg = ph.message[0] + else: + msg = '' + if self.ui.formatted(): width = self.ui.termwidth() - len(pfx) - len(patchname) - 2 if width > 0: @@ -2228,7 +2240,10 @@ ui.write(_("all patches applied\n")) return 1 - length = opts.get('first') and 1 or None + if opts.get('first'): + length = 1 + else: + length = None q.qseries(repo, start=start, length=length, status='U', summary=opts.get('summary')) @@ -2454,7 +2469,11 @@ Returns 0 on success.""" q = repo.mq - t = q.applied and q.seriesend(True) or 0 + if q.applied: + t = q.seriesend(True) + else: + t = 0 + if t: q.qseries(repo, start=t - 1, length=1, status='A', summary=opts.get('summary')) diff -r 612ed41ae359 -r e89f909edffa hgext/notify.py --- a/hgext/notify.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/notify.py Thu Apr 16 20:57:51 2015 -0500 @@ -340,7 +340,10 @@ maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) prev = ctx.p1().node() - ref = ref and ref.node() or ctx.node() + if ref: + ref = ref.node() + else: + ref = ctx.node() chunks = patch.diff(self.repo, prev, ref, opts=patch.diffallopts(self.ui)) difflines = ''.join(chunks).splitlines() diff -r 612ed41ae359 -r e89f909edffa hgext/pager.py --- a/hgext/pager.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/pager.py Thu Apr 16 20:57:51 2015 -0500 @@ -149,6 +149,8 @@ usepager = True break + setattr(ui, 'pageractive', usepager) + if usepager: ui.setconfig('ui', 'formatted', ui.formatted(), 'pager') ui.setconfig('ui', 'interactive', False, 'pager') @@ -157,7 +159,12 @@ _runpager(ui, p) return orig(ui, options, cmd, cmdfunc) - extensions.wrapfunction(dispatch, '_runcommand', pagecmd) + # Wrap dispatch._runcommand after color is loaded so color can see + # ui.pageractive. Otherwise, if we loaded first, color's wrapped + # dispatch._runcommand would run without having access to ui.pageractive. + def afterloaded(loaded): + extensions.wrapfunction(dispatch, '_runcommand', pagecmd) + extensions.afterloaded('color', afterloaded) def extsetup(ui): commands.globalopts.append( diff -r 612ed41ae359 -r e89f909edffa hgext/patchbomb.py --- a/hgext/patchbomb.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/patchbomb.py Thu Apr 16 20:57:51 2015 -0500 @@ -186,7 +186,7 @@ """ ui = repo.ui prev = repo['.'].rev() - for r in scmutil.revrange(repo, revs): + for r in revs: if r == prev and (repo[None].files() or repo[None].deleted()): ui.warn(_('warning: working directory has ' 'uncommitted changes\n')) @@ -339,14 +339,13 @@ url = hg.parseurl(url)[0] ui.status(_('comparing with %s\n') % util.hidepassword(url)) - revs = [r for r in scmutil.revrange(repo, revs) if r >= 0] + revs = [r for r in revs if r >= 0] if not revs: revs = [len(repo) - 1] revs = repo.revs('outgoing(%s) and ::%ld', dest or '', revs) if not revs: ui.status(_("no changes found\n")) - return [] - return [str(r) for r in revs] + return revs emailopts = [ ('', 'body', None, _('send patches as inline message text (default)')), @@ -489,7 +488,10 @@ if outgoing or bundle: if len(revs) > 1: raise util.Abort(_("too many destinations")) - dest = revs and revs[0] or None + if revs: + dest = revs[0] + else: + dest = None revs = [] if rev: @@ -497,10 +499,11 @@ raise util.Abort(_('use only one form to specify the revision')) revs = rev + revs = scmutil.revrange(repo, revs) if outgoing: - revs = _getoutgoing(repo, dest, rev) + revs = _getoutgoing(repo, dest, revs) if bundle: - opts['revs'] = revs + opts['revs'] = [str(r) for r in revs] # start if date: diff -r 612ed41ae359 -r e89f909edffa hgext/rebase.py --- a/hgext/rebase.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/rebase.py Thu Apr 16 20:57:51 2015 -0500 @@ -231,7 +231,8 @@ hint = _('use "hg rebase --abort" to clear broken state') raise util.Abort(msg, hint=hint) if abortf: - return abort(repo, originalwd, target, state) + return abort(repo, originalwd, target, state, + activebookmark=activebookmark) else: if srcf and basef: raise util.Abort(_('cannot specify both a ' @@ -852,8 +853,11 @@ return False -def abort(repo, originalwd, target, state): - 'Restore the repository to its original state' +def abort(repo, originalwd, target, state, activebookmark=None): + '''Restore the repository to its original state. Additional args: + + activebookmark: the name of the bookmark that should be active after the + restore''' dstates = [s for s in state.values() if s >= 0] immutable = [d for d in dstates if not repo[d].mutable()] cleanup = True @@ -883,6 +887,9 @@ # no backup of rebased cset versions needed repair.strip(repo.ui, repo, strippoints) + if activebookmark: + bookmarks.setcurrent(repo, activebookmark) + clearstatus(repo) repo.ui.warn(_('rebase aborted\n')) return 0 diff -r 612ed41ae359 -r e89f909edffa hgext/record.py --- a/hgext/record.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/record.py Thu Apr 16 20:57:51 2015 -0500 @@ -8,409 +8,18 @@ '''commands to interactively select changes for commit/qrefresh''' from mercurial.i18n import _ -from mercurial import cmdutil, commands, extensions, hg, patch +from mercurial import cmdutil, commands, extensions from mercurial import util -import copy, cStringIO, errno, os, re, shutil, tempfile cmdtable = {} command = cmdutil.command(cmdtable) testedwith = 'internal' -lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') - -def scanpatch(fp): - """like patch.iterhunks, but yield different events - - - ('file', [header_lines + fromfile + tofile]) - - ('context', [context_lines]) - - ('hunk', [hunk_lines]) - - ('range', (-start,len, +start,len, proc)) - """ - lr = patch.linereader(fp) - - def scanwhile(first, p): - """scan lr while predicate holds""" - lines = [first] - while True: - line = lr.readline() - if not line: - break - if p(line): - lines.append(line) - else: - lr.push(line) - break - return lines - - while True: - line = lr.readline() - if not line: - break - if line.startswith('diff --git a/') or line.startswith('diff -r '): - def notheader(line): - s = line.split(None, 1) - return not s or s[0] not in ('---', 'diff') - header = scanwhile(line, notheader) - fromfile = lr.readline() - if fromfile.startswith('---'): - tofile = lr.readline() - header += [fromfile, tofile] - else: - lr.push(fromfile) - yield 'file', header - elif line[0] == ' ': - yield 'context', scanwhile(line, lambda l: l[0] in ' \\') - elif line[0] in '-+': - yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') - else: - m = lines_re.match(line) - if m: - yield 'range', m.groups() - else: - yield 'other', line - -class header(object): - """patch header - - XXX shouldn't we move this to mercurial/patch.py ? - """ - diffgit_re = re.compile('diff --git a/(.*) b/(.*)$') - diff_re = re.compile('diff -r .* (.*)$') - allhunks_re = re.compile('(?:index|new file|deleted file) ') - pretty_re = re.compile('(?:new file|deleted file) ') - special_re = re.compile('(?:index|new|deleted|copy|rename) ') - - def __init__(self, header): - self.header = header - self.hunks = [] - - def binary(self): - return util.any(h.startswith('index ') for h in self.header) - - def pretty(self, fp): - for h in self.header: - if h.startswith('index '): - fp.write(_('this modifies a binary file (all or nothing)\n')) - break - if self.pretty_re.match(h): - fp.write(h) - if self.binary(): - fp.write(_('this is a binary file\n')) - break - if h.startswith('---'): - fp.write(_('%d hunks, %d lines changed\n') % - (len(self.hunks), - sum([max(h.added, h.removed) for h in self.hunks]))) - break - fp.write(h) - - def write(self, fp): - fp.write(''.join(self.header)) - - def allhunks(self): - return util.any(self.allhunks_re.match(h) for h in self.header) - - def files(self): - match = self.diffgit_re.match(self.header[0]) - if match: - fromfile, tofile = match.groups() - if fromfile == tofile: - return [fromfile] - return [fromfile, tofile] - else: - return self.diff_re.match(self.header[0]).groups() - - def filename(self): - return self.files()[-1] - - def __repr__(self): - return '
' % (' '.join(map(repr, self.files()))) - - def special(self): - return util.any(self.special_re.match(h) for h in self.header) - -def countchanges(hunk): - """hunk -> (n+,n-)""" - add = len([h for h in hunk if h[0] == '+']) - rem = len([h for h in hunk if h[0] == '-']) - return add, rem - -class hunk(object): - """patch hunk - - XXX shouldn't we merge this with patch.hunk ? - """ - maxcontext = 3 - - def __init__(self, header, fromline, toline, proc, before, hunk, after): - def trimcontext(number, lines): - delta = len(lines) - self.maxcontext - if False and delta > 0: - return number + delta, lines[:self.maxcontext] - return number, lines - - self.header = header - self.fromline, self.before = trimcontext(fromline, before) - self.toline, self.after = trimcontext(toline, after) - self.proc = proc - self.hunk = hunk - self.added, self.removed = countchanges(self.hunk) - - def write(self, fp): - delta = len(self.before) + len(self.after) - if self.after and self.after[-1] == '\\ No newline at end of file\n': - delta -= 1 - fromlen = delta + self.removed - tolen = delta + self.added - fp.write('@@ -%d,%d +%d,%d @@%s\n' % - (self.fromline, fromlen, self.toline, tolen, - self.proc and (' ' + self.proc))) - fp.write(''.join(self.before + self.hunk + self.after)) - - pretty = write - - def filename(self): - return self.header.filename() - - def __repr__(self): - return '' % (self.filename(), self.fromline) - -def parsepatch(fp): - """patch -> [] of headers -> [] of hunks """ - class parser(object): - """patch parsing state machine""" - def __init__(self): - self.fromline = 0 - self.toline = 0 - self.proc = '' - self.header = None - self.context = [] - self.before = [] - self.hunk = [] - self.headers = [] - - def addrange(self, limits): - fromstart, fromend, tostart, toend, proc = limits - self.fromline = int(fromstart) - self.toline = int(tostart) - self.proc = proc - - def addcontext(self, context): - if self.hunk: - h = hunk(self.header, self.fromline, self.toline, self.proc, - self.before, self.hunk, context) - self.header.hunks.append(h) - self.fromline += len(self.before) + h.removed - self.toline += len(self.before) + h.added - self.before = [] - self.hunk = [] - self.proc = '' - self.context = context - - def addhunk(self, hunk): - if self.context: - self.before = self.context - self.context = [] - self.hunk = hunk - - def newfile(self, hdr): - self.addcontext([]) - h = header(hdr) - self.headers.append(h) - self.header = h - - def addother(self, line): - pass # 'other' lines are ignored - - def finished(self): - self.addcontext([]) - return self.headers - - transitions = { - 'file': {'context': addcontext, - 'file': newfile, - 'hunk': addhunk, - 'range': addrange}, - 'context': {'file': newfile, - 'hunk': addhunk, - 'range': addrange, - 'other': addother}, - 'hunk': {'context': addcontext, - 'file': newfile, - 'range': addrange}, - 'range': {'context': addcontext, - 'hunk': addhunk}, - 'other': {'other': addother}, - } - - p = parser() - - state = 'context' - for newstate, data in scanpatch(fp): - try: - p.transitions[state][newstate](p, data) - except KeyError: - raise patch.PatchError('unhandled transition: %s -> %s' % - (state, newstate)) - state = newstate - return p.finished() - -def filterpatch(ui, headers): - """Interactively filter patch chunks into applied-only chunks""" - - def prompt(skipfile, skipall, query, chunk): - """prompt query, and process base inputs - - - y/n for the rest of file - - y/n for the rest - - ? (help) - - q (quit) - - Return True/False and possibly updated skipfile and skipall. - """ - newpatches = None - if skipall is not None: - return skipall, skipfile, skipall, newpatches - if skipfile is not None: - return skipfile, skipfile, skipall, newpatches - while True: - resps = _('[Ynesfdaq?]' - '$$ &Yes, record this change' - '$$ &No, skip this change' - '$$ &Edit this change manually' - '$$ &Skip remaining changes to this file' - '$$ Record remaining changes to this &file' - '$$ &Done, skip remaining changes and files' - '$$ Record &all changes to all remaining files' - '$$ &Quit, recording no changes' - '$$ &? (display help)') - r = ui.promptchoice("%s %s" % (query, resps)) - ui.write("\n") - if r == 8: # ? - for c, t in ui.extractchoices(resps)[1]: - ui.write('%s - %s\n' % (c, t.lower())) - continue - elif r == 0: # yes - ret = True - elif r == 1: # no - ret = False - elif r == 2: # Edit patch - if chunk is None: - ui.write(_('cannot edit patch for whole file')) - ui.write("\n") - continue - if chunk.header.binary(): - ui.write(_('cannot edit patch for binary file')) - ui.write("\n") - continue - # Patch comment based on the Git one (based on comment at end of - # http://mercurial.selenic.com/wiki/RecordExtension) - phelp = '---' + _(""" -To remove '-' lines, make them ' ' lines (context). -To remove '+' lines, delete them. -Lines starting with # will be removed from the patch. - -If the patch applies cleanly, the edited hunk will immediately be -added to the record list. If it does not apply cleanly, a rejects -file will be generated: you can use that when you try again. If -all lines of the hunk are removed, then the edit is aborted and -the hunk is left unchanged. -""") - (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", - suffix=".diff", text=True) - ncpatchfp = None - try: - # Write the initial patch - f = os.fdopen(patchfd, "w") - chunk.header.write(f) - chunk.write(f) - f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) - f.close() - # Start the editor and wait for it to complete - editor = ui.geteditor() - ui.system("%s \"%s\"" % (editor, patchfn), - environ={'HGUSER': ui.username()}, - onerr=util.Abort, errprefix=_("edit failed")) - # Remove comment lines - patchfp = open(patchfn) - ncpatchfp = cStringIO.StringIO() - for line in patchfp: - if not line.startswith('#'): - ncpatchfp.write(line) - patchfp.close() - ncpatchfp.seek(0) - newpatches = parsepatch(ncpatchfp) - finally: - os.unlink(patchfn) - del ncpatchfp - # Signal that the chunk shouldn't be applied as-is, but - # provide the new patch to be used instead. - ret = False - elif r == 3: # Skip - ret = skipfile = False - elif r == 4: # file (Record remaining) - ret = skipfile = True - elif r == 5: # done, skip remaining - ret = skipall = False - elif r == 6: # all - ret = skipall = True - elif r == 7: # quit - raise util.Abort(_('user quit')) - return ret, skipfile, skipall, newpatches - - seen = set() - applied = {} # 'filename' -> [] of chunks - skipfile, skipall = None, None - pos, total = 1, sum(len(h.hunks) for h in headers) - for h in headers: - pos += len(h.hunks) - skipfile = None - fixoffset = 0 - hdr = ''.join(h.header) - if hdr in seen: - continue - seen.add(hdr) - if skipall is None: - h.pretty(ui) - msg = (_('examine changes to %s?') % - _(' and ').join("'%s'" % f for f in h.files())) - r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None) - if not r: - continue - applied[h.filename()] = [h] - if h.allhunks(): - applied[h.filename()] += h.hunks - continue - for i, chunk in enumerate(h.hunks): - if skipfile is None and skipall is None: - chunk.pretty(ui) - if total == 1: - msg = _("record this change to '%s'?") % chunk.filename() - else: - idx = pos - len(h.hunks) + i - msg = _("record change %d/%d to '%s'?") % (idx, total, - chunk.filename()) - r, skipfile, skipall, newpatches = prompt(skipfile, - skipall, msg, chunk) - if r: - if fixoffset: - chunk = copy.copy(chunk) - chunk.toline += fixoffset - applied[chunk.filename()].append(chunk) - elif newpatches is not None: - for newpatch in newpatches: - for newhunk in newpatch.hunks: - if fixoffset: - newhunk.toline += fixoffset - applied[newhunk.filename()].append(newhunk) - else: - fixoffset += chunk.removed - chunk.added - return sum([h for h in applied.itervalues() - if h[0].special() or len(h) > 1], []) @command("record", # same options as commit + white space diff options - commands.table['^commit|ci'][1][:] + commands.diffwsopts, + [c for c in commands.table['^commit|ci'][1][:] + if c[1] != "interactive"] + commands.diffwsopts, _('hg record [OPTION]... [FILE]...')) def record(ui, repo, *pats, **opts): '''interactively select changes to commit @@ -440,7 +49,8 @@ This command is not available when committing a merge.''' - dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts) + opts["interactive"] = True + commands.commit(ui, repo, *pats, **opts) def qrefresh(origfn, ui, repo, *pats, **opts): if not opts['interactive']: @@ -456,7 +66,8 @@ mq.refresh(ui, repo, **opts) # backup all changed files - dorecord(ui, repo, committomq, 'qrefresh', True, *pats, **opts) + cmdutil.dorecord(ui, repo, committomq, 'qrefresh', True, + cmdutil.recordfilter, *pats, **opts) # This command registration is replaced during uisetup(). @command('qrecord', @@ -481,162 +92,14 @@ opts['checkname'] = False mq.new(ui, repo, patch, *pats, **opts) - dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts) + cmdutil.dorecord(ui, repo, committomq, 'qnew', False, + cmdutil.recordfilter, *pats, **opts) def qnew(origfn, ui, repo, patch, *args, **opts): if opts['interactive']: return qrecord(ui, repo, patch, *args, **opts) return origfn(ui, repo, patch, *args, **opts) -def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts): - if not ui.interactive(): - raise util.Abort(_('running non-interactively, use %s instead') % - cmdsuggest) - - # make sure username is set before going interactive - if not opts.get('user'): - ui.username() # raise exception, username not provided - - def recordfunc(ui, repo, message, match, opts): - """This is generic record driver. - - Its job is to interactively filter local changes, and - accordingly prepare working directory into a state in which the - job can be delegated to a non-interactive commit command such as - 'commit' or 'qrefresh'. - - After the actual job is done by non-interactive command, the - working directory is restored to its original state. - - In the end we'll record interesting changes, and everything else - will be left in place, so the user can continue working. - """ - - cmdutil.checkunfinished(repo, commit=True) - merge = len(repo[None].parents()) > 1 - if merge: - raise util.Abort(_('cannot partially commit a merge ' - '(use "hg commit" instead)')) - - status = repo.status(match=match) - diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True) - diffopts.nodates = True - diffopts.git = True - chunks = patch.diff(repo, changes=status, opts=diffopts) - fp = cStringIO.StringIO() - fp.write(''.join(chunks)) - fp.seek(0) - - # 1. filter patch, so we have intending-to apply subset of it - try: - chunks = filterpatch(ui, parsepatch(fp)) - except patch.PatchError, err: - raise util.Abort(_('error parsing patch: %s') % err) - - del fp - - contenders = set() - for h in chunks: - try: - contenders.update(set(h.files())) - except AttributeError: - pass - - changed = status.modified + status.added + status.removed - newfiles = [f for f in changed if f in contenders] - if not newfiles: - ui.status(_('no changes to record\n')) - return 0 - - modified = set(status.modified) - - # 2. backup changed files, so we can restore them in the end - if backupall: - tobackup = changed - else: - tobackup = [f for f in newfiles if f in modified] - - backups = {} - if tobackup: - backupdir = repo.join('record-backups') - try: - os.mkdir(backupdir) - except OSError, err: - if err.errno != errno.EEXIST: - raise - try: - # backup continues - for f in tobackup: - fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', - dir=backupdir) - os.close(fd) - ui.debug('backup %r as %r\n' % (f, tmpname)) - util.copyfile(repo.wjoin(f), tmpname) - shutil.copystat(repo.wjoin(f), tmpname) - backups[f] = tmpname - - fp = cStringIO.StringIO() - for c in chunks: - if c.filename() in backups: - c.write(fp) - dopatch = fp.tell() - fp.seek(0) - - # 3a. apply filtered patch to clean repo (clean) - if backups: - hg.revert(repo, repo.dirstate.p1(), - lambda key: key in backups) - - # 3b. (apply) - if dopatch: - try: - ui.debug('applying patch\n') - ui.debug(fp.getvalue()) - patch.internalpatch(ui, repo, fp, 1, eolmode=None) - except patch.PatchError, err: - raise util.Abort(str(err)) - del fp - - # 4. We prepared working directory according to filtered - # patch. Now is the time to delegate the job to - # commit/qrefresh or the like! - - # Make all of the pathnames absolute. - newfiles = [repo.wjoin(nf) for nf in newfiles] - commitfunc(ui, repo, *newfiles, **opts) - - return 0 - finally: - # 5. finally restore backed-up files - try: - for realname, tmpname in backups.iteritems(): - ui.debug('restoring %r to %r\n' % (tmpname, realname)) - util.copyfile(tmpname, repo.wjoin(realname)) - # Our calls to copystat() here and above are a - # hack to trick any editors that have f open that - # we haven't modified them. - # - # Also note that this racy as an editor could - # notice the file's mtime before we've finished - # writing it. - shutil.copystat(tmpname, repo.wjoin(realname)) - os.unlink(tmpname) - if tobackup: - os.rmdir(backupdir) - except OSError: - pass - - # wrap ui.write so diff output can be labeled/colorized - def wrapwrite(orig, *args, **kw): - label = kw.pop('label', '') - for chunk, l in patch.difflabel(lambda: args): - orig(chunk, label=label + l) - oldwrite = ui.write - extensions.wrapfunction(ui, 'write', wrapwrite) - try: - return cmdutil.commit(ui, repo, recordfunc, pats, opts) - finally: - ui.write = oldwrite def uisetup(ui): try: diff -r 612ed41ae359 -r e89f909edffa hgext/share.py --- a/hgext/share.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/share.py Thu Apr 16 20:57:51 2015 -0500 @@ -15,7 +15,7 @@ testedwith = 'internal' @command('share', - [('U', 'noupdate', None, _('do not create a working copy')), + [('U', 'noupdate', None, _('do not create a working directory')), ('B', 'bookmarks', None, _('also share bookmarks'))], _('[-U] [-B] SOURCE [DEST]'), norepo=True) diff -r 612ed41ae359 -r e89f909edffa hgext/shelve.py --- a/hgext/shelve.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/shelve.py Thu Apr 16 20:57:51 2015 -0500 @@ -226,9 +226,17 @@ raise util.Abort(_('shelved change names may not contain slashes')) if name.startswith('.'): raise util.Abort(_("shelved change names may not start with '.'")) + interactive = opts.get('interactive', False) - node = cmdutil.commit(ui, repo, commitfunc, pats, opts) - + def interactivecommitfunc(ui, repo, *pats, **opts): + match = scmutil.match(repo['.'], pats, {}) + message = opts['message'] + return commitfunc(ui, repo, message, match, opts) + if not interactive: + node = cmdutil.commit(ui, repo, commitfunc, pats, opts) + else: + node = cmdutil.dorecord(ui, repo, interactivecommitfunc, 'commit', + False, cmdutil.recordfilter, *pats, **opts) if not node: stat = repo.status(match=scmutil.match(repo[None], pats, opts)) if stat.deleted: @@ -536,8 +544,8 @@ oldquiet = ui.quiet wlock = lock = tr = None try: + wlock = repo.wlock() lock = repo.lock() - wlock = repo.wlock() tr = repo.transaction('unshelve', report=lambda x: None) oldtiprev = len(repo) @@ -649,6 +657,8 @@ _('use the given name for the shelved commit'), _('NAME')), ('p', 'patch', None, _('show patch')), + ('i', 'interactive', None, + _('interactive mode, only works while creating a shelve')), ('', 'stat', None, _('output diffstat-style summary of changes'))] + commands.walkopts, _('hg shelve [OPTION]... [FILE]...')) diff -r 612ed41ae359 -r e89f909edffa hgext/strip.py --- a/hgext/strip.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/strip.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,7 +7,7 @@ from mercurial.node import nullid from mercurial.lock import release from mercurial import cmdutil, hg, scmutil, util -from mercurial import repair, bookmarks +from mercurial import repair, bookmarks, merge cmdtable = {} command = cmdutil.command(cmdtable) @@ -23,10 +23,8 @@ else: bctx = wctx.parents()[0] for s in sorted(wctx.substate): - if wctx.sub(s).dirty(True): - raise util.Abort( - _("uncommitted changes in subrepository %s") % s) - elif s not in bctx.substate or bctx.sub(s).dirty(): + wctx.sub(s).bailifchanged(True) + if s not in bctx.substate or bctx.sub(s).dirty(): inclsubs.append(s) return inclsubs @@ -81,7 +79,8 @@ ('', 'no-backup', None, _('no backups')), ('', 'nobackup', None, _('no backups (DEPRECATED)')), ('n', '', None, _('ignored (DEPRECATED)')), - ('k', 'keep', None, _("do not modify working copy during strip")), + ('k', 'keep', None, _("do not modify working directory during " + "strip")), ('B', 'bookmark', '', _("remove revs only reachable from given" " bookmark"))], _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...')) @@ -206,6 +205,11 @@ repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles) repo.dirstate.write() + + # clear resolve state + ms = merge.mergestate(repo) + ms.reset(repo['.'].node()) + update = False diff -r 612ed41ae359 -r e89f909edffa hgext/transplant.py --- a/hgext/transplant.py Thu Apr 16 22:33:53 2015 +0900 +++ b/hgext/transplant.py Thu Apr 16 20:57:51 2015 -0500 @@ -342,9 +342,8 @@ try: p1, p2 = repo.dirstate.parents() if p1 != parent: - raise util.Abort( - _('working dir not at transplant parent %s') % - revlog.hex(parent)) + raise util.Abort(_('working directory not at transplant ' + 'parent %s') % revlog.hex(parent)) if merge: repo.setparents(p1, parents[1]) modified, added, removed, deleted = repo.status()[:4] diff -r 612ed41ae359 -r e89f909edffa i18n/ja.po --- a/i18n/ja.po Thu Apr 16 22:33:53 2015 +0900 +++ b/i18n/ja.po Thu Apr 16 20:57:51 2015 -0500 @@ -27151,8 +27151,8 @@ msgid "couldn't read journal entry %r!\n" msgstr "ジャーナルファイル中のエントリ %r の解析に失敗\n" -msgid "journal was created by a different version of Mercurial" -msgstr "ジャーナルファイルは異なる版の mercurial で作成されたものです" +msgid "journal was created by a different version of Mercurial\n" +msgstr "ジャーナルファイルは異なる版の mercurial で作成されたものです\n" msgid "already have changeset " msgstr "既にあるリビジョンです " diff -r 612ed41ae359 -r e89f909edffa i18n/polib.py --- a/i18n/polib.py Thu Apr 16 22:33:53 2015 +0900 +++ b/i18n/polib.py Thu Apr 16 20:57:51 2015 -0500 @@ -437,8 +437,15 @@ # the keys are sorted in the .mo file def cmp(_self, other): # msgfmt compares entries with msgctxt if it exists - self_msgid = _self.msgctxt and _self.msgctxt or _self.msgid - other_msgid = other.msgctxt and other.msgctxt or other.msgid + if _self.msgctxt: + self_msgid = _self.msgctxt + else: + self_msgid = _self.msgid + + if other.msgctxt: + other_msgid = other.msgctxt + else: + other_msgid = other.msgid if self_msgid > other_msgid: return 1 elif self_msgid < other_msgid: diff -r 612ed41ae359 -r e89f909edffa mercurial/archival.py --- a/mercurial/archival.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/archival.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,7 +6,6 @@ # GNU General Public License version 2 or any later version. from i18n import _ -from node import hex import match as matchmod import cmdutil import scmutil, util, encoding @@ -55,6 +54,33 @@ return kind return None +def _rootctx(repo): + # repo[0] may be hidden + for rev in repo: + return repo[rev] + return repo['null'] + +def buildmetadata(ctx): + '''build content of .hg_archival.txt''' + repo = ctx.repo() + base = 'repo: %s\nnode: %s\nbranch: %s\n' % ( + _rootctx(repo).hex(), ctx.hex(), encoding.fromlocal(ctx.branch())) + + tags = ''.join('tag: %s\n' % t for t in ctx.tags() + if repo.tagtype(t) == 'global') + if not tags: + repo.ui.pushbuffer() + opts = {'template': '{latesttag}\n{latesttagdistance}', + 'style': '', 'patch': None, 'git': None} + cmdutil.show_changeset(repo.ui, repo, opts).show(ctx) + ltags, dist = repo.ui.popbuffer().split('\n') + ltags = ltags.split(':') + changessince = len(repo.revs('only(.,%s)', ltags[0])) + tags = ''.join('latesttag: %s\n' % t for t in ltags) + tags += 'latesttagdistance: %s\n' % dist + tags += 'changessincelatesttag: %s\n' % changessince + + return base + tags class tarit(object): '''write archive to tar file or stream. can write uncompressed, @@ -230,7 +256,7 @@ } def archive(repo, dest, node, kind, decode=True, matchfn=None, - prefix=None, mtime=None, subrepos=False): + prefix='', mtime=None, subrepos=False): '''create archive of repo as it was at node. dest can be name of directory, name of archive file, or file @@ -264,29 +290,9 @@ archiver = archivers[kind](dest, mtime or ctx.date()[0]) if repo.ui.configbool("ui", "archivemeta", True): - def metadata(): - base = 'repo: %s\nnode: %s\nbranch: %s\n' % ( - repo[0].hex(), hex(node), encoding.fromlocal(ctx.branch())) - - tags = ''.join('tag: %s\n' % t for t in ctx.tags() - if repo.tagtype(t) == 'global') - if not tags: - repo.ui.pushbuffer() - opts = {'template': '{latesttag}\n{latesttagdistance}', - 'style': '', 'patch': None, 'git': None} - cmdutil.show_changeset(repo.ui, repo, opts).show(ctx) - ltags, dist = repo.ui.popbuffer().split('\n') - ltags = ltags.split(':') - changessince = len(repo.revs('only(.,%s)', ltags[0])) - tags = ''.join('latesttag: %s\n' % t for t in ltags) - tags += 'latesttagdistance: %s\n' % dist - tags += 'changessincelatesttag: %s\n' % changessince - - return base + tags - name = '.hg_archival.txt' if not matchfn or matchfn(name): - write(name, 0644, False, metadata) + write(name, 0644, False, lambda: buildmetadata(ctx)) if matchfn: files = [f for f in ctx.manifest().keys() if matchfn(f)] diff -r 612ed41ae359 -r e89f909edffa mercurial/bookmarks.py --- a/mercurial/bookmarks.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/bookmarks.py Thu Apr 16 20:57:51 2015 -0500 @@ -362,14 +362,17 @@ return results -def _diverge(ui, b, path, localmarks): +def _diverge(ui, b, path, localmarks, remotenode): + '''Return appropriate diverged bookmark for specified ``path`` + + This returns None, if it is failed to assign any divergent + bookmark name. + + This reuses already existing one with "@number" suffix, if it + refers ``remotenode``. + ''' if b == '@': b = '' - # find a unique @ suffix - for x in range(1, 100): - n = '%s@%d' % (b, x) - if n not in localmarks: - break # try to use an @pathalias suffix # if an @pathalias already exists, we overwrite (update) it if path.startswith("file:"): @@ -378,8 +381,15 @@ if u.startswith("file:"): u = util.url(u).path if path == u: - n = '%s@%s' % (b, p) - return n + return '%s@%s' % (b, p) + + # assign a unique "@number" suffix newly + for x in range(1, 100): + n = '%s@%d' % (b, x) + if n not in localmarks or localmarks[n] == remotenode: + return n + + return None def updatefromremote(ui, repo, remotemarks, path, trfunc, explicit=()): ui.debug("checking for updated bookmarks\n") @@ -410,10 +420,15 @@ changed.append((b, bin(scid), status, _("importing bookmark %s\n") % (b))) else: - db = _diverge(ui, b, path, localmarks) - changed.append((db, bin(scid), warn, - _("divergent bookmark %s stored as %s\n") - % (b, db))) + snode = bin(scid) + db = _diverge(ui, b, path, localmarks, snode) + if db: + changed.append((db, snode, warn, + _("divergent bookmark %s stored as %s\n") % + (b, db))) + else: + warn(_("warning: failed to assign numbered name " + "to divergent bookmark %s\n") % (b)) for b, scid, dcid in adddst + advdst: if b in explicit: explicit.discard(b) @@ -427,22 +442,94 @@ writer(msg) localmarks.recordchange(tr) -def diff(ui, dst, src): +def incoming(ui, repo, other): + '''Show bookmarks incoming from other to repo + ''' + ui.status(_("searching for changed bookmarks\n")) + + r = compare(repo, other.listkeys('bookmarks'), repo._bookmarks, + dsthex=hex) + addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r + + incomings = [] + if ui.debugflag: + getid = lambda id: id + else: + getid = lambda id: id[:12] + if ui.verbose: + def add(b, id, st): + incomings.append(" %-25s %s %s\n" % (b, getid(id), st)) + else: + def add(b, id, st): + incomings.append(" %-25s %s\n" % (b, getid(id))) + for b, scid, dcid in addsrc: + add(b, scid, _('added')) + for b, scid, dcid in advsrc: + add(b, scid, _('advanced')) + for b, scid, dcid in diverge: + add(b, scid, _('diverged')) + for b, scid, dcid in differ: + add(b, scid, _('changed')) + + if not incomings: + ui.status(_("no changed bookmarks found\n")) + return 1 + + for s in sorted(incomings): + ui.write(s) + + return 0 + +def outgoing(ui, repo, other): + '''Show bookmarks outgoing from repo to other + ''' ui.status(_("searching for changed bookmarks\n")) - smarks = src.listkeys('bookmarks') - dmarks = dst.listkeys('bookmarks') + r = compare(repo, repo._bookmarks, other.listkeys('bookmarks'), + srchex=hex) + addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r - diff = sorted(set(smarks) - set(dmarks)) - for k in diff: - mark = ui.debugflag and smarks[k] or smarks[k][:12] - ui.write(" %-25s %s\n" % (k, mark)) + outgoings = [] + if ui.debugflag: + getid = lambda id: id + else: + getid = lambda id: id[:12] + if ui.verbose: + def add(b, id, st): + outgoings.append(" %-25s %s %s\n" % (b, getid(id), st)) + else: + def add(b, id, st): + outgoings.append(" %-25s %s\n" % (b, getid(id))) + for b, scid, dcid in addsrc: + add(b, scid, _('added')) + for b, scid, dcid in adddst: + add(b, ' ' * 40, _('deleted')) + for b, scid, dcid in advsrc: + add(b, scid, _('advanced')) + for b, scid, dcid in diverge: + add(b, scid, _('diverged')) + for b, scid, dcid in differ: + add(b, scid, _('changed')) - if len(diff) <= 0: + if not outgoings: ui.status(_("no changed bookmarks found\n")) return 1 + + for s in sorted(outgoings): + ui.write(s) + return 0 +def summary(repo, other): + '''Compare bookmarks between repo and other for "hg summary" output + + This returns "(# of incoming, # of outgoing)" tuple. + ''' + r = compare(repo, other.listkeys('bookmarks'), repo._bookmarks, + dsthex=hex) + addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = r + return (len(addsrc), len(adddst)) + def validdest(repo, old, new): """Is the new bookmark destination a valid update from the old one""" repo = repo.unfiltered() @@ -456,5 +543,5 @@ elif repo.obsstore: return new.node() in obsolete.foreground(repo, [old.node()]) else: - # still an independent clause as it is lazyer (and therefore faster) + # still an independent clause as it is lazier (and therefore faster) return old.descendant(new) diff -r 612ed41ae359 -r e89f909edffa mercurial/branchmap.py --- a/mercurial/branchmap.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/branchmap.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,6 +7,7 @@ from node import bin, hex, nullid, nullrev import encoding +import scmutil import util import time from array import array @@ -96,6 +97,7 @@ if revs: partial.update(repo, revs) partial.write(repo) + assert partial.validfor(repo), filtername repo._branchcaches[repo.filtername] = partial @@ -134,28 +136,6 @@ self._closednodes = set() else: self._closednodes = closednodes - self._revbranchcache = None - - def _hashfiltered(self, repo): - """build hash of revision filtered in the current cache - - Tracking tipnode and tiprev is not enough to ensure validity of the - cache as they do not help to distinct cache that ignored various - revision bellow tiprev. - - To detect such difference, we build a cache of all ignored revisions. - """ - cl = repo.changelog - if not cl.filteredrevs: - return None - key = None - revs = sorted(r for r in cl.filteredrevs if r <= self.tiprev) - if revs: - s = util.sha1() - for rev in revs: - s.update('%s;' % rev) - key = s.digest() - return key def validfor(self, repo): """Is the cache content valid regarding a repo @@ -164,7 +144,8 @@ - True when cache is up to date or a subset of current repo.""" try: return ((self.tipnode == repo.changelog.node(self.tiprev)) - and (self.filteredhash == self._hashfiltered(repo))) + and (self.filteredhash == \ + scmutil.filteredhash(repo, self.tiprev))) except IndexError: return False @@ -226,9 +207,6 @@ repo.ui.debug("couldn't write branch cache: %s\n" % inst) # Abort may be raise by read only opener pass - if self._revbranchcache: - self._revbranchcache.write(repo.unfiltered()) - self._revbranchcache = None def update(self, repo, revgen): """Given a branchhead cache, self, that may have extra nodes or be @@ -239,12 +217,9 @@ cl = repo.changelog # collect new branch entries newbranches = {} - urepo = repo.unfiltered() - self._revbranchcache = revbranchcache(urepo) - getbranchinfo = self._revbranchcache.branchinfo - ucl = urepo.changelog + getbranchinfo = repo.revbranchcache().branchinfo for r in revgen: - branch, closesbranch = getbranchinfo(ucl, r) + branch, closesbranch = getbranchinfo(r) newbranches.setdefault(branch, []).append(r) if closesbranch: self._closednodes.add(cl.node(r)) @@ -289,7 +264,7 @@ if tiprev > self.tiprev: self.tipnode = cl.node(tiprev) self.tiprev = tiprev - self.filteredhash = self._hashfiltered(repo) + self.filteredhash = scmutil.filteredhash(repo, self.tiprev) duration = time.time() - starttime repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n', @@ -332,6 +307,7 @@ def __init__(self, repo, readonly=True): assert repo.filtername is None + self._repo = repo self._names = [] # branch names in local encoding with static index self._rbcrevs = array('c') # structs of type _rbcrecfmt self._rbcsnameslen = 0 @@ -340,8 +316,6 @@ self._rbcsnameslen = len(bndata) # for verification before writing self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')] except (IOError, OSError), inst: - repo.ui.debug("couldn't read revision branch cache names: %s\n" % - inst) if readonly: # don't try to use cache - fall back to the slow path self.branchinfo = self._branchinfo @@ -361,18 +335,16 @@ self._rbcnamescount = len(self._names) # number of good names on disk self._namesreverse = dict((b, r) for r, b in enumerate(self._names)) - def branchinfo(self, changelog, rev): + def branchinfo(self, rev): """Return branch name and close flag for rev, using and updating persistent cache.""" + changelog = self._repo.changelog rbcrevidx = rev * _rbcrecsize # if requested rev is missing, add and populate all missing revs if len(self._rbcrevs) < rbcrevidx + _rbcrecsize: - first = len(self._rbcrevs) // _rbcrecsize self._rbcrevs.extend('\0' * (len(changelog) * _rbcrecsize - len(self._rbcrevs))) - for r in xrange(first, len(changelog)): - self._branchinfo(changelog, r) # fast path: extract data from cache, use it if node is matching reponode = changelog.node(rev)[:_rbcnodelen] @@ -381,14 +353,22 @@ close = bool(branchidx & _rbccloseflag) if close: branchidx &= _rbcbranchidxmask - if cachenode == reponode: + if cachenode == '\0\0\0\0': + pass + elif cachenode == reponode: return self._names[branchidx], close + else: + # rev/node map has changed, invalidate the cache from here up + truncate = rbcrevidx + _rbcrecsize + del self._rbcrevs[truncate:] + self._rbcrevslen = min(self._rbcrevslen, truncate) + # fall back to slow path and make sure it will be written to disk - self._rbcrevslen = min(self._rbcrevslen, rev) - return self._branchinfo(changelog, rev) + return self._branchinfo(rev) - def _branchinfo(self, changelog, rev): + def _branchinfo(self, rev): """Retrieve branch info from changelog and update _rbcrevs""" + changelog = self._repo.changelog b, close = changelog.branchinfo(rev) if b in self._namesreverse: branchidx = self._namesreverse[b] @@ -399,21 +379,28 @@ reponode = changelog.node(rev) if close: branchidx |= _rbccloseflag + self._setcachedata(rev, reponode, branchidx) + return b, close + + def _setcachedata(self, rev, node, branchidx): + """Writes the node's branch data to the in-memory cache data.""" rbcrevidx = rev * _rbcrecsize rec = array('c') - rec.fromstring(pack(_rbcrecfmt, reponode, branchidx)) + rec.fromstring(pack(_rbcrecfmt, node, branchidx)) self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec - return b, close + self._rbcrevslen = min(self._rbcrevslen, rev) - def write(self, repo): + tr = self._repo.currenttransaction() + if tr: + tr.addfinalize('write-revbranchcache', self.write) + + def write(self, tr=None): """Save branch cache if it is dirty.""" + repo = self._repo if self._rbcnamescount < len(self._names): try: if self._rbcnamescount != 0: f = repo.vfs.open(_rbcnames, 'ab') - # The position after open(x, 'a') is implementation defined- - # see issue3543. SEEK_END was added in 2.5 - f.seek(0, 2) #os.SEEK_END if f.tell() == self._rbcsnameslen: f.write('\0') else: @@ -438,9 +425,6 @@ revs = min(len(repo.changelog), len(self._rbcrevs) // _rbcrecsize) try: f = repo.vfs.open(_rbcrevs, 'ab') - # The position after open(x, 'a') is implementation defined- - # see issue3543. SEEK_END was added in 2.5 - f.seek(0, 2) #os.SEEK_END if f.tell() != start: repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start)) f.seek(start) diff -r 612ed41ae359 -r e89f909edffa mercurial/bundle2.py --- a/mercurial/bundle2.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/bundle2.py Thu Apr 16 20:57:51 2015 -0500 @@ -145,6 +145,7 @@ preserve. """ +import errno import sys import util import struct @@ -161,8 +162,6 @@ _pack = struct.pack _unpack = struct.unpack -_magicstring = 'HG2Y' - _fstreamparamsize = '>i' _fpartheadersize = '>i' _fparttypesize = '>B' @@ -312,13 +311,17 @@ except Exception, exc: for part in iterparts: # consume the bundle content - part.read() + part.seek(0, 2) # Small hack to let caller code distinguish exceptions from bundle2 # processing from processing the old format. This is mostly # needed to handle different return codes to unbundle according to the # type of bundle. We should probably clean up or drop this return code # craziness in a future version. exc.duringunbundle2 = True + salvaged = [] + if op.reply is not None: + salvaged = op.reply.salvageoutput() + exc._bundle2salvagedoutput = salvaged raise return op @@ -358,13 +361,13 @@ finally: if output is not None: output = op.ui.popbuffer() - if output: - outpart = op.reply.newpart('b2x:output', data=output, - mandatory=False) - outpart.addparam('in-reply-to', str(part.id), mandatory=False) + if output: + outpart = op.reply.newpart('output', data=output, + mandatory=False) + outpart.addparam('in-reply-to', str(part.id), mandatory=False) finally: # consume the part content to not corrupt the stream. - part.read() + part.seek(0, 2) def decodecaps(blob): @@ -409,6 +412,8 @@ populate it. Then call `getchunks` to retrieve all the binary chunks of data that compose the bundle2 container.""" + _magicstring = 'HG20' + def __init__(self, ui, capabilities=()): self.ui = ui self._params = [] @@ -452,8 +457,8 @@ # methods used to generate the bundle2 stream def getchunks(self): - self.ui.debug('start emission of %s stream\n' % _magicstring) - yield _magicstring + self.ui.debug('start emission of %s stream\n' % self._magicstring) + yield self._magicstring param = self._paramchunk() self.ui.debug('bundle parameter: %s\n' % param) yield _pack(_fstreamparamsize, len(param)) @@ -479,11 +484,25 @@ blocks.append(par) return ' '.join(blocks) + def salvageoutput(self): + """return a list with a copy of all output parts in the bundle + + This is meant to be used during error handling to make sure we preserve + server output""" + salvaged = [] + for part in self._parts: + if part.type.startswith('output'): + salvaged.append(part.copy()) + return salvaged + + class unpackermixin(object): """A mixin to extract bytes and struct data from a stream""" def __init__(self, fp): self._fp = fp + self._seekable = (util.safehasattr(fp, 'seek') and + util.safehasattr(fp, 'tell')) def _unpack(self, format): """unpack this struct format from the stream""" @@ -494,6 +513,43 @@ """read exactly bytes from the stream""" return changegroup.readexactly(self._fp, size) + def seek(self, offset, whence=0): + """move the underlying file pointer""" + if self._seekable: + return self._fp.seek(offset, whence) + else: + raise NotImplementedError(_('File pointer is not seekable')) + + def tell(self): + """return the file offset, or None if file is not seekable""" + if self._seekable: + try: + return self._fp.tell() + except IOError, e: + if e.errno == errno.ESPIPE: + self._seekable = False + else: + raise + return None + + def close(self): + """close underlying file""" + if util.safehasattr(self._fp, 'close'): + return self._fp.close() + +def getunbundler(ui, fp, header=None): + """return a valid unbundler object for a given header""" + if header is None: + header = changegroup.readexactly(fp, 4) + magic, version = header[0:2], header[2:4] + if magic != 'HG': + raise util.Abort(_('not a Mercurial bundle')) + unbundlerclass = formatmap.get(version) + if unbundlerclass is None: + raise util.Abort(_('unknown bundle version %s') % version) + unbundler = unbundlerclass(ui, fp) + ui.debug('start processing of %s stream\n' % header) + return unbundler class unbundle20(unpackermixin): """interpret a bundle2 stream @@ -501,18 +557,10 @@ This class is fed with a binary stream and yields parts through its `iterparts` methods.""" - def __init__(self, ui, fp, header=None): + def __init__(self, ui, fp): """If header is specified, we do not read it out of the stream.""" self.ui = ui super(unbundle20, self).__init__(fp) - if header is None: - header = self._readexact(4) - magic, version = header[0:2], header[2:4] - if magic != 'HG': - raise util.Abort(_('not a Mercurial bundle')) - if version != '2Y': - raise util.Abort(_('unknown bundle version %s') % version) - self.ui.debug('start processing of %s stream\n' % header) @util.propertycache def params(self): @@ -564,6 +612,7 @@ while headerblock is not None: part = unbundlepart(self.ui, headerblock, self._fp) yield part + part.seek(0, 2) headerblock = self._readpartheader() self.ui.debug('end of bundle2 stream\n') @@ -580,6 +629,10 @@ return self._readexact(headersize) return None + def compressed(self): + return False + +formatmap = {'20': unbundle20} class bundlepart(object): """A bundle2 part contains application level payload @@ -618,6 +671,15 @@ self._generated = None self.mandatory = mandatory + def copy(self): + """return a copy of the part + + The new part have the very same content but no partid assigned yet. + Parts with generated data cannot be copied.""" + assert not util.safehasattr(self.data, 'next') + return self.__class__(self.type, self._mandatoryparams, + self._advisoryparams, self._data, self.mandatory) + # methods used to defines the part content def __setdata(self, data): if self._generated is not None: @@ -697,7 +759,7 @@ # backup exception data for later exc_info = sys.exc_info() msg = 'unexpected error: %s' % exc - interpart = bundlepart('b2x:error:abort', [('message', msg)], + interpart = bundlepart('error:abort', [('message', msg)], mandatory=False) interpart.id = 0 yield _pack(_fpayloadsize, -1) @@ -801,6 +863,8 @@ self._payloadstream = None self._readheader() self._mandatory = None + self._chunkindex = [] #(payload, file) position tuples for chunk starts + self._pos = 0 def _fromheader(self, size): """return the next byte from the header""" @@ -826,6 +890,47 @@ self.params.update(dict(self.advisoryparams)) self.mandatorykeys = frozenset(p[0] for p in mandatoryparams) + def _payloadchunks(self, chunknum=0): + '''seek to specified chunk and start yielding data''' + if len(self._chunkindex) == 0: + assert chunknum == 0, 'Must start with chunk 0' + self._chunkindex.append((0, super(unbundlepart, self).tell())) + else: + assert chunknum < len(self._chunkindex), \ + 'Unknown chunk %d' % chunknum + super(unbundlepart, self).seek(self._chunkindex[chunknum][1]) + + pos = self._chunkindex[chunknum][0] + payloadsize = self._unpack(_fpayloadsize)[0] + self.ui.debug('payload chunk size: %i\n' % payloadsize) + while payloadsize: + if payloadsize == flaginterrupt: + # interruption detection, the handler will now read a + # single part and process it. + interrupthandler(self.ui, self._fp)() + elif payloadsize < 0: + msg = 'negative payload chunk size: %i' % payloadsize + raise error.BundleValueError(msg) + else: + result = self._readexact(payloadsize) + chunknum += 1 + pos += payloadsize + if chunknum == len(self._chunkindex): + self._chunkindex.append((pos, + super(unbundlepart, self).tell())) + yield result + payloadsize = self._unpack(_fpayloadsize)[0] + self.ui.debug('payload chunk size: %i\n' % payloadsize) + + def _findchunk(self, pos): + '''for a given payload position, return a chunk number and offset''' + for chunk, (ppos, fpos) in enumerate(self._chunkindex): + if ppos == pos: + return chunk, 0 + elif ppos > pos: + return chunk - 1, pos - self._chunkindex[chunk - 1][0] + raise ValueError('Unknown chunk') + def _readheader(self): """read the header and setup the object""" typesize = self._unpackheader(_fparttypesize)[0] @@ -857,22 +962,7 @@ advparams.append((self._fromheader(key), self._fromheader(value))) self._initparams(manparams, advparams) ## part payload - def payloadchunks(): - payloadsize = self._unpack(_fpayloadsize)[0] - self.ui.debug('payload chunk size: %i\n' % payloadsize) - while payloadsize: - if payloadsize == flaginterrupt: - # interruption detection, the handler will now read a - # single part and process it. - interrupthandler(self.ui, self._fp)() - elif payloadsize < 0: - msg = 'negative payload chunk size: %i' % payloadsize - raise error.BundleValueError(msg) - else: - yield self._readexact(payloadsize) - payloadsize = self._unpack(_fpayloadsize)[0] - self.ui.debug('payload chunk size: %i\n' % payloadsize) - self._payloadstream = util.chunkbuffer(payloadchunks()) + self._payloadstream = util.chunkbuffer(self._payloadchunks()) # we read the data, tell it self._initialized = True @@ -886,13 +976,42 @@ data = self._payloadstream.read(size) if size is None or len(data) < size: self.consumed = True + self._pos += len(data) return data -capabilities = {'HG2Y': (), - 'b2x:listkeys': (), - 'b2x:pushkey': (), + def tell(self): + return self._pos + + def seek(self, offset, whence=0): + if whence == 0: + newpos = offset + elif whence == 1: + newpos = self._pos + offset + elif whence == 2: + if not self.consumed: + self.read() + newpos = self._chunkindex[-1][0] - offset + else: + raise ValueError('Unknown whence value: %r' % (whence,)) + + if newpos > self._chunkindex[-1][0] and not self.consumed: + self.read() + if not 0 <= newpos <= self._chunkindex[-1][0]: + raise ValueError('Offset out of range') + + if self._pos != newpos: + chunk, internaloffset = self._findchunk(newpos) + self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk)) + adjust = self.read(internaloffset) + if len(adjust) != internaloffset: + raise util.Abort(_('Seek failed\n')) + self._pos = newpos + +capabilities = {'HG20': (), + 'listkeys': (), + 'pushkey': (), 'digests': tuple(sorted(util.DIGESTS.keys())), - 'b2x:remote-changegroup': ('http', 'https'), + 'remote-changegroup': ('http', 'https'), } def getrepocaps(repo, allowpushback=False): @@ -901,29 +1020,29 @@ Exists to allow extensions (like evolution) to mutate the capabilities. """ caps = capabilities.copy() - caps['b2x:changegroup'] = tuple(sorted(changegroup.packermap.keys())) + caps['changegroup'] = tuple(sorted(changegroup.packermap.keys())) if obsolete.isenabled(repo, obsolete.exchangeopt): supportedformat = tuple('V%i' % v for v in obsolete.formats) - caps['b2x:obsmarkers'] = supportedformat + caps['obsmarkers'] = supportedformat if allowpushback: - caps['b2x:pushback'] = () + caps['pushback'] = () return caps def bundle2caps(remote): """return the bundle capabilities of a peer as dict""" - raw = remote.capable('bundle2-exp') + raw = remote.capable('bundle2') if not raw and raw != '': return {} - capsblob = urllib.unquote(remote.capable('bundle2-exp')) + capsblob = urllib.unquote(remote.capable('bundle2')) return decodecaps(capsblob) def obsmarkersversion(caps): """extract the list of supported obsmarkers versions from a bundle2caps dict """ - obscaps = caps.get('b2x:obsmarkers', ()) + obscaps = caps.get('obsmarkers', ()) return [int(c[1:]) for c in obscaps if c.startswith('V')] -@parthandler('b2x:changegroup', ('version',)) +@parthandler('changegroup', ('version',)) def handlechangegroup(op, inpart): """apply a changegroup part on the repo @@ -947,14 +1066,14 @@ if op.reply is not None: # This is definitely not the final form of this # return. But one need to start somewhere. - part = op.reply.newpart('b2x:reply:changegroup', mandatory=False) + part = op.reply.newpart('reply:changegroup', mandatory=False) part.addparam('in-reply-to', str(inpart.id), mandatory=False) part.addparam('return', '%i' % ret, mandatory=False) assert not inpart.read() _remotechangegroupparams = tuple(['url', 'size', 'digests'] + ['digest:%s' % k for k in util.DIGESTS.keys()]) -@parthandler('b2x:remote-changegroup', _remotechangegroupparams) +@parthandler('remote-changegroup', _remotechangegroupparams) def handleremotechangegroup(op, inpart): """apply a bundle10 on the repo, given an url and validation information @@ -976,7 +1095,7 @@ except KeyError: raise util.Abort(_('remote-changegroup: missing "%s" param') % 'url') parsed_url = util.url(raw_url) - if parsed_url.scheme not in capabilities['b2x:remote-changegroup']: + if parsed_url.scheme not in capabilities['remote-changegroup']: raise util.Abort(_('remote-changegroup does not support %s urls') % parsed_url.scheme) @@ -1016,7 +1135,7 @@ if op.reply is not None: # This is definitely not the final form of this # return. But one need to start somewhere. - part = op.reply.newpart('b2x:reply:changegroup') + part = op.reply.newpart('reply:changegroup') part.addparam('in-reply-to', str(inpart.id), mandatory=False) part.addparam('return', '%i' % ret, mandatory=False) try: @@ -1026,13 +1145,13 @@ (util.hidepassword(raw_url), str(e))) assert not inpart.read() -@parthandler('b2x:reply:changegroup', ('return', 'in-reply-to')) +@parthandler('reply:changegroup', ('return', 'in-reply-to')) def handlereplychangegroup(op, inpart): ret = int(inpart.params['return']) replyto = int(inpart.params['in-reply-to']) op.records.add('changegroup', {'return': ret}, replyto) -@parthandler('b2x:check:heads') +@parthandler('check:heads') def handlecheckheads(op, inpart): """check that head of the repo did not change @@ -1048,13 +1167,13 @@ raise error.PushRaced('repository changed while pushing - ' 'please try again') -@parthandler('b2x:output') +@parthandler('output') def handleoutput(op, inpart): """forward output captured on the server to the client""" for line in inpart.read().splitlines(): op.ui.write(('remote: %s\n' % line)) -@parthandler('b2x:replycaps') +@parthandler('replycaps') def handlereplycaps(op, inpart): """Notify that a reply bundle should be created @@ -1063,13 +1182,13 @@ if op.reply is None: op.reply = bundle20(op.ui, caps) -@parthandler('b2x:error:abort', ('message', 'hint')) -def handlereplycaps(op, inpart): +@parthandler('error:abort', ('message', 'hint')) +def handleerrorabort(op, inpart): """Used to transmit abort error over the wire""" raise util.Abort(inpart.params['message'], hint=inpart.params.get('hint')) -@parthandler('b2x:error:unsupportedcontent', ('parttype', 'params')) -def handlereplycaps(op, inpart): +@parthandler('error:unsupportedcontent', ('parttype', 'params')) +def handleerrorunsupportedcontent(op, inpart): """Used to transmit unknown content error over the wire""" kwargs = {} parttype = inpart.params.get('parttype') @@ -1081,19 +1200,19 @@ raise error.UnsupportedPartError(**kwargs) -@parthandler('b2x:error:pushraced', ('message',)) -def handlereplycaps(op, inpart): +@parthandler('error:pushraced', ('message',)) +def handleerrorpushraced(op, inpart): """Used to transmit push race error over the wire""" raise error.ResponseError(_('push failed:'), inpart.params['message']) -@parthandler('b2x:listkeys', ('namespace',)) +@parthandler('listkeys', ('namespace',)) def handlelistkeys(op, inpart): """retrieve pushkey namespace content stored in a bundle2""" namespace = inpart.params['namespace'] r = pushkey.decodekeys(inpart.read()) op.records.add('listkeys', (namespace, r)) -@parthandler('b2x:pushkey', ('namespace', 'key', 'old', 'new')) +@parthandler('pushkey', ('namespace', 'key', 'old', 'new')) def handlepushkey(op, inpart): """process a pushkey request""" dec = pushkey.decode @@ -1108,32 +1227,36 @@ 'new': new} op.records.add('pushkey', record) if op.reply is not None: - rpart = op.reply.newpart('b2x:reply:pushkey') + rpart = op.reply.newpart('reply:pushkey') rpart.addparam('in-reply-to', str(inpart.id), mandatory=False) rpart.addparam('return', '%i' % ret, mandatory=False) -@parthandler('b2x:reply:pushkey', ('return', 'in-reply-to')) +@parthandler('reply:pushkey', ('return', 'in-reply-to')) def handlepushkeyreply(op, inpart): """retrieve the result of a pushkey request""" ret = int(inpart.params['return']) partid = int(inpart.params['in-reply-to']) op.records.add('pushkey', {'return': ret}, partid) -@parthandler('b2x:obsmarkers') +@parthandler('obsmarkers') def handleobsmarker(op, inpart): """add a stream of obsmarkers to the repo""" tr = op.gettransaction() - new = op.repo.obsstore.mergemarkers(tr, inpart.read()) + markerdata = inpart.read() + if op.ui.config('experimental', 'obsmarkers-exchange-debug', False): + op.ui.write(('obsmarker-exchange: %i bytes received\n') + % len(markerdata)) + new = op.repo.obsstore.mergemarkers(tr, markerdata) if new: op.repo.ui.status(_('%i new obsolescence markers\n') % new) op.records.add('obsmarkers', {'new': new}) if op.reply is not None: - rpart = op.reply.newpart('b2x:reply:obsmarkers') + rpart = op.reply.newpart('reply:obsmarkers') rpart.addparam('in-reply-to', str(inpart.id), mandatory=False) rpart.addparam('new', '%i' % new, mandatory=False) -@parthandler('b2x:reply:obsmarkers', ('new', 'in-reply-to')) +@parthandler('reply:obsmarkers', ('new', 'in-reply-to')) def handlepushkeyreply(op, inpart): """retrieve the result of a pushkey request""" ret = int(inpart.params['new']) diff -r 612ed41ae359 -r e89f909edffa mercurial/bundlerepo.py --- a/mercurial/bundlerepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/bundlerepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -15,7 +15,7 @@ from i18n import _ import os, tempfile, shutil import changegroup, util, mdiff, discovery, cmdutil, scmutil, exchange -import localrepo, changelog, manifest, filelog, revlog, error, phases +import localrepo, changelog, manifest, filelog, revlog, error, phases, bundle2 class bundlerevlog(revlog.revlog): def __init__(self, opener, indexfile, bundle, linkmapper): @@ -177,9 +177,6 @@ def baserevision(self, nodeorrev): return filelog.filelog.revision(self, nodeorrev) - def _file(self, f): - self._repo.file(f) - class bundlepeer(localrepo.localpeer): def canpush(self): return False @@ -219,7 +216,7 @@ self.tempfile = None f = util.posixfile(bundlename, "rb") - self.bundle = exchange.readbundle(ui, f, bundlename) + self.bundlefile = self.bundle = exchange.readbundle(ui, f, bundlename) if self.bundle.compressed(): fdtemp, temp = self.vfs.mkstemp(prefix="hg-bundle-", suffix=".hg10un") @@ -237,7 +234,27 @@ fptemp.close() f = self.vfs.open(self.tempfile, mode="rb") - self.bundle = exchange.readbundle(ui, f, bundlename, self.vfs) + self.bundlefile = self.bundle = exchange.readbundle(ui, f, + bundlename, + self.vfs) + + if isinstance(self.bundle, bundle2.unbundle20): + cgparts = [part for part in self.bundle.iterparts() + if (part.type == 'changegroup') + and (part.params.get('version', '01') + in changegroup.packermap)] + + if not cgparts: + raise util.Abort('No changegroups found') + version = cgparts[0].params.get('version', '01') + cgparts = [p for p in cgparts + if p.params.get('version', '01') == version] + if len(cgparts) > 1: + raise NotImplementedError("Can't process multiple changegroups") + part = cgparts[0] + + part.seek(0) + self.bundle = changegroup.packermap[version][1](part, 'UN') # dict with the mapping 'filename' -> position in the bundle self.bundlefilespos = {} @@ -303,7 +320,7 @@ def close(self): """Close assigned bundle file immediately.""" - self.bundle.close() + self.bundlefile.close() if self.tempfile is not None: self.vfs.unlink(self.tempfile) if self._tempparent: @@ -409,7 +426,10 @@ rheads = None else: cg = other.changegroupsubset(incoming, rheads, 'incoming') - bundletype = localrepo and "HG10BZ" or "HG10UN" + if localrepo: + bundletype = "HG10BZ" + else: + bundletype = "HG10UN" fname = bundle = changegroup.writebundle(ui, cg, bundlename, bundletype) # keep written bundle? if bundlename: diff -r 612ed41ae359 -r e89f909edffa mercurial/byterange.py --- a/mercurial/byterange.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/byterange.py Thu Apr 16 20:57:51 2015 -0500 @@ -274,7 +274,11 @@ dirs = dirs[1:] try: fw = self.connect_ftp(user, passwd, host, port, dirs) - type = file and 'I' or 'D' + if file: + type = 'I' + else: + type = 'D' + for attr in attrs: attr, value = splitattr(attr) if attr.lower() == 'type' and \ diff -r 612ed41ae359 -r e89f909edffa mercurial/changegroup.py --- a/mercurial/changegroup.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/changegroup.py Thu Apr 16 20:57:51 2015 -0500 @@ -71,7 +71,7 @@ "": ("", nocompress), # only when using unbundle on ssh and old http servers # since the unification ssh accepts a header but there # is no capability signaling it. - "HG2Y": (), # special-cased below + "HG20": (), # special-cased below "HG10UN": ("HG10UN", nocompress), "HG10BZ": ("HG10", lambda: bz2.BZ2Compressor()), "HG10GZ": ("HG10GZ", lambda: zlib.compressobj()), @@ -102,16 +102,17 @@ fh = os.fdopen(fd, "wb") cleanup = filename - if bundletype == "HG2Y": + if bundletype == "HG20": import bundle2 bundle = bundle2.bundle20(ui) - part = bundle.newpart('b2x:changegroup', data=cg.getchunks()) + part = bundle.newpart('changegroup', data=cg.getchunks()) part.addparam('version', cg.version) z = nocompress() chunkiter = bundle.getchunks() else: if cg.version != '01': - raise util.Abort(_('Bundle1 only supports v1 changegroups\n')) + raise util.Abort(_('old bundle types only supports v1 ' + 'changegroups')) header, compressor = bundletypes[bundletype] fh.write(header) z = compressor() @@ -481,7 +482,17 @@ base = self.deltaparent(revlog, rev, p1, p2, prev) prefix = '' - if base == nullrev: + if revlog.iscensored(base) or revlog.iscensored(rev): + try: + delta = revlog.revision(node) + except error.CensoredNodeError, e: + delta = e.tombstone + if base == nullrev: + prefix = mdiff.trivialdiffheader(len(delta)) + else: + baselen = revlog.rawsize(base) + prefix = mdiff.replacediffheader(baselen, len(delta)) + elif base == nullrev: delta = revlog.revision(node) prefix = mdiff.trivialdiffheader(len(delta)) else: @@ -659,8 +670,11 @@ pr() fl = repo.file(f) o = len(fl) - if not fl.addgroup(source, revmap, trp): - raise util.Abort(_("received file revlog group is empty")) + try: + if not fl.addgroup(source, revmap, trp): + raise util.Abort(_("received file revlog group is empty")) + except error.CensoredBaseError, e: + raise util.Abort(_("received delta base is censored: %s") % e) revisions += len(fl) - o files += 1 if f in needfiles: @@ -877,6 +891,7 @@ finally: tr.release() + repo.ui.flush() # never return 0 here: if dh < 0: return dh - 1 diff -r 612ed41ae359 -r e89f909edffa mercurial/changelog.py --- a/mercurial/changelog.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/changelog.py Thu Apr 16 20:57:51 2015 -0500 @@ -143,6 +143,11 @@ if i not in self.filteredrevs: return self.node(i) + def __contains__(self, rev): + """filtered version of revlog.__contains__""" + return (0 <= rev < len(self) + and rev not in self.filteredrevs) + def __iter__(self): """filtered version of revlog.__iter__""" if len(self.filteredrevs) == 0: diff -r 612ed41ae359 -r e89f909edffa mercurial/cmdutil.py --- a/mercurial/cmdutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/cmdutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,18 +7,207 @@ from node import hex, nullid, nullrev, short from i18n import _ -import os, sys, errno, re, tempfile +import os, sys, errno, re, tempfile, cStringIO, shutil import util, scmutil, templater, patch, error, templatekw, revlog, copies import match as matchmod import context, repair, graphmod, revset, phases, obsolete, pathutil import changelog import bookmarks import encoding +import crecord as crecordmod import lock as lockmod def parsealiases(cmd): return cmd.lstrip("^").split("|") +def setupwrapcolorwrite(ui): + # wrap ui.write so diff output can be labeled/colorized + def wrapwrite(orig, *args, **kw): + label = kw.pop('label', '') + for chunk, l in patch.difflabel(lambda: args): + orig(chunk, label=label + l) + + oldwrite = ui.write + def wrap(*args, **kwargs): + return wrapwrite(oldwrite, *args, **kwargs) + setattr(ui, 'write', wrap) + return oldwrite + +def filterchunks(ui, originalhunks, usecurses, testfile): + if usecurses: + if testfile: + recordfn = crecordmod.testdecorator(testfile, + crecordmod.testchunkselector) + else: + recordfn = crecordmod.chunkselector + + return crecordmod.filterpatch(ui, originalhunks, recordfn) + + else: + return patch.filterpatch(ui, originalhunks) + +def recordfilter(ui, originalhunks): + usecurses = ui.configbool('experimental', 'crecord', False) + testfile = ui.config('experimental', 'crecordtest', None) + oldwrite = setupwrapcolorwrite(ui) + try: + newchunks = filterchunks(ui, originalhunks, usecurses, testfile) + finally: + ui.write = oldwrite + return newchunks + +def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, + filterfn, *pats, **opts): + import merge as mergemod + hunkclasses = (crecordmod.uihunk, patch.recordhunk) + ishunk = lambda x: isinstance(x, hunkclasses) + + if not ui.interactive(): + raise util.Abort(_('running non-interactively, use %s instead') % + cmdsuggest) + + # make sure username is set before going interactive + if not opts.get('user'): + ui.username() # raise exception, username not provided + + def recordfunc(ui, repo, message, match, opts): + """This is generic record driver. + + Its job is to interactively filter local changes, and + accordingly prepare working directory into a state in which the + job can be delegated to a non-interactive commit command such as + 'commit' or 'qrefresh'. + + After the actual job is done by non-interactive command, the + working directory is restored to its original state. + + In the end we'll record interesting changes, and everything else + will be left in place, so the user can continue working. + """ + + checkunfinished(repo, commit=True) + merge = len(repo[None].parents()) > 1 + if merge: + raise util.Abort(_('cannot partially commit a merge ' + '(use "hg commit" instead)')) + + status = repo.status(match=match) + diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True) + diffopts.nodates = True + diffopts.git = True + originaldiff = patch.diff(repo, changes=status, opts=diffopts) + originalchunks = patch.parsepatch(originaldiff) + + # 1. filter patch, so we have intending-to apply subset of it + try: + chunks = filterfn(ui, originalchunks) + except patch.PatchError, err: + raise util.Abort(_('error parsing patch: %s') % err) + + contenders = set() + for h in chunks: + try: + contenders.update(set(h.files())) + except AttributeError: + pass + + changed = status.modified + status.added + status.removed + newfiles = [f for f in changed if f in contenders] + if not newfiles: + ui.status(_('no changes to record\n')) + return 0 + + newandmodifiedfiles = set() + for h in chunks: + isnew = h.filename() in status.added + if ishunk(h) and isnew and not h in originalchunks: + newandmodifiedfiles.add(h.filename()) + + modified = set(status.modified) + + # 2. backup changed files, so we can restore them in the end + + if backupall: + tobackup = changed + else: + tobackup = [f for f in newfiles + if f in modified or f in newandmodifiedfiles] + + backups = {} + if tobackup: + backupdir = repo.join('record-backups') + try: + os.mkdir(backupdir) + except OSError, err: + if err.errno != errno.EEXIST: + raise + try: + # backup continues + for f in tobackup: + fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', + dir=backupdir) + os.close(fd) + ui.debug('backup %r as %r\n' % (f, tmpname)) + util.copyfile(repo.wjoin(f), tmpname) + shutil.copystat(repo.wjoin(f), tmpname) + backups[f] = tmpname + + fp = cStringIO.StringIO() + for c in chunks: + fname = c.filename() + if fname in backups or fname in newandmodifiedfiles: + c.write(fp) + dopatch = fp.tell() + fp.seek(0) + + [os.unlink(c) for c in newandmodifiedfiles] + + # 3a. apply filtered patch to clean repo (clean) + if backups: + # Equivalent to hg.revert + choices = lambda key: key in backups + mergemod.update(repo, repo.dirstate.p1(), + False, True, choices) + + # 3b. (apply) + if dopatch: + try: + ui.debug('applying patch\n') + ui.debug(fp.getvalue()) + patch.internalpatch(ui, repo, fp, 1, eolmode=None) + except patch.PatchError, err: + raise util.Abort(str(err)) + del fp + + # 4. We prepared working directory according to filtered + # patch. Now is the time to delegate the job to + # commit/qrefresh or the like! + + # Make all of the pathnames absolute. + newfiles = [repo.wjoin(nf) for nf in newfiles] + return commitfunc(ui, repo, *newfiles, **opts) + finally: + # 5. finally restore backed-up files + try: + for realname, tmpname in backups.iteritems(): + ui.debug('restoring %r to %r\n' % (tmpname, realname)) + util.copyfile(tmpname, repo.wjoin(realname)) + # Our calls to copystat() here and above are a + # hack to trick any editors that have f open that + # we haven't modified them. + # + # Also note that this racy as an editor could + # notice the file's mtime before we've finished + # writing it. + shutil.copystat(tmpname, repo.wjoin(realname)) + os.unlink(tmpname) + if tobackup: + os.rmdir(backupdir) + except OSError: + pass + + return commit(ui, repo, recordfunc, pats, opts) + def findpossible(cmd, table, strict=False): """ Return cmd -> (aliases, command table entry) @@ -34,8 +223,10 @@ else: keys = table.keys() + allcmds = [] for e in keys: aliases = parsealiases(e) + allcmds.extend(aliases) found = None if cmd in aliases: found = cmd @@ -53,11 +244,11 @@ if not choice and debugchoice: choice = debugchoice - return choice + return choice, allcmds def findcmd(cmd, table, strict=True): """Return (aliases, command table entry) for command string.""" - choice = findpossible(cmd, table, strict) + choice, allcmds = findpossible(cmd, table, strict) if cmd in choice: return choice[cmd] @@ -70,7 +261,7 @@ if choice: return choice.values()[0] - raise error.UnknownCommand(cmd) + raise error.UnknownCommand(cmd, allcmds) def findrepo(p): while not os.path.isdir(os.path.join(p, ".hg")): @@ -80,16 +271,15 @@ return p -def bailifchanged(repo): - if repo.dirstate.p2() != nullid: +def bailifchanged(repo, merge=True): + if merge and repo.dirstate.p2() != nullid: raise util.Abort(_('outstanding uncommitted merge')) modified, added, removed, deleted = repo.status()[:4] if modified or added or removed or deleted: raise util.Abort(_('uncommitted changes')) ctx = repo[None] for s in sorted(ctx.substate): - if ctx.sub(s).dirty(): - raise util.Abort(_("uncommitted changes in subrepo %s") % s) + ctx.sub(s).bailifchanged() def logmessage(ui, opts): """ get the log message according to -m and -l option """ @@ -110,22 +300,22 @@ (logfile, inst.strerror)) return message -def mergeeditform(ctxorbool, baseform): - """build appropriate editform from ctxorbool and baseform - - 'ctxorbool' is one of a ctx to be committed, or a bool whether +def mergeeditform(ctxorbool, baseformname): + """return appropriate editform name (referencing a committemplate) + + 'ctxorbool' is either a ctx to be committed, or a bool indicating whether merging is committed. - This returns editform 'baseform' with '.merge' if merging is - committed, or one with '.normal' suffix otherwise. + This returns baseformname with '.merge' appended if it is a merge, + otherwise '.normal' is appended. """ if isinstance(ctxorbool, bool): if ctxorbool: - return baseform + ".merge" + return baseformname + ".merge" elif 1 < len(ctxorbool.parents()): - return baseform + ".merge" - - return baseform + ".normal" + return baseformname + ".merge" + + return baseformname + ".normal" def getcommiteditor(edit=False, finishdesc=None, extramsg=None, editform='', **opts): @@ -225,7 +415,10 @@ writable = mode not in ('r', 'rb') if not pat or pat == '-': - fp = writable and repo.ui.fout or repo.ui.fin + if writable: + fp = repo.ui.fout + else: + fp = repo.ui.fin if util.safehasattr(fp, 'fileno'): return os.fdopen(os.dup(fp.fileno()), mode) else: @@ -301,7 +494,10 @@ def walkpat(pat): srcs = [] - badstates = after and '?' or '?r' + if after: + badstates = '?' + else: + badstates = '?r' m = scmutil.match(repo[None], [pat], opts, globbed=True) for abs in repo.walk(m): state = repo.dirstate[abs] @@ -387,7 +583,7 @@ srcexists = True except IOError, inst: if inst.errno == errno.ENOENT: - ui.warn(_('%s: deleted in working copy\n') % relsrc) + ui.warn(_('%s: deleted in working directory\n') % relsrc) srcexists = False else: ui.warn(_('%s: cannot copy - %s\n') % @@ -476,7 +672,6 @@ res = lambda p: dest return res - pats = scmutil.expandpats(pats) if not pats: raise util.Abort(_('no source or destination specified')) @@ -520,7 +715,10 @@ def writepid(pid): if opts['pid_file']: - mode = appendpid and 'a' or 'w' + if appendpid: + mode = 'a' + else: + mode = 'w' fp = open(opts['pid_file'], mode) fp.write(str(pid) + '\n') fp.close() @@ -613,6 +811,7 @@ update = not opts.get('bypass') strip = opts["strip"] + prefix = opts["prefix"] sim = float(opts.get('similarity') or 0) if not tmpname: return (None, None, False) @@ -672,8 +871,8 @@ partial = opts.get('partial', False) files = set() try: - patch.patch(ui, repo, tmpname, strip=strip, files=files, - eolmode=None, similarity=sim / 100.0) + patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix, + files=files, eolmode=None, similarity=sim / 100.0) except patch.PatchError, e: if not partial: raise util.Abort(str(e)) @@ -710,7 +909,7 @@ try: files = set() try: - patch.patchrepo(ui, repo, p1, store, tmpname, strip, + patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix, files, eolmode=None) except patch.PatchError, e: raise util.Abort(str(e)) @@ -755,7 +954,11 @@ branch = ctx.branch() if switch_parent: parents.reverse() - prev = (parents and parents[0]) or nullid + + if parents: + prev = parents[0] + else: + prev = nullid shouldclose = False if not fp and len(template) > 0: @@ -775,7 +978,6 @@ def write(s, **kw): fp.write(s) - write("# HG changeset patch\n") write("# User %s\n" % ctx.user()) write("# Date %d %d\n" % ctx.date()) @@ -800,7 +1002,7 @@ def diffordiffstat(ui, repo, diffopts, node1, node2, match, changes=None, stat=False, fp=None, prefix='', - listsubrepos=False): + root='', listsubrepos=False): '''show diff or diffstat.''' if fp is None: write = ui.write @@ -808,20 +1010,35 @@ def write(s, **kw): fp.write(s) + if root: + relroot = pathutil.canonpath(repo.root, repo.getcwd(), root) + else: + relroot = '' + if relroot != '': + # XXX relative roots currently don't work if the root is within a + # subrepo + uirelroot = match.uipath(relroot) + relroot += '/' + for matchroot in match.files(): + if not matchroot.startswith(relroot): + ui.warn(_('warning: %s not inside relative root %s\n') % ( + match.uipath(matchroot), uirelroot)) + if stat: diffopts = diffopts.copy(context=0) width = 80 if not ui.plain(): width = ui.termwidth() chunks = patch.diff(repo, node1, node2, match, changes, diffopts, - prefix=prefix) + prefix=prefix, relroot=relroot) for chunk, label in patch.diffstatui(util.iterlines(chunks), width=width, git=diffopts.git): write(chunk, label=label) else: for chunk, label in patch.diffui(repo, node1, node2, match, - changes, diffopts, prefix=prefix): + changes, diffopts, prefix=prefix, + relroot=relroot): write(chunk, label=label) if listsubrepos: @@ -884,22 +1101,24 @@ '''show a single changeset or file revision''' changenode = ctx.node() rev = ctx.rev() + if self.ui.debugflag: + hexfunc = hex + else: + hexfunc = short + if rev is None: + pctx = ctx.p1() + revnode = (pctx.rev(), hexfunc(pctx.node()) + '+') + else: + revnode = (rev, hexfunc(changenode)) if self.ui.quiet: - self.ui.write("%d:%s\n" % (rev, short(changenode)), - label='log.node') + self.ui.write("%d:%s\n" % revnode, label='log.node') return - log = self.repo.changelog date = util.datestr(ctx.date()) - hexfunc = self.ui.debugflag and hex or short - - parents = [(p, hexfunc(log.node(p))) - for p in self._meaningful_parentrevs(log, rev)] - # i18n: column positioning for "hg log" - self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)), + self.ui.write(_("changeset: %d:%s\n") % revnode, label='log.changeset changeset.%s' % ctx.phasestr()) # branches are shown first before any other names due to backwards @@ -925,13 +1144,14 @@ # i18n: column positioning for "hg log" self.ui.write(_("phase: %s\n") % _(ctx.phasestr()), label='log.phase') - for parent in parents: - label = 'log.parent changeset.%s' % self.repo[parent[0]].phasestr() + for pctx in self._meaningful_parentrevs(ctx): + label = 'log.parent changeset.%s' % pctx.phasestr() # i18n: column positioning for "hg log" - self.ui.write(_("parent: %d:%s\n") % parent, + self.ui.write(_("parent: %d:%s\n") + % (pctx.rev(), hexfunc(pctx.node())), label=label) - if self.ui.debugflag: + if self.ui.debugflag and rev is not None: mnode = ctx.manifestnode() # i18n: column positioning for "hg log" self.ui.write(_("manifest: %d:%s\n") % @@ -945,7 +1165,7 @@ label='log.date') if self.ui.debugflag: - files = self.repo.status(log.parents(changenode)[0], changenode)[:3] + files = ctx.p1().status(ctx)[:3] for key, value in zip([# i18n: column positioning for "hg log" _("files:"), # i18n: column positioning for "hg log" @@ -1008,19 +1228,20 @@ match=matchfn, stat=False) self.ui.write("\n") - def _meaningful_parentrevs(self, log, rev): + def _meaningful_parentrevs(self, ctx): """Return list of meaningful (or all if debug) parentrevs for rev. For merges (two non-nullrev revisions) both parents are meaningful. Otherwise the first parent revision is considered meaningful if it is not the preceding revision. """ - parents = log.parentrevs(rev) - if not self.ui.debugflag and parents[1] == nullrev: - if parents[0] >= rev - 1: - parents = [] - else: - parents = [parents[0]] + parents = ctx.parents() + if len(parents) > 1: + return parents + if self.ui.debugflag: + return [parents[0], self.repo['null']] + if parents[0].rev() >= scmutil.intrev(self.repo, ctx.rev()) - 1: + return [] return parents class jsonchangeset(changeset_printer): @@ -1039,8 +1260,12 @@ def _show(self, ctx, copies, matchfn, props): '''show a single changeset or file revision''' - hexnode = hex(ctx.node()) rev = ctx.rev() + if rev is None: + jrev = jnode = 'null' + else: + jrev = str(rev) + jnode = '"%s"' % hex(ctx.node()) j = encoding.jsonescape if self._first: @@ -1050,13 +1275,13 @@ self.ui.write(",\n {") if self.ui.quiet: - self.ui.write('\n "rev": %d' % rev) - self.ui.write(',\n "node": "%s"' % hexnode) + self.ui.write('\n "rev": %s' % jrev) + self.ui.write(',\n "node": %s' % jnode) self.ui.write('\n }') return - self.ui.write('\n "rev": %d' % rev) - self.ui.write(',\n "node": "%s"' % hexnode) + self.ui.write('\n "rev": %s' % jrev) + self.ui.write(',\n "node": %s' % jnode) self.ui.write(',\n "branch": "%s"' % j(ctx.branch())) self.ui.write(',\n "phase": "%s"' % ctx.phasestr()) self.ui.write(',\n "user": "%s"' % j(ctx.user())) @@ -1071,7 +1296,11 @@ ", ".join('"%s"' % c.hex() for c in ctx.parents())) if self.ui.debugflag: - self.ui.write(',\n "manifest": "%s"' % hex(ctx.manifestnode())) + if rev is None: + jmanifestnode = 'null' + else: + jmanifestnode = '"%s"' % hex(ctx.manifestnode()) + self.ui.write(',\n "manifest": %s' % jmanifestnode) self.ui.write(',\n "extra": {%s}' % ", ".join('"%s": "%s"' % (j(k), j(v)) @@ -1134,18 +1363,6 @@ self.cache = {} - def _meaningful_parentrevs(self, ctx): - """Return list of meaningful (or all if debug) parentrevs for rev. - """ - parents = ctx.parents() - if len(parents) > 1: - return parents - if self.ui.debugflag: - return [parents[0], self.repo['null']] - if parents[0].rev() >= ctx.rev() - 1: - return [] - return parents - def _show(self, ctx, copies, matchfn, props): '''show a single changeset or file revision''' @@ -1429,7 +1646,6 @@ else: last = filelog.rev(node) - # keep track of all ancestors of the file ancestors = set([filelog.linkrev(last)]) @@ -1457,6 +1673,44 @@ return wanted +class _followfilter(object): + def __init__(self, repo, onlyfirst=False): + self.repo = repo + self.startrev = nullrev + self.roots = set() + self.onlyfirst = onlyfirst + + def match(self, rev): + def realparents(rev): + if self.onlyfirst: + return self.repo.changelog.parentrevs(rev)[0:1] + else: + return filter(lambda x: x != nullrev, + self.repo.changelog.parentrevs(rev)) + + if self.startrev == nullrev: + self.startrev = rev + return True + + if rev > self.startrev: + # forward: all descendants + if not self.roots: + self.roots.add(self.startrev) + for parent in realparents(rev): + if parent in self.roots: + self.roots.add(rev) + return True + else: + # backwards: all parents + if not self.roots: + self.roots.update(realparents(self.startrev)) + if rev in self.roots: + self.roots.remove(rev) + self.roots.update(realparents(rev)) + return True + + return False + def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. @@ -1473,14 +1727,7 @@ function on each context in the window in forward order.''' follow = opts.get('follow') or opts.get('follow_first') - - if opts.get('rev'): - revs = scmutil.revrange(repo, opts.get('rev')) - elif follow: - revs = repo.revs('reverse(:.)') - else: - revs = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return [] wanted = set() @@ -1493,7 +1740,7 @@ # wanted: a cache of filenames that were changed (ctx.files()) and that # match the file filtering conditions. - if not slowpath and not match.files(): + if match.always(): # No files, no patterns. Display all revs. wanted = revs @@ -1552,48 +1799,11 @@ wanted = lazywantedset() - class followfilter(object): - def __init__(self, onlyfirst=False): - self.startrev = nullrev - self.roots = set() - self.onlyfirst = onlyfirst - - def match(self, rev): - def realparents(rev): - if self.onlyfirst: - return repo.changelog.parentrevs(rev)[0:1] - else: - return filter(lambda x: x != nullrev, - repo.changelog.parentrevs(rev)) - - if self.startrev == nullrev: - self.startrev = rev - return True - - if rev > self.startrev: - # forward: all descendants - if not self.roots: - self.roots.add(self.startrev) - for parent in realparents(rev): - if parent in self.roots: - self.roots.add(rev) - return True - else: - # backwards: all parents - if not self.roots: - self.roots.update(realparents(self.startrev)) - if rev in self.roots: - self.roots.remove(rev) - self.roots.update(realparents(rev)) - return True - - return False - # it might be worthwhile to do this in the iterator if the rev range # is descending and the prune args are all within that range for rev in opts.get('prune', ()): rev = repo[rev].rev() - ff = followfilter() + ff = _followfilter(repo) stop = min(revs[0], revs[-1]) for x in xrange(rev, stop - 1, -1): if ff.match(x): @@ -1603,7 +1813,7 @@ # revision range, yielding only revisions in wanted. def iterate(): if follow and not match.files(): - ff = followfilter(onlyfirst=opts.get('follow_first')) + ff = _followfilter(repo, onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: @@ -1699,7 +1909,10 @@ opts = dict(opts) # follow or not follow? follow = opts.get('follow') or opts.get('follow_first') - followfirst = opts.get('follow_first') and 1 or 0 + if opts.get('follow_first'): + followfirst = 1 + else: + followfirst = 0 # --follow with FILE behaviour depends on revs... it = iter(revs) startrev = it.next() @@ -1716,12 +1929,12 @@ # _matchfiles() revset but walkchangerevs() builds its matcher with # scmutil.match(). The difference is input pats are globbed on # platforms without shell expansion (windows). - pctx = repo[None] - match, pats = scmutil.matchandpats(pctx, pats, opts) + wctx = repo[None] + match, pats = scmutil.matchandpats(wctx, pats, opts) slowpath = match.anypats() or (match.files() and opts.get('removed')) if not slowpath: for f in match.files(): - if follow and f not in pctx: + if follow and f not in wctx: # If the file exists, it may be a directory, so let it # take the slow path. if os.path.exists(repo.wjoin(f)): @@ -1822,6 +2035,21 @@ expr = None return expr, filematcher +def _logrevs(repo, opts): + # Default --rev value depends on --follow but --follow behaviour + # depends on revisions resolved from --rev... + follow = opts.get('follow') or opts.get('follow_first') + if opts.get('rev'): + revs = scmutil.revrange(repo, opts['rev']) + elif follow and repo.dirstate.p1() == nullid: + revs = revset.baseset() + elif follow: + revs = repo.revs('reverse(:.)') + else: + revs = revset.spanset(repo) + revs.reverse() + return revs + def getgraphlogrevs(repo, pats, opts): """Return (revs, expr, filematcher) where revs is an iterable of revision numbers, expr is a revset string built from log options @@ -1830,28 +2058,14 @@ callable taking a revision number and returning a match objects filtering the files to be detailed when displaying the revision. """ - if not len(repo): - return [], None, None limit = loglimit(opts) - # Default --rev value depends on --follow but --follow behaviour - # depends on revisions resolved from --rev... - follow = opts.get('follow') or opts.get('follow_first') - possiblyunsorted = False # whether revs might need sorting - if opts.get('rev'): - revs = scmutil.revrange(repo, opts['rev']) - # Don't sort here because _makelogrevset might depend on the - # order of revs - possiblyunsorted = True - else: - if follow and len(repo) > 0: - revs = repo.revs('reverse(:.)') - else: - revs = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return revset.baseset(), None, None expr, filematcher = _makelogrevset(repo, pats, opts, revs) - if possiblyunsorted: + if opts.get('rev'): + # User-specified revs might be unsorted, but don't sort before + # _makelogrevset because it might depend on the order of revs revs.sort(reverse=True) if expr: # Revset matchers often operate faster on revisions in changelog @@ -1882,16 +2096,7 @@ filtering the files to be detailed when displaying the revision. """ limit = loglimit(opts) - # Default --rev value depends on --follow but --follow behaviour - # depends on revisions resolved from --rev... - follow = opts.get('follow') or opts.get('follow_first') - if opts.get('rev'): - revs = scmutil.revrange(repo, opts['rev']) - elif follow: - revs = repo.revs('reverse(:.)') - else: - revs = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return revset.baseset([]), None, None expr, filematcher = _makelogrevset(repo, pats, opts, revs) @@ -1930,6 +2135,8 @@ char = '@' elif ctx.obsolete(): char = 'x' + elif ctx.closesbranch(): + char = '_' copies = None if getrenamed and ctx.rev(): copies = [] @@ -2064,6 +2271,35 @@ forgot.extend(f for f in forget if f not in rejected) return bad, forgot +def files(ui, ctx, m, fm, fmt, subrepos): + rev = ctx.rev() + ret = 1 + ds = ctx.repo().dirstate + + for f in ctx.matches(m): + if rev is None and ds[f] == 'r': + continue + fm.startitem() + if ui.verbose: + fc = ctx[f] + fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags()) + fm.data(abspath=f) + fm.write('path', fmt, m.rel(f)) + ret = 0 + + if subrepos: + for subpath in sorted(ctx.substate): + sub = ctx.sub(subpath) + try: + submatch = matchmod.narrowmatcher(subpath, m) + if sub.printfiles(ui, submatch, fm, fmt) == 0: + ret = 0 + except error.LookupError: + ui.status(_("skipping missing subrepository: %s\n") + % m.abs(subpath)) + + return ret + def remove(ui, repo, m, prefix, after, force, subrepos): join = lambda f: os.path.join(prefix, f) ret = 0 @@ -2092,6 +2328,7 @@ % join(subpath)) # warn about failure to delete explicit files/dirs + deleteddirs = util.dirs(deleted) for f in m.files(): def insubrepo(): for subpath in wctx.substate: @@ -2099,7 +2336,8 @@ return True return False - if f in repo.dirstate or f in wctx.dirs() or f == '.' or insubrepo(): + isdir = f in deleteddirs or f in wctx.dirs() + if f in repo.dirstate or isdir or f == '.' or insubrepo(): continue if repo.wvfs.exists(f): @@ -2164,8 +2402,8 @@ if len(matcher.files()) == 1 and not matcher.anypats(): file = matcher.files()[0] mf = repo.manifest - mfnode = ctx._changeset[0] - if mf.find(mfnode, file)[0]: + mfnode = ctx.manifestnode() + if mfnode and mf.find(mfnode, file)[0]: write(file) return 0 @@ -2220,7 +2458,7 @@ def amend(ui, repo, commitfunc, old, extra, pats, opts): # amend will reuse the existing user if not specified, but the obsolete # marker creation requires that the current user's name is specified. - if obsolete._enabled: + if obsolete.isenabled(repo, obsolete.createmarkersopt): ui.username() # raise exception if username not set ui.note(_('amending changeset %s\n') % old) @@ -2555,7 +2793,9 @@ # need all matching names in dirstate and manifest of target rev, # so have to walk both. do not print errors if files exist in one - # but not other. + # but not other. in both cases, filesets should be evaluated against + # workingctx to get consistent result (issue4497). this means 'set:**' + # cannot be used to select missing files from target rev. # `names` is a mapping for all elements in working copy and target revision # The mapping is in the form: @@ -2567,8 +2807,14 @@ ## filling of the `names` mapping # walk dirstate to fill `names` - m = scmutil.match(repo[None], pats, opts) - if not m.always() or node != parent: + interactive = opts.get('interactive', False) + wctx = repo[None] + m = scmutil.match(wctx, pats, opts) + + # we'll need this later + targetsubs = sorted(s for s in wctx.substate if m(s)) + + if not m.always(): m.bad = lambda x, y: False for abs in repo.walk(m): names[abs] = m.rel(abs), m.exact(abs) @@ -2586,7 +2832,6 @@ return ui.warn("%s: %s\n" % (m.rel(path), msg)) - m = scmutil.match(ctx, pats, opts) m.bad = badfn for abs in ctx.walk(m): if abs not in names: @@ -2598,7 +2843,7 @@ changes = repo.status(node1=node, match=m, unknown=True, ignored=True, clean=True) else: - changes = repo.status(match=m) + changes = repo.status(node1=node, match=m) for kind in changes: for abs in kind: names[abs] = m.rel(abs), m.exact(abs) @@ -2621,9 +2866,8 @@ deladded = _deleted - smf deleted = _deleted - deladded - # We need to account for the state of file in the dirstate. - # - # Even, when we revert against something else than parent. This will + # We need to account for the state of the file in the dirstate, + # even when we revert against something else than parent. This will # slightly alter the behavior of revert (doing back up or not, delete # or just forget etc). if parent == node: @@ -2772,7 +3016,6 @@ (unknown, actions['unknown'], discard), ) - wctx = repo[None] for abs, (rel, exact) in sorted(names.items()): # target file to be touch on disk (relative to cwd) target = repo.wjoin(abs) @@ -2790,7 +3033,10 @@ ui.note(_('saving current version of %s as %s\n') % (rel, bakname)) if not opts.get('dry_run'): - util.rename(target, bakname) + if interactive: + util.copyfile(target, bakname) + else: + util.rename(target, bakname) if ui.verbose or not exact: if not isinstance(msg, basestring): msg = msg(abs) @@ -2799,21 +3045,19 @@ ui.warn(msg % rel) break - if not opts.get('dry_run'): needdata = ('revert', 'add', 'undelete') _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata]) - - _performrevert(repo, parents, ctx, actions) - - # get the list of subrepos that must be reverted - subrepomatch = scmutil.match(ctx, pats, opts) - targetsubs = sorted(s for s in ctx.substate if subrepomatch(s)) - - if targetsubs: - # Revert the subrepos on the revert list - for sub in targetsubs: - ctx.sub(sub).revert(ctx.substate[sub], *pats, **opts) + _performrevert(repo, parents, ctx, actions, interactive) + + if targetsubs: + # Revert the subrepos on the revert list + for sub in targetsubs: + try: + wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts) + except KeyError: + raise util.Abort("subrepository '%s' does not exist in %s!" + % (sub, short(ctx.node()))) finally: wlock.release() @@ -2821,7 +3065,7 @@ """Let extension changing the storage layer prefetch content""" pass -def _performrevert(repo, parents, ctx, actions): +def _performrevert(repo, parents, ctx, actions, interactive=False): """function that actually perform all the actions computed for revert This is an independent function to let extension to plug in and react to @@ -2855,10 +3099,35 @@ normal = repo.dirstate.normallookup else: normal = repo.dirstate.normal - for f in actions['revert'][0]: - checkout(f) - if normal: - normal(f) + + if interactive: + # Prompt the user for changes to revert + torevert = [repo.wjoin(f) for f in actions['revert'][0]] + m = scmutil.match(ctx, torevert, {}) + diff = patch.diff(repo, None, ctx.node(), m) + originalchunks = patch.parsepatch(diff) + try: + chunks = recordfilter(repo.ui, originalchunks) + except patch.PatchError, err: + raise util.Abort(_('error parsing patch: %s') % err) + + # Apply changes + fp = cStringIO.StringIO() + for c in chunks: + c.write(fp) + dopatch = fp.tell() + fp.seek(0) + if dopatch: + try: + patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None) + except patch.PatchError, err: + raise util.Abort(str(err)) + del fp + else: + for f in actions['revert'][0]: + checkout(f) + if normal: + normal(f) for f in actions['add'][0]: checkout(f) diff -r 612ed41ae359 -r e89f909edffa mercurial/commands.py --- a/mercurial/commands.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/commands.py Thu Apr 16 20:57:51 2015 -0500 @@ -148,6 +148,7 @@ ('U', 'unified', '', _('number of lines of context to show'), _('NUM')), ('', 'stat', None, _('output diffstat-style summary of changes')), + ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')), ] mergetoolopts = [ @@ -276,13 +277,44 @@ # to mimic the behavior of Mercurial before version 1.5 opts['file'] = True + ctx = scmutil.revsingle(repo, opts.get('rev')) + fm = ui.formatter('annotate', opts) - datefunc = ui.quiet and util.shortdate or util.datestr - hexfn = fm.hexfunc + if ui.quiet: + datefunc = util.shortdate + else: + datefunc = util.datestr + if ctx.rev() is None: + def hexfn(node): + if node is None: + return None + else: + return fm.hexfunc(node) + if opts.get('changeset'): + # omit "+" suffix which is appended to node hex + def formatrev(rev): + if rev is None: + return '%d' % ctx.p1().rev() + else: + return '%d' % rev + else: + def formatrev(rev): + if rev is None: + return '%d+' % ctx.p1().rev() + else: + return '%d ' % rev + def formathex(hex): + if hex is None: + return '%s+' % fm.hexfunc(ctx.p1().node()) + else: + return '%s ' % hex + else: + hexfn = fm.hexfunc + formatrev = formathex = str opmap = [('user', ' ', lambda x: x[0].user(), ui.shortuser), - ('number', ' ', lambda x: x[0].rev(), str), - ('changeset', ' ', lambda x: hexfn(x[0].node()), str), + ('number', ' ', lambda x: x[0].rev(), formatrev), + ('changeset', ' ', lambda x: hexfn(x[0].node()), formathex), ('date', ' ', lambda x: x[0].date(), util.cachefunc(datefunc)), ('file', ' ', lambda x: x[0].path(), str), ('line_number', ':', lambda x: x[1], str), @@ -312,7 +344,6 @@ def bad(x, y): raise util.Abort("%s: %s" % (x, y)) - ctx = scmutil.revsingle(repo, opts.get('rev')) m = scmutil.match(ctx, pats, opts) m.bad = bad follow = not opts.get('no_follow') @@ -664,7 +695,10 @@ # one of the parent was not checked. parents = repo[nodes[0]].parents() if len(parents) > 1: - side = good and state['bad'] or state['good'] + if good: + side = state['bad'] + else: + side = state['good'] num = len(set(i.node() for i in parents) & set(side)) if num == 1: return parents[0].ancestor(parents[1]) @@ -1184,7 +1218,7 @@ btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ', - 'bundle2': 'HG2Y'} + 'bundle2': 'HG20'} bundletype = btypes.get(bundletype) if bundletype not in changegroup.bundletypes: raise util.Abort(_('unknown bundle type specified with --type')) @@ -1257,8 +1291,8 @@ return cmdutil.cat(ui, repo, ctx, m, '', **opts) @command('^clone', - [('U', 'noupdate', None, - _('the clone will include an empty working copy (only a repository)')), + [('U', 'noupdate', None, _('the clone will include an empty working ' + 'directory (only a repository)')), ('u', 'updaterev', '', _('revision, tag or branch to check out'), _('REV')), ('r', 'rev', [], _('include the specified changeset'), _('REV')), ('b', 'branch', [], _('clone only the specified branch'), _('BRANCH')), @@ -1380,9 +1414,10 @@ _('mark new/missing files as added/removed before committing')), ('', 'close-branch', None, _('mark a branch as closed, hiding it from the branch list')), - ('', 'amend', None, _('amend the parent of the working dir')), + ('', 'amend', None, _('amend the parent of the working directory')), ('s', 'secret', None, _('use the secret phase for committing')), ('e', 'edit', None, _('invoke editor on commit messages')), + ('i', 'interactive', None, _('use interactive mode')), ] + walkopts + commitopts + commitopts2 + subrepoopts, _('[OPTION]... [FILE]...'), inferrepo=True) @@ -1422,6 +1457,12 @@ Returns 0 on success, 1 if nothing changed. """ + if opts.get('interactive'): + opts.pop('interactive') + cmdutil.dorecord(ui, repo, commit, 'commit', False, + cmdutil.recordfilter, *pats, **opts) + return + if opts.get('subrepos'): if opts.get('amend'): raise util.Abort(_('cannot amend with --subrepos')) @@ -1874,7 +1915,7 @@ ui.write(('Stream params: %s\n' % repr(gen.params))) for part in gen.iterparts(): ui.write('%s -- %r\n' % (part.type, repr(part.params))) - if part.type == 'b2x:changegroup': + if part.type == 'changegroup': version = part.params.get('version', '01') cg = changegroup.packermap[version][1](part, 'UN') chunkdata = cg.changelogheader() @@ -1946,7 +1987,7 @@ ui.write("%s\n" % "\n".join(options)) return - cmdlist = cmdutil.findpossible(cmd, table) + cmdlist, unused_allcmds = cmdutil.findpossible(cmd, table) if ui.verbose: cmdlist = [' '.join(c[0]) for c in cmdlist.values()] ui.write("%s\n" % "\n".join(sorted(cmdlist))) @@ -2167,7 +2208,7 @@ btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ', - 'bundle2': 'HG2Y'} + 'bundle2': 'HG20'} bundletype = btypes.get(bundletype) if bundletype not in changegroup.bundletypes: raise util.Abort(_('unknown bundle type specified with --type')) @@ -2799,6 +2840,7 @@ deltasize[2] /= numrevs - numfull totalsize = fulltotal + deltatotal avgchainlen = sum(chainlengths) / numrevs + maxchainlen = max(chainlengths) compratio = totalrawsize / totalsize basedfmtstr = '%%%dd\n' @@ -2831,6 +2873,7 @@ ui.write('\n') fmt = dfmtstr(max(avgchainlen, compratio)) ui.write(('avg chain length : ') + fmt % avgchainlen) + ui.write(('max chain length : ') + fmt % maxchainlen) ui.write(('compression ratio : ') + fmt % compratio) if format > 0: @@ -2885,7 +2928,10 @@ weight, optimizedtree = revset.optimize(newtree, True) ui.note("* optimized:\n", revset.prettyformat(optimizedtree), "\n") func = revset.match(ui, expr) - for c in func(repo, revset.spanset(repo)): + revs = func(repo) + if ui.verbose: + ui.note("* set:\n", revset.prettyformatset(revs), "\n") + for c in revs: ui.write("%s\n" % c) @command('debugsetparents', [], _('REV1 [REV2]')) @@ -2893,7 +2939,9 @@ """manually set the parents of the current working directory This is useful for writing repository conversion tools, but should - be used with care. + be used with care. For example, neither the working directory nor the + dirstate is updated, so file status may be incorrect after running this + command. Returns 0 on success. """ @@ -3124,7 +3172,8 @@ diffopts = patch.diffallopts(ui, opts) m = scmutil.match(repo[node2], pats, opts) cmdutil.diffordiffstat(ui, repo, diffopts, node1, node2, m, stat=stat, - listsubrepos=opts.get('subrepos')) + listsubrepos=opts.get('subrepos'), + root=opts.get('root')) @command('^export', [('o', 'output', '', @@ -3210,7 +3259,7 @@ @command('files', [('r', 'rev', '', _('search the repository as it is in REV'), _('REV')), ('0', 'print0', None, _('end filenames with NUL, for use with xargs')), - ] + walkopts + formatteropts, + ] + walkopts + formatteropts + subrepoopts, _('[OPTION]... [PATTERN]...')) def files(ui, repo, *pats, **opts): """list tracked files @@ -3220,7 +3269,7 @@ removed files). If no patterns are given to match, this command prints the names - of all files under Mercurial control in the working copy. + of all files under Mercurial control in the working directory. .. container:: verbose @@ -3257,8 +3306,6 @@ """ ctx = scmutil.revsingle(repo, opts.get('rev'), None) - rev = ctx.rev() - ret = 1 end = '\n' if opts.get('print0'): @@ -3267,17 +3314,7 @@ fmt = '%s' + end m = scmutil.match(ctx, pats, opts) - ds = repo.dirstate - for f in ctx.matches(m): - if rev is None and ds[f] == 'r': - continue - fm.startitem() - if ui.verbose: - fc = ctx[f] - fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags()) - fm.data(abspath=f) - fm.write('path', fmt, m.rel(f)) - ret = 0 + ret = cmdutil.files(ui, ctx, m, fm, fmt, opts.get('subrepos')) fm.end() @@ -3507,9 +3544,12 @@ continue source = ctx.extra().get('source') - if not source: - source = ctx.hex() - extra = {'source': source} + extra = {} + if source: + extra['source'] = source + extra['intermediate-source'] = ctx.hex() + else: + extra['source'] = ctx.hex() user = ctx.user() if opts.get('user'): user = opts['user'] @@ -3675,7 +3715,10 @@ def display(fn, ctx, pstates, states): rev = ctx.rev() - datefunc = ui.quiet and util.shortdate or util.datestr + if ui.quiet: + datefunc = util.shortdate + else: + datefunc = util.datestr found = False @util.cachefunc def binary(): @@ -3915,7 +3958,7 @@ optionalrepo=True) def identify(ui, repo, source=None, rev=None, num=None, id=None, branch=None, tags=None, bookmarks=None, **opts): - """identify the working copy or specified revision + """identify the working directory or specified revision Print a summary identifying the repository state at REV using one or two parent hash identifiers, followed by a "+" if the working @@ -3951,7 +3994,10 @@ raise util.Abort(_("there is no Mercurial repository here " "(.hg not found)")) - hexfunc = ui.debugflag and hex or short + if ui.debugflag: + hexfunc = hex + else: + hexfunc = short default = not (num or id or branch or tags or bookmarks) output = [] revs = [] @@ -4056,6 +4102,8 @@ _('commit even if some hunks fail')), ('', 'exact', None, _('apply patch to the nodes from which it was generated')), + ('', 'prefix', '', + _('apply patch to subdirectory'), _('DIR')), ('', 'import-branch', None, _('use any branch information in patch (implied by --exact)'))] + commitopts + commitopts2 + similarityopts, @@ -4155,6 +4203,8 @@ raise util.Abort(_('cannot use --similarity with --bypass')) if opts.get('exact') and opts.get('edit'): raise util.Abort(_('cannot use --exact with --edit')) + if opts.get('exact') and opts.get('prefix'): + raise util.Abort(_('cannot use --exact with --prefix')) if update: cmdutil.checkunfinished(repo) @@ -4243,13 +4293,36 @@ pull location. These are the changesets that would have been pulled if a pull at the time you issued this command. - For remote repository, using --bundle avoids downloading the - changesets twice if the incoming is followed by a pull. - See pull for valid source format details. .. container:: verbose + With -B/--bookmarks, the result of bookmark comparison between + local and remote repositories is displayed. With -v/--verbose, + status is also displayed for each bookmark like below:: + + BM1 01234567890a added + BM2 1234567890ab advanced + BM3 234567890abc diverged + BM4 34567890abcd changed + + The action taken locally when pulling depends on the + status of each bookmark: + + :``added``: pull will create it + :``advanced``: pull will update it + :``diverged``: pull will create a divergent bookmark + :``changed``: result depends on remote changesets + + From the point of view of pulling behavior, bookmark + existing only in the remote repository are treated as ``added``, + even if it is in fact locally deleted. + + .. container:: verbose + + For remote repository, using --bundle avoids downloading the + changesets twice if the incoming is followed by a pull. + Examples: - show incoming changes with patches and full description:: @@ -4289,7 +4362,7 @@ ui.warn(_("remote doesn't support bookmarks\n")) return 0 ui.status(_('comparing with %s\n') % util.hidepassword(source)) - return bookmarks.diff(ui, repo, other) + return bookmarks.incoming(ui, repo, other) repo._subtoppath = ui.expandpath(source) try: @@ -4343,7 +4416,10 @@ Returns 0 if a match is found, 1 otherwise. """ - end = opts.get('print0') and '\0' or '\n' + if opts.get('print0'): + end = '\0' + else: + end = '\n' rev = scmutil.revsingle(repo, opts.get('rev'), None).node() ret = 1 @@ -4477,6 +4553,10 @@ Returns 0 on success. """ + if opts.get('follow') and opts.get('rev'): + opts['rev'] = [revset.formatspec('reverse(::%lr)', opts.get('rev'))] + del opts['follow'] + if opts.get('graph'): return cmdutil.graphlog(ui, repo, *pats, **opts) @@ -4503,7 +4583,10 @@ rename = getrenamed(fn, rev) if rename: copies.append((fn, rename[0])) - revmatchfn = filematcher and filematcher(ctx.rev()) or None + if filematcher: + revmatchfn = filematcher(ctx.rev()) + else: + revmatchfn = None displayer.show(ctx, copies=copies, matchfn=revmatchfn) if displayer.flush(rev): count += 1 @@ -4709,6 +4792,31 @@ See pull for details of valid destination formats. + .. container:: verbose + + With -B/--bookmarks, the result of bookmark comparison between + local and remote repositories is displayed. With -v/--verbose, + status is also displayed for each bookmark like below:: + + BM1 01234567890a added + BM2 deleted + BM3 234567890abc advanced + BM4 34567890abcd diverged + BM5 4567890abcde changed + + The action taken when pushing depends on the + status of each bookmark: + + :``added``: push with ``-B`` will create it + :``deleted``: push with ``-B`` will delete it + :``advanced``: push will update it + :``diverged``: push with ``-B`` will update it + :``changed``: push with ``-B`` will update it + + From the point of view of pushing behavior, bookmarks + existing only in the remote repository are treated as + ``deleted``, even if it is in fact added remotely. + Returns 0 if there are outgoing changes, 1 otherwise. """ if opts.get('graph'): @@ -4734,7 +4842,7 @@ ui.warn(_("remote doesn't support bookmarks\n")) return 0 ui.status(_('comparing with %s\n') % util.hidepassword(dest)) - return bookmarks.diff(ui, other, repo) + return bookmarks.outgoing(ui, repo, other) repo._subtoppath = ui.expandpath(dest or 'default-push', dest or 'default') try: @@ -4821,19 +4929,20 @@ Returns 0 on success. """ if search: - for name, path in ui.configitems("paths"): + for name, path in sorted(ui.paths.iteritems()): if name == search: - ui.status("%s\n" % util.hidepassword(path)) + ui.status("%s\n" % util.hidepassword(path.loc)) return if not ui.quiet: ui.warn(_("not found!\n")) return 1 else: - for name, path in ui.configitems("paths"): + for name, path in sorted(ui.paths.iteritems()): if ui.quiet: ui.write("%s\n" % name) else: - ui.write("%s = %s\n" % (name, util.hidepassword(path))) + ui.write("%s = %s\n" % (name, + util.hidepassword(path.loc))) @command('phase', [('p', 'public', False, _('set changeset phase to public')), @@ -4984,9 +5093,9 @@ Returns 0 on success, 1 if an update had unresolved files. """ source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch')) + ui.status(_('pulling from %s\n') % util.hidepassword(source)) other = hg.peer(repo, opts, source) try: - ui.status(_('pulling from %s\n') % util.hidepassword(source)) revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) @@ -5098,6 +5207,9 @@ if revs: revs = [repo.lookup(r) for r in scmutil.revrange(repo, revs)] + if not revs: + raise util.Abort(_("specified revisions evaluate to an empty set"), + hint=_("use different revision arguments")) repo._subtoppath = dest try: @@ -5225,7 +5337,7 @@ ('m', 'mark', None, _('mark files as resolved')), ('u', 'unmark', None, _('mark files as unresolved')), ('n', 'no-status', None, _('hide status prefix'))] - + mergetoolopts + walkopts, + + mergetoolopts + walkopts + formatteropts, _('[OPTION]... [FILE]...'), inferrepo=True) def resolve(ui, repo, *pats, **opts): @@ -5277,11 +5389,25 @@ raise util.Abort(_('no files or directories specified'), hint=('use --all to remerge all files')) + if show: + fm = ui.formatter('resolve', opts) + ms = mergemod.mergestate(repo) + m = scmutil.match(repo[None], pats, opts) + for f in ms: + if not m(f): + continue + l = 'resolve.' + {'u': 'unresolved', 'r': 'resolved'}[ms[f]] + fm.startitem() + fm.condwrite(not nostatus, 'status', '%s ', ms[f].upper(), label=l) + fm.write('path', '%s\n', f, label=l) + fm.end() + return 0 + wlock = repo.wlock() try: ms = mergemod.mergestate(repo) - if not (ms.active() or repo.dirstate.p2() != nullid) and not show: + if not (ms.active() or repo.dirstate.p2() != nullid): raise util.Abort( _('resolve command not applicable when not merging')) @@ -5295,14 +5421,7 @@ didwork = True - if show: - if nostatus: - ui.write("%s\n" % f) - else: - ui.write("%s %s\n" % (ms[f].upper(), f), - label='resolve.' + - {'u': 'unresolved', 'r': 'resolved'}[ms[f]]) - elif mark: + if mark: ms.mark(f, "r") elif unmark: ms.mark(f, "u") @@ -5334,10 +5453,8 @@ finally: wlock.release() - # Nudge users into finishing an unfinished operation. We don't print - # this with the list/show operation because we want list/show to remain - # machine readable. - if not list(ms.unresolved()) and not show: + # Nudge users into finishing an unfinished operation + if not list(ms.unresolved()): ui.status(_('(no more unresolved files)\n')) return ret @@ -5347,6 +5464,7 @@ ('d', 'date', '', _('tipmost revision matching date'), _('DATE')), ('r', 'rev', '', _('revert to the specified revision'), _('REV')), ('C', 'no-backup', None, _('do not save backup copies of files')), + ('i', 'interactive', None, _('interactively select the changes')), ] + walkopts + dryrunopts, _('[OPTION]... [-r REV] [NAME]...')) def revert(ui, repo, *pats, **opts): @@ -5393,7 +5511,7 @@ ctx = scmutil.revsingle(repo, opts.get('rev')) - if not pats and not opts.get('all'): + if not pats and not (opts.get('all') or opts.get('interactive')): msg = _("no files or directories specified") if p2 != nullid: hint = _("uncommitted merge, use --all to discard all changes," @@ -5541,7 +5659,10 @@ if opts.get('port'): opts['port'] = util.getport(opts.get('port')) - baseui = repo and repo.baseui or ui + if repo: + baseui = repo.baseui + else: + baseui = ui optlist = ("name templates style address port prefix ipv6" " accesslog errorlog certificate encoding") for o in optlist.split(): @@ -5668,6 +5789,11 @@ hg status --rev 9353 + - show changes in the working directory relative to the + current directory (see :hg:`help patterns` for more information):: + + hg status re: + - show all changes including copies in an existing changeset:: hg status --copies --change 9353 @@ -5691,22 +5817,33 @@ else: node1, node2 = scmutil.revpair(repo, revs) - cwd = (pats and repo.getcwd()) or '' - end = opts.get('print0') and '\0' or '\n' + if pats: + cwd = repo.getcwd() + else: + cwd = '' + + if opts.get('print0'): + end = '\0' + else: + end = '\n' copy = {} states = 'modified added removed deleted unknown ignored clean'.split() show = [k for k in states if opts.get(k)] if opts.get('all'): show += ui.quiet and (states[:4] + ['clean']) or states if not show: - show = ui.quiet and states[:4] or states[:5] + if ui.quiet: + show = states[:4] + else: + show = states[:5] stat = repo.status(node1, node2, scmutil.match(repo[node2], pats, opts), 'ignored' in show, 'clean' in show, 'unknown' in show, opts.get('subrepos')) changestates = zip(states, 'MAR!?IC', stat) - if (opts.get('all') or opts.get('copies')) and not opts.get('no_status'): + if (opts.get('all') or opts.get('copies') + or ui.configbool('ui', 'statuscopies')) and not opts.get('no_status'): copy = copies.pathcopies(repo[node1], repo[node2]) fm = ui.formatter('status', opts) @@ -5939,14 +6076,11 @@ t.append(_('%d outgoing') % len(o)) other = dother or sother if 'bookmarks' in other.listkeys('namespaces'): - lmarks = repo.listkeys('bookmarks') - rmarks = other.listkeys('bookmarks') - diff = set(rmarks) - set(lmarks) - if len(diff) > 0: - t.append(_('%d incoming bookmarks') % len(diff)) - diff = set(lmarks) - set(rmarks) - if len(diff) > 0: - t.append(_('%d outgoing bookmarks') % len(diff)) + counts = bookmarks.summary(repo, other) + if counts[0] > 0: + t.append(_('%d incoming bookmarks') % counts[0]) + if counts[1] > 0: + t.append(_('%d outgoing bookmarks') % counts[1]) if t: # i18n: column positioning for "hg summary" @@ -6020,7 +6154,11 @@ rev_ = opts['rev'] message = opts.get('message') if opts.get('remove'): - expectedtype = opts.get('local') and 'local' or 'global' + if opts.get('local'): + expectedtype = 'local' + else: + expectedtype = 'global' + for n in names: if not repo.tagtype(n): raise util.Abort(_("tag '%s' does not exist") % n) @@ -6250,9 +6388,7 @@ rev = cmdutil.finddate(ui, repo, date) if check: - c = repo[None] - if c.dirty(merge=False, branch=False, missing=True): - raise util.Abort(_("uncommitted changes")) + cmdutil.bailifchanged(repo, merge=False) if rev is None: rev = repo[repo[None].branch()].rev() @@ -6303,7 +6439,7 @@ % util.version()) ui.status(_( "(see http://mercurial.selenic.com for more information)\n" - "\nCopyright (C) 2005-2014 Matt Mackall and others\n" + "\nCopyright (C) 2005-2015 Matt Mackall and others\n" "This is free software; see the source for copying conditions. " "There is NO\nwarranty; " "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n" diff -r 612ed41ae359 -r e89f909edffa mercurial/context.py --- a/mercurial/context.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/context.py Thu Apr 16 20:57:51 2015 -0500 @@ -66,8 +66,7 @@ return self.filectx(key) def __iter__(self): - for f in sorted(self._manifest): - yield f + return iter(self._manifest) def _manifestmatches(self, match, s): """generate a new manifest filtered by the match argument @@ -153,6 +152,8 @@ return hex(self.node()) def manifest(self): return self._manifest + def repo(self): + return self._repo def phasestr(self): return phases.phasenames[self.phase()] def mutable(self): @@ -265,12 +266,11 @@ diffopts = patch.diffopts(self._repo.ui, opts) return patch.diff(self._repo, ctx2, self, match=match, opts=diffopts) - @propertycache - def _dirs(self): - return scmutil.dirs(self._manifest) + def dirs(self): + return self._manifest.dirs() - def dirs(self): - return self._dirs + def hasdir(self, dir): + return self._manifest.hasdir(dir) def dirty(self, missing=False, merge=True, branch=True): return False @@ -376,10 +376,6 @@ return if isinstance(changeid, long): changeid = str(changeid) - if changeid == '.': - self._node = repo.dirstate.p1() - self._rev = repo.changelog.rev(self._node) - return if changeid == 'null': self._node = nullid self._rev = nullrev @@ -388,6 +384,12 @@ self._node = repo.changelog.tip() self._rev = repo.changelog.rev(self._node) return + if changeid == '.' or changeid == repo.dirstate.p1(): + # this is a hack to delay/avoid loading obsmarkers + # when we know that '.' won't be hidden + self._node = repo.dirstate.p1() + self._rev = repo.unfiltered().changelog.rev(self._node) + return if len(changeid) == 20: try: self._node = changeid @@ -585,30 +587,15 @@ return self._repo.changelog.descendant(self._rev, other._rev) def walk(self, match): - fset = set(match.files()) - # for dirstate.walk, files=['.'] means "walk the whole tree". - # follow that here, too - fset.discard('.') + '''Generates matching file names.''' - # avoid the entire walk if we're only looking for specific files - if fset and not match.anypats(): - if util.all([fn in self for fn in fset]): - for fn in sorted(fset): - if match(fn): - yield fn - raise StopIteration + # Override match.bad method to have message with nodeid + oldbad = match.bad + def bad(fn, msg): + oldbad(fn, _('no such file in rev %s') % self) + match.bad = bad - for fn in self: - if fn in fset: - # specified pattern is the exact name - fset.remove(fn) - if match(fn): - yield fn - for fn in sorted(fset): - if fn in self._dirs: - # specified pattern is a directory - continue - match.bad(fn, _('no such file in rev %s') % self) + return self._manifest.walk(match) def matches(self, match): return self.walk(match) @@ -722,6 +709,8 @@ return self._changectx.manifest() def changectx(self): return self._changectx + def repo(self): + return self._repo def path(self): return self._path @@ -752,7 +741,7 @@ return True def _adjustlinkrev(self, path, filelog, fnode, srcrev, inclusive=False): - """return the first ancestor of introducting + """return the first ancestor of introducing If the linkrev of the file revision does not point to an ancestor of srcrev, we'll walk down the ancestors until we find one introducing @@ -830,7 +819,7 @@ # be replaced with the rename information. This parent is -always- # the first one. # - # As null id have alway been filtered out in the previous list + # As null id have always been filtered out in the previous list # comprehension, inserting to 0 will always result in "replacing # first nullid parent with rename information. pl.insert(0, (r[0], r[1], self._repo.file(r[0]))) @@ -919,7 +908,7 @@ introrev = self.introrev() if self.rev() != introrev: base = self.filectx(self.filenode(), changeid=introrev) - if getattr(base, '_ancestrycontext', None) is None: + if introrev and getattr(base, '_ancestrycontext', None) is None: ac = self._repo.changelog.ancestors([introrev], inclusive=True) base._ancestrycontext = ac @@ -969,7 +958,11 @@ def ancestors(self, followfirst=False): visit = {} c = self - cut = followfirst and 1 or None + if followfirst: + cut = 1 + else: + cut = None + while True: for parent in c.parents()[:cut]: visit[(parent.linkrev(), parent.filenode())] = parent @@ -1199,6 +1192,8 @@ def subrev(self, subpath): return None + def manifestnode(self): + return None def user(self): return self._user or self._repo.ui.username() def date(self): @@ -1265,6 +1260,7 @@ return self._parents[0].ancestor(c2) # punt on two parents for now def walk(self, match): + '''Generates matching file names.''' return sorted(self._repo.dirstate.walk(match, sorted(self.substate), True, False)) @@ -1296,9 +1292,6 @@ self._repo.dirstate.setparents(node) self._repo.dirstate.endparentchange() - def dirs(self): - return self._repo.dirstate.dirs() - class workingctx(committablectx): """A workingctx object makes access to data related to the current working directory convenient. @@ -1434,6 +1427,18 @@ finally: wlock.release() + def match(self, pats=[], include=None, exclude=None, default='glob'): + r = self._repo + + # Only a case insensitive filesystem needs magic to translate user input + # to actual case in the filesystem. + if not util.checkcase(r.root): + return matchmod.icasefsmatcher(r.root, r.getcwd(), pats, include, + exclude, default, r.auditor, self) + return matchmod.match(r.root, r.getcwd(), pats, + include, exclude, default, + auditor=r.auditor, ctx=self) + def _filtersuspectsymlink(self, files): if not files or self._repo.dirstate._checklink: return files @@ -1570,7 +1575,7 @@ def bad(f, msg): # 'f' may be a directory pattern from 'match.files()', # so 'f not in ctx1' is not enough - if f not in other and f not in other.dirs(): + if f not in other and not other.hasdir(f): self._repo.ui.warn('%s: %s\n' % (self._repo.dirstate.pathto(f), msg)) match.bad = bad @@ -1593,6 +1598,10 @@ def __nonzero__(self): return True + def linkrev(self): + # linked to self._changectx no matter if file is modified or not + return self.rev() + def parents(self): '''return parent filectxs, following copies if necessary''' def filenode(ctx, path): @@ -1764,7 +1773,11 @@ # "filectxfn" for performance (e.g. converting from another VCS) self._filectxfn = util.cachefunc(filectxfn) - self._extra = extra and extra.copy() or {} + if extra: + self._extra = extra.copy() + else: + self._extra = {} + if self._extra.get('branch', '') == '': self._extra['branch'] = 'default' diff -r 612ed41ae359 -r e89f909edffa mercurial/copies.py --- a/mercurial/copies.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/copies.py Thu Apr 16 20:57:51 2015 -0500 @@ -8,10 +8,6 @@ import util import heapq -def _nonoverlap(d1, d2, d3): - "Return list of elements in d1 not in d2 or d3" - return sorted([d for d in d1 if d not in d3 and d not in d2]) - def _dirname(f): s = f.rfind("/") if s == -1: @@ -144,7 +140,19 @@ del c[k] return c -def _forwardcopies(a, b): +def _computeforwardmissing(a, b, match=None): + """Computes which files are in b but not a. + This is its own function so extensions can easily wrap this call to see what + files _forwardcopies is about to process. + """ + ma = a.manifest() + mb = b.manifest() + if match: + ma = ma.matches(match) + mb = mb.matches(match) + return mb.filesnotin(ma) + +def _forwardcopies(a, b, match=None): '''find {dst@b: src@a} copy mapping where a is an ancestor of b''' # check for working copy @@ -167,9 +175,7 @@ # we currently don't try to find where old files went, too expensive # this means we can miss a case like 'hg rm b; hg cp a b' cm = {} - missing = set(b.manifest().iterkeys()) - missing.difference_update(a.manifest().iterkeys()) - + missing = _computeforwardmissing(a, b, match=match) ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True) for f in missing: fctx = b[f] @@ -197,16 +203,36 @@ r[v] = k return r -def pathcopies(x, y): +def pathcopies(x, y, match=None): '''find {dst@y: src@x} copy mapping for directed compare''' if x == y or not x or not y: return {} a = y.ancestor(x) if a == x: - return _forwardcopies(x, y) + return _forwardcopies(x, y, match=match) if a == y: return _backwardrenames(x, y) - return _chain(x, y, _backwardrenames(x, a), _forwardcopies(a, y)) + return _chain(x, y, _backwardrenames(x, a), + _forwardcopies(a, y, match=match)) + +def _computenonoverlap(repo, c1, c2, addedinm1, addedinm2): + """Computes, based on addedinm1 and addedinm2, the files exclusive to c1 + and c2. This is its own function so extensions can easily wrap this call + to see what files mergecopies is about to process. + + Even though c1 and c2 are not used in this function, they are useful in + other extensions for being able to read the file nodes of the changed files. + """ + u1 = sorted(addedinm1 - addedinm2) + u2 = sorted(addedinm2 - addedinm1) + + if u1: + repo.ui.debug(" unmatched files in local:\n %s\n" + % "\n ".join(u1)) + if u2: + repo.ui.debug(" unmatched files in other:\n %s\n" + % "\n ".join(u2)) + return u1, u2 def mergecopies(repo, c1, c2, ca): """ @@ -288,15 +314,9 @@ repo.ui.debug(" searching for copies back to rev %d\n" % limit) - u1 = _nonoverlap(m1, m2, ma) - u2 = _nonoverlap(m2, m1, ma) - - if u1: - repo.ui.debug(" unmatched files in local:\n %s\n" - % "\n ".join(u1)) - if u2: - repo.ui.debug(" unmatched files in other:\n %s\n" - % "\n ".join(u2)) + addedinm1 = m1.filesnotin(ma) + addedinm2 = m2.filesnotin(ma) + u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2) for f in u1: ctx = setupctx(c1) @@ -320,7 +340,7 @@ else: diverge2.update(fl) # reverse map for below - bothnew = sorted([d for d in m1 if d in m2 and d not in ma]) + bothnew = sorted(addedinm1 & addedinm2) if bothnew: repo.ui.debug(" unmatched files new in both:\n %s\n" % "\n ".join(bothnew)) diff -r 612ed41ae359 -r e89f909edffa mercurial/crecord.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/crecord.py Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,1597 @@ +# stuff related specifically to patch manipulation / parsing +# +# Copyright 2008 Mark Edgington +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +# +# This code is based on the Mark Edgington's crecord extension. +# (Itself based on Bryan O'Sullivan's record extension.) + +from i18n import _ +import patch as patchmod +import util, encoding + +import os, re, sys, struct, signal, tempfile, locale, cStringIO + +# This is required for ncurses to display non-ASCII characters in default user +# locale encoding correctly. --immerrr +locale.setlocale(locale.LC_ALL, '') + +# os.name is one of: 'posix', 'nt', 'dos', 'os2', 'mac', or 'ce' +if os.name == 'posix': + import curses, fcntl, termios +else: + # I have no idea if wcurses works with crecord... + try: + import wcurses as curses + except ImportError: + # wcurses is not shipped on Windows by default + pass + +try: + curses +except NameError: + if os.name != 'nt': # Temporary hack to get running on Windows again + raise util.Abort( + _('the python curses/wcurses module is not available/installed')) + +_origstdout = sys.__stdout__ # used by gethw() + +class patchnode(object): + """abstract class for patch graph nodes + (i.e. patchroot, header, hunk, hunkline) + """ + + def firstchild(self): + raise NotImplementedError("method must be implemented by subclass") + + def lastchild(self): + raise NotImplementedError("method must be implemented by subclass") + + def allchildren(self): + "Return a list of all of the direct children of this node" + raise NotImplementedError("method must be implemented by subclass") + def nextsibling(self): + """ + Return the closest next item of the same type where there are no items + of different types between the current item and this closest item. + If no such item exists, return None. + + """ + raise NotImplementedError("method must be implemented by subclass") + + def prevsibling(self): + """ + Return the closest previous item of the same type where there are no + items of different types between the current item and this closest item. + If no such item exists, return None. + + """ + raise NotImplementedError("method must be implemented by subclass") + + def parentitem(self): + raise NotImplementedError("method must be implemented by subclass") + + + def nextitem(self, constrainlevel=True, skipfolded=True): + """ + If constrainLevel == True, return the closest next item + of the same type where there are no items of different types between + the current item and this closest item. + + If constrainLevel == False, then try to return the next item + closest to this item, regardless of item's type (header, hunk, or + HunkLine). + + If skipFolded == True, and the current item is folded, then the child + items that are hidden due to folding will be skipped when determining + the next item. + + If it is not possible to get the next item, return None. + + """ + try: + itemfolded = self.folded + except AttributeError: + itemfolded = False + if constrainlevel: + return self.nextsibling() + elif skipfolded and itemfolded: + nextitem = self.nextsibling() + if nextitem is None: + try: + nextitem = self.parentitem().nextsibling() + except AttributeError: + nextitem = None + return nextitem + else: + # try child + item = self.firstchild() + if item is not None: + return item + + # else try next sibling + item = self.nextsibling() + if item is not None: + return item + + try: + # else try parent's next sibling + item = self.parentitem().nextsibling() + if item is not None: + return item + + # else return grandparent's next sibling (or None) + return self.parentitem().parentitem().nextsibling() + + except AttributeError: # parent and/or grandparent was None + return None + + def previtem(self, constrainlevel=True, skipfolded=True): + """ + If constrainLevel == True, return the closest previous item + of the same type where there are no items of different types between + the current item and this closest item. + + If constrainLevel == False, then try to return the previous item + closest to this item, regardless of item's type (header, hunk, or + HunkLine). + + If skipFolded == True, and the current item is folded, then the items + that are hidden due to folding will be skipped when determining the + next item. + + If it is not possible to get the previous item, return None. + + """ + if constrainlevel: + return self.prevsibling() + else: + # try previous sibling's last child's last child, + # else try previous sibling's last child, else try previous sibling + prevsibling = self.prevsibling() + if prevsibling is not None: + prevsiblinglastchild = prevsibling.lastchild() + if ((prevsiblinglastchild is not None) and + not prevsibling.folded): + prevsiblinglclc = prevsiblinglastchild.lastchild() + if ((prevsiblinglclc is not None) and + not prevsiblinglastchild.folded): + return prevsiblinglclc + else: + return prevsiblinglastchild + else: + return prevsibling + + # try parent (or None) + return self.parentitem() + +class patch(patchnode, list): # todo: rename patchroot + """ + list of header objects representing the patch. + + """ + def __init__(self, headerlist): + self.extend(headerlist) + # add parent patch object reference to each header + for header in self: + header.patch = self + +class uiheader(patchnode): + """patch header + + xxx shoudn't we move this to mercurial/patch.py ? + """ + + def __init__(self, header): + self.nonuiheader = header + # flag to indicate whether to apply this chunk + self.applied = True + # flag which only affects the status display indicating if a node's + # children are partially applied (i.e. some applied, some not). + self.partial = False + + # flag to indicate whether to display as folded/unfolded to user + self.folded = True + + # list of all headers in patch + self.patch = None + + # flag is False if this header was ever unfolded from initial state + self.neverunfolded = True + self.hunks = [uihunk(h, self) for h in self.hunks] + + + def prettystr(self): + x = cStringIO.StringIO() + self.pretty(x) + return x.getvalue() + + def nextsibling(self): + numheadersinpatch = len(self.patch) + indexofthisheader = self.patch.index(self) + + if indexofthisheader < numheadersinpatch - 1: + nextheader = self.patch[indexofthisheader + 1] + return nextheader + else: + return None + + def prevsibling(self): + indexofthisheader = self.patch.index(self) + if indexofthisheader > 0: + previousheader = self.patch[indexofthisheader - 1] + return previousheader + else: + return None + + def parentitem(self): + """ + there is no 'real' parent item of a header that can be selected, + so return None. + """ + return None + + def firstchild(self): + "return the first child of this item, if one exists. otherwise None." + if len(self.hunks) > 0: + return self.hunks[0] + else: + return None + + def lastchild(self): + "return the last child of this item, if one exists. otherwise None." + if len(self.hunks) > 0: + return self.hunks[-1] + else: + return None + + def allchildren(self): + "return a list of all of the direct children of this node" + return self.hunks + + def __getattr__(self, name): + return getattr(self.nonuiheader, name) + +class uihunkline(patchnode): + "represents a changed line in a hunk" + def __init__(self, linetext, hunk): + self.linetext = linetext + self.applied = True + # the parent hunk to which this line belongs + self.hunk = hunk + # folding lines currently is not used/needed, but this flag is needed + # in the previtem method. + self.folded = False + + def prettystr(self): + return self.linetext + + def nextsibling(self): + numlinesinhunk = len(self.hunk.changedlines) + indexofthisline = self.hunk.changedlines.index(self) + + if (indexofthisline < numlinesinhunk - 1): + nextline = self.hunk.changedlines[indexofthisline + 1] + return nextline + else: + return None + + def prevsibling(self): + indexofthisline = self.hunk.changedlines.index(self) + if indexofthisline > 0: + previousline = self.hunk.changedlines[indexofthisline - 1] + return previousline + else: + return None + + def parentitem(self): + "return the parent to the current item" + return self.hunk + + def firstchild(self): + "return the first child of this item, if one exists. otherwise None." + # hunk-lines don't have children + return None + + def lastchild(self): + "return the last child of this item, if one exists. otherwise None." + # hunk-lines don't have children + return None + +class uihunk(patchnode): + """ui patch hunk, wraps a hunk and keep track of ui behavior """ + maxcontext = 3 + + def __init__(self, hunk, header): + self._hunk = hunk + self.changedlines = [uihunkline(line, self) for line in hunk.hunk] + self.header = header + # used at end for detecting how many removed lines were un-applied + self.originalremoved = self.removed + + # flag to indicate whether to display as folded/unfolded to user + self.folded = True + # flag to indicate whether to apply this chunk + self.applied = True + # flag which only affects the status display indicating if a node's + # children are partially applied (i.e. some applied, some not). + self.partial = False + + def nextsibling(self): + numhunksinheader = len(self.header.hunks) + indexofthishunk = self.header.hunks.index(self) + + if (indexofthishunk < numhunksinheader - 1): + nexthunk = self.header.hunks[indexofthishunk + 1] + return nexthunk + else: + return None + + def prevsibling(self): + indexofthishunk = self.header.hunks.index(self) + if indexofthishunk > 0: + previoushunk = self.header.hunks[indexofthishunk - 1] + return previoushunk + else: + return None + + def parentitem(self): + "return the parent to the current item" + return self.header + + def firstchild(self): + "return the first child of this item, if one exists. otherwise None." + if len(self.changedlines) > 0: + return self.changedlines[0] + else: + return None + + def lastchild(self): + "return the last child of this item, if one exists. otherwise None." + if len(self.changedlines) > 0: + return self.changedlines[-1] + else: + return None + + def allchildren(self): + "return a list of all of the direct children of this node" + return self.changedlines + def countchanges(self): + """changedlines -> (n+,n-)""" + add = len([l for l in self.changedlines if l.applied + and l.prettystr()[0] == '+']) + rem = len([l for l in self.changedlines if l.applied + and l.prettystr()[0] == '-']) + return add, rem + + def getfromtoline(self): + # calculate the number of removed lines converted to context lines + removedconvertedtocontext = self.originalremoved - self.removed + + contextlen = (len(self.before) + len(self.after) + + removedconvertedtocontext) + if self.after and self.after[-1] == '\\ no newline at end of file\n': + contextlen -= 1 + fromlen = contextlen + self.removed + tolen = contextlen + self.added + + # diffutils manual, section "2.2.2.2 detailed description of unified + # format": "an empty hunk is considered to end at the line that + # precedes the hunk." + # + # so, if either of hunks is empty, decrease its line start. --immerrr + # but only do this if fromline > 0, to avoid having, e.g fromline=-1. + fromline, toline = self.fromline, self.toline + if fromline != 0: + if fromlen == 0: + fromline -= 1 + if tolen == 0: + toline -= 1 + + fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % ( + fromline, fromlen, toline, tolen, + self.proc and (' ' + self.proc)) + return fromtoline + + def write(self, fp): + # updated self.added/removed, which are used by getfromtoline() + self.added, self.removed = self.countchanges() + fp.write(self.getfromtoline()) + + hunklinelist = [] + # add the following to the list: (1) all applied lines, and + # (2) all unapplied removal lines (convert these to context lines) + for changedline in self.changedlines: + changedlinestr = changedline.prettystr() + if changedline.applied: + hunklinelist.append(changedlinestr) + elif changedlinestr[0] == "-": + hunklinelist.append(" " + changedlinestr[1:]) + + fp.write(''.join(self.before + hunklinelist + self.after)) + + pretty = write + + def prettystr(self): + x = cStringIO.StringIO() + self.pretty(x) + return x.getvalue() + + def __getattr__(self, name): + return getattr(self._hunk, name) + def __repr__(self): + return '' % (self.filename(), self.fromline) + +def filterpatch(ui, chunks, chunkselector): + """interactively filter patch chunks into applied-only chunks""" + + chunks = list(chunks) + # convert chunks list into structure suitable for displaying/modifying + # with curses. create a list of headers only. + headers = [c for c in chunks if isinstance(c, patchmod.header)] + + # if there are no changed files + if len(headers) == 0: + return [] + uiheaders = [uiheader(h) for h in headers] + # let user choose headers/hunks/lines, and mark their applied flags + # accordingly + chunkselector(ui, uiheaders) + appliedhunklist = [] + for hdr in uiheaders: + if (hdr.applied and + (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)): + appliedhunklist.append(hdr) + fixoffset = 0 + for hnk in hdr.hunks: + if hnk.applied: + appliedhunklist.append(hnk) + # adjust the 'to'-line offset of the hunk to be correct + # after de-activating some of the other hunks for this file + if fixoffset: + #hnk = copy.copy(hnk) # necessary?? + hnk.toline += fixoffset + else: + fixoffset += hnk.removed - hnk.added + + return appliedhunklist + +def gethw(): + """ + magically get the current height and width of the window (without initscr) + + this is a rip-off of a rip-off - taken from the bpython code. it is + useful / necessary because otherwise curses.initscr() must be called, + which can leave the terminal in a nasty state after exiting. + + """ + h, w = struct.unpack( + "hhhh", fcntl.ioctl(_origstdout, termios.TIOCGWINSZ, "\000"*8))[0:2] + return h, w + +def chunkselector(ui, headerlist): + """ + curses interface to get selection of chunks, and mark the applied flags + of the chosen chunks. + + """ + ui.write(_('starting interactive selection\n')) + chunkselector = curseschunkselector(headerlist, ui) + curses.wrapper(chunkselector.main) + +def testdecorator(testfn, f): + def u(*args, **kwargs): + return f(testfn, *args, **kwargs) + return u + +def testchunkselector(testfn, ui, headerlist): + """ + test interface to get selection of chunks, and mark the applied flags + of the chosen chunks. + + """ + chunkselector = curseschunkselector(headerlist, ui) + if testfn and os.path.exists(testfn): + testf = open(testfn) + testcommands = map(lambda x: x.rstrip('\n'), testf.readlines()) + testf.close() + while True: + if chunkselector.handlekeypressed(testcommands.pop(0), test=True): + break + +class curseschunkselector(object): + def __init__(self, headerlist, ui): + # put the headers into a patch object + self.headerlist = patch(headerlist) + + self.ui = ui + + # list of all chunks + self.chunklist = [] + for h in headerlist: + self.chunklist.append(h) + self.chunklist.extend(h.hunks) + + # dictionary mapping (fgcolor, bgcolor) pairs to the + # corresponding curses color-pair value. + self.colorpairs = {} + # maps custom nicknames of color-pairs to curses color-pair values + self.colorpairnames = {} + + # the currently selected header, hunk, or hunk-line + self.currentselecteditem = self.headerlist[0] + + # updated when printing out patch-display -- the 'lines' here are the + # line positions *in the pad*, not on the screen. + self.selecteditemstartline = 0 + self.selecteditemendline = None + + # define indentation levels + self.headerindentnumchars = 0 + self.hunkindentnumchars = 3 + self.hunklineindentnumchars = 6 + + # the first line of the pad to print to the screen + self.firstlineofpadtoprint = 0 + + # keeps track of the number of lines in the pad + self.numpadlines = None + + self.numstatuslines = 2 + + # keep a running count of the number of lines printed to the pad + # (used for determining when the selected item begins/ends) + self.linesprintedtopadsofar = 0 + + # the first line of the pad which is visible on the screen + self.firstlineofpadtoprint = 0 + + # stores optional text for a commit comment provided by the user + self.commenttext = "" + + # if the last 'toggle all' command caused all changes to be applied + self.waslasttoggleallapplied = True + + def uparrowevent(self): + """ + try to select the previous item to the current item that has the + most-indented level. for example, if a hunk is selected, try to select + the last hunkline of the hunk prior to the selected hunk. or, if + the first hunkline of a hunk is currently selected, then select the + hunk itself. + + if the currently selected item is already at the top of the screen, + scroll the screen down to show the new-selected item. + + """ + currentitem = self.currentselecteditem + + nextitem = currentitem.previtem(constrainlevel=False) + + if nextitem is None: + # if no parent item (i.e. currentitem is the first header), then + # no change... + nextitem = currentitem + + self.currentselecteditem = nextitem + + def uparrowshiftevent(self): + """ + select (if possible) the previous item on the same level as the + currently selected item. otherwise, select (if possible) the + parent-item of the currently selected item. + + if the currently selected item is already at the top of the screen, + scroll the screen down to show the new-selected item. + + """ + currentitem = self.currentselecteditem + nextitem = currentitem.previtem() + # if there's no previous item on this level, try choosing the parent + if nextitem is None: + nextitem = currentitem.parentitem() + if nextitem is None: + # if no parent item (i.e. currentitem is the first header), then + # no change... + nextitem = currentitem + + self.currentselecteditem = nextitem + + def downarrowevent(self): + """ + try to select the next item to the current item that has the + most-indented level. for example, if a hunk is selected, select + the first hunkline of the selected hunk. or, if the last hunkline of + a hunk is currently selected, then select the next hunk, if one exists, + or if not, the next header if one exists. + + if the currently selected item is already at the bottom of the screen, + scroll the screen up to show the new-selected item. + + """ + #self.startprintline += 1 #debug + currentitem = self.currentselecteditem + + nextitem = currentitem.nextitem(constrainlevel=False) + # if there's no next item, keep the selection as-is + if nextitem is None: + nextitem = currentitem + + self.currentselecteditem = nextitem + + def downarrowshiftevent(self): + """ + if the cursor is already at the bottom chunk, scroll the screen up and + move the cursor-position to the subsequent chunk. otherwise, only move + the cursor position down one chunk. + + """ + # todo: update docstring + + currentitem = self.currentselecteditem + nextitem = currentitem.nextitem() + # if there's no previous item on this level, try choosing the parent's + # nextitem. + if nextitem is None: + try: + nextitem = currentitem.parentitem().nextitem() + except AttributeError: + # parentitem returned None, so nextitem() can't be called + nextitem = None + if nextitem is None: + # if no next item on parent-level, then no change... + nextitem = currentitem + + self.currentselecteditem = nextitem + + def rightarrowevent(self): + """ + select (if possible) the first of this item's child-items. + + """ + currentitem = self.currentselecteditem + nextitem = currentitem.firstchild() + + # turn off folding if we want to show a child-item + if currentitem.folded: + self.togglefolded(currentitem) + + if nextitem is None: + # if no next item on parent-level, then no change... + nextitem = currentitem + + self.currentselecteditem = nextitem + + def leftarrowevent(self): + """ + if the current item can be folded (i.e. it is an unfolded header or + hunk), then fold it. otherwise try select (if possible) the parent + of this item. + + """ + currentitem = self.currentselecteditem + + # try to fold the item + if not isinstance(currentitem, uihunkline): + if not currentitem.folded: + self.togglefolded(item=currentitem) + return + + # if it can't be folded, try to select the parent item + nextitem = currentitem.parentitem() + + if nextitem is None: + # if no item on parent-level, then no change... + nextitem = currentitem + if not nextitem.folded: + self.togglefolded(item=nextitem) + + self.currentselecteditem = nextitem + + def leftarrowshiftevent(self): + """ + select the header of the current item (or fold current item if the + current item is already a header). + + """ + currentitem = self.currentselecteditem + + if isinstance(currentitem, uiheader): + if not currentitem.folded: + self.togglefolded(item=currentitem) + return + + # select the parent item recursively until we're at a header + while True: + nextitem = currentitem.parentitem() + if nextitem is None: + break + else: + currentitem = nextitem + + self.currentselecteditem = currentitem + + def updatescroll(self): + "scroll the screen to fully show the currently-selected" + selstart = self.selecteditemstartline + selend = self.selecteditemendline + #selnumlines = selend - selstart + padstart = self.firstlineofpadtoprint + padend = padstart + self.yscreensize - self.numstatuslines - 1 + # 'buffered' pad start/end values which scroll with a certain + # top/bottom context margin + padstartbuffered = padstart + 3 + padendbuffered = padend - 3 + + if selend > padendbuffered: + self.scrolllines(selend - padendbuffered) + elif selstart < padstartbuffered: + # negative values scroll in pgup direction + self.scrolllines(selstart - padstartbuffered) + + + def scrolllines(self, numlines): + "scroll the screen up (down) by numlines when numlines >0 (<0)." + self.firstlineofpadtoprint += numlines + if self.firstlineofpadtoprint < 0: + self.firstlineofpadtoprint = 0 + if self.firstlineofpadtoprint > self.numpadlines - 1: + self.firstlineofpadtoprint = self.numpadlines - 1 + + def toggleapply(self, item=None): + """ + toggle the applied flag of the specified item. if no item is specified, + toggle the flag of the currently selected item. + + """ + if item is None: + item = self.currentselecteditem + + item.applied = not item.applied + + if isinstance(item, uiheader): + item.partial = False + if item.applied: + # apply all its hunks + for hnk in item.hunks: + hnk.applied = True + # apply all their hunklines + for hunkline in hnk.changedlines: + hunkline.applied = True + else: + # un-apply all its hunks + for hnk in item.hunks: + hnk.applied = False + hnk.partial = False + # un-apply all their hunklines + for hunkline in hnk.changedlines: + hunkline.applied = False + elif isinstance(item, uihunk): + item.partial = False + # apply all it's hunklines + for hunkline in item.changedlines: + hunkline.applied = item.applied + + siblingappliedstatus = [hnk.applied for hnk in item.header.hunks] + allsiblingsapplied = not (False in siblingappliedstatus) + nosiblingsapplied = not (True in siblingappliedstatus) + + siblingspartialstatus = [hnk.partial for hnk in item.header.hunks] + somesiblingspartial = (True in siblingspartialstatus) + + #cases where applied or partial should be removed from header + + # if no 'sibling' hunks are applied (including this hunk) + if nosiblingsapplied: + if not item.header.special(): + item.header.applied = False + item.header.partial = False + else: # some/all parent siblings are applied + item.header.applied = True + item.header.partial = (somesiblingspartial or + not allsiblingsapplied) + + elif isinstance(item, uihunkline): + siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines] + allsiblingsapplied = not (False in siblingappliedstatus) + nosiblingsapplied = not (True in siblingappliedstatus) + + # if no 'sibling' lines are applied + if nosiblingsapplied: + item.hunk.applied = False + item.hunk.partial = False + elif allsiblingsapplied: + item.hunk.applied = True + item.hunk.partial = False + else: # some siblings applied + item.hunk.applied = True + item.hunk.partial = True + + parentsiblingsapplied = [hnk.applied for hnk + in item.hunk.header.hunks] + noparentsiblingsapplied = not (True in parentsiblingsapplied) + allparentsiblingsapplied = not (False in parentsiblingsapplied) + + parentsiblingspartial = [hnk.partial for hnk + in item.hunk.header.hunks] + someparentsiblingspartial = (True in parentsiblingspartial) + + # if all parent hunks are not applied, un-apply header + if noparentsiblingsapplied: + if not item.hunk.header.special(): + item.hunk.header.applied = False + item.hunk.header.partial = False + # set the applied and partial status of the header if needed + else: # some/all parent siblings are applied + item.hunk.header.applied = True + item.hunk.header.partial = (someparentsiblingspartial or + not allparentsiblingsapplied) + + def toggleall(self): + "toggle the applied flag of all items." + if self.waslasttoggleallapplied: # then unapply them this time + for item in self.headerlist: + if item.applied: + self.toggleapply(item) + else: + for item in self.headerlist: + if not item.applied: + self.toggleapply(item) + self.waslasttoggleallapplied = not self.waslasttoggleallapplied + + def togglefolded(self, item=None, foldparent=False): + "toggle folded flag of specified item (defaults to currently selected)" + if item is None: + item = self.currentselecteditem + if foldparent or (isinstance(item, uiheader) and item.neverunfolded): + if not isinstance(item, uiheader): + # we need to select the parent item in this case + self.currentselecteditem = item = item.parentitem() + elif item.neverunfolded: + item.neverunfolded = False + + # also fold any foldable children of the parent/current item + if isinstance(item, uiheader): # the original or 'new' item + for child in item.allchildren(): + child.folded = not item.folded + + if isinstance(item, (uiheader, uihunk)): + item.folded = not item.folded + + + def alignstring(self, instr, window): + """ + add whitespace to the end of a string in order to make it fill + the screen in the x direction. the current cursor position is + taken into account when making this calculation. the string can span + multiple lines. + + """ + y, xstart = window.getyx() + width = self.xscreensize + # turn tabs into spaces + instr = instr.expandtabs(4) + strwidth = encoding.colwidth(instr) + numspaces = (width - ((strwidth + xstart) % width) - 1) + return instr + " " * numspaces + "\n" + + def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None, + pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False): + """ + print the string, text, with the specified colors and attributes, to + the specified curses window object. + + the foreground and background colors are of the form + curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green, + magenta, red, white, yellow]. if pairname is provided, a color + pair will be looked up in the self.colorpairnames dictionary. + + attrlist is a list containing text attributes in the form of + curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout, + underline]. + + if align == True, whitespace is added to the printed string such that + the string stretches to the right border of the window. + + if showwhtspc == True, trailing whitespace of a string is highlighted. + + """ + # preprocess the text, converting tabs to spaces + text = text.expandtabs(4) + # strip \n, and convert control characters to ^[char] representation + text = re.sub(r'[\x00-\x08\x0a-\x1f]', + lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n')) + + if pair is not None: + colorpair = pair + elif pairname is not None: + colorpair = self.colorpairnames[pairname] + else: + if fgcolor is None: + fgcolor = -1 + if bgcolor is None: + bgcolor = -1 + if (fgcolor, bgcolor) in self.colorpairs: + colorpair = self.colorpairs[(fgcolor, bgcolor)] + else: + colorpair = self.getcolorpair(fgcolor, bgcolor) + # add attributes if possible + if attrlist is None: + attrlist = [] + if colorpair < 256: + # then it is safe to apply all attributes + for textattr in attrlist: + colorpair |= textattr + else: + # just apply a select few (safe?) attributes + for textattr in (curses.A_UNDERLINE, curses.A_BOLD): + if textattr in attrlist: + colorpair |= textattr + + y, xstart = self.chunkpad.getyx() + t = "" # variable for counting lines printed + # if requested, show trailing whitespace + if showwhtspc: + origlen = len(text) + text = text.rstrip(' \n') # tabs have already been expanded + strippedlen = len(text) + numtrailingspaces = origlen - strippedlen + + if towin: + window.addstr(text, colorpair) + t += text + + if showwhtspc: + wscolorpair = colorpair | curses.A_REVERSE + if towin: + for i in range(numtrailingspaces): + window.addch(curses.ACS_CKBOARD, wscolorpair) + t += " " * numtrailingspaces + + if align: + if towin: + extrawhitespace = self.alignstring("", window) + window.addstr(extrawhitespace, colorpair) + else: + # need to use t, since the x position hasn't incremented + extrawhitespace = self.alignstring(t, window) + t += extrawhitespace + + # is reset to 0 at the beginning of printitem() + + linesprinted = (xstart + len(t)) / self.xscreensize + self.linesprintedtopadsofar += linesprinted + return t + + def updatescreen(self): + self.statuswin.erase() + self.chunkpad.erase() + + printstring = self.printstring + + # print out the status lines at the top + try: + printstring(self.statuswin, + "SELECT CHUNKS: (j/k/up/dn/pgup/pgdn) move cursor; " + "(space/A) toggle hunk/all; (e)dit hunk;", + pairname="legend") + printstring(self.statuswin, + " (f)old/unfold; (c)ommit applied; (q)uit; (?) help " + "| [X]=hunk applied **=folded", + pairname="legend") + except curses.error: + pass + + # print out the patch in the remaining part of the window + try: + self.printitem() + self.updatescroll() + self.chunkpad.refresh(self.firstlineofpadtoprint, 0, + self.numstatuslines, 0, + self.yscreensize + 1 - self.numstatuslines, + self.xscreensize) + except curses.error: + pass + + # refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol]) + self.statuswin.refresh() + + def getstatusprefixstring(self, item): + """ + create a string to prefix a line with which indicates whether 'item' + is applied and/or folded. + + """ + # create checkbox string + if item.applied: + if not isinstance(item, uihunkline) and item.partial: + checkbox = "[~]" + else: + checkbox = "[x]" + else: + checkbox = "[ ]" + + try: + if item.folded: + checkbox += "**" + if isinstance(item, uiheader): + # one of "m", "a", or "d" (modified, added, deleted) + filestatus = item.changetype + + checkbox += filestatus + " " + else: + checkbox += " " + if isinstance(item, uiheader): + # add two more spaces for headers + checkbox += " " + except AttributeError: # not foldable + checkbox += " " + + return checkbox + + def printheader(self, header, selected=False, towin=True, + ignorefolding=False): + """ + print the header to the pad. if countlines is True, don't print + anything, but just count the number of lines which would be printed. + + """ + outstr = "" + text = header.prettystr() + chunkindex = self.chunklist.index(header) + + if chunkindex != 0 and not header.folded: + # add separating line before headers + outstr += self.printstring(self.chunkpad, '_' * self.xscreensize, + towin=towin, align=False) + # select color-pair based on if the header is selected + colorpair = self.getcolorpair(name=selected and "selected" or "normal", + attrlist=[curses.A_BOLD]) + + # print out each line of the chunk, expanding it to screen width + + # number of characters to indent lines on this level by + indentnumchars = 0 + checkbox = self.getstatusprefixstring(header) + if not header.folded or ignorefolding: + textlist = text.split("\n") + linestr = checkbox + textlist[0] + else: + linestr = checkbox + header.filename() + outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, + towin=towin) + if not header.folded or ignorefolding: + if len(textlist) > 1: + for line in textlist[1:]: + linestr = " "*(indentnumchars + len(checkbox)) + line + outstr += self.printstring(self.chunkpad, linestr, + pair=colorpair, towin=towin) + + return outstr + + def printhunklinesbefore(self, hunk, selected=False, towin=True, + ignorefolding=False): + "includes start/end line indicator" + outstr = "" + # where hunk is in list of siblings + hunkindex = hunk.header.hunks.index(hunk) + + if hunkindex != 0: + # add separating line before headers + outstr += self.printstring(self.chunkpad, ' '*self.xscreensize, + towin=towin, align=False) + + colorpair = self.getcolorpair(name=selected and "selected" or "normal", + attrlist=[curses.A_BOLD]) + + # print out from-to line with checkbox + checkbox = self.getstatusprefixstring(hunk) + + lineprefix = " "*self.hunkindentnumchars + checkbox + frtoline = " " + hunk.getfromtoline().strip("\n") + + + outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, + align=False) # add uncolored checkbox/indent + outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair, + towin=towin) + + if hunk.folded and not ignorefolding: + # skip remainder of output + return outstr + + # print out lines of the chunk preceeding changed-lines + for line in hunk.before: + linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line + outstr += self.printstring(self.chunkpad, linestr, towin=towin) + + return outstr + + def printhunklinesafter(self, hunk, towin=True, ignorefolding=False): + outstr = "" + if hunk.folded and not ignorefolding: + return outstr + + # a bit superfluous, but to avoid hard-coding indent amount + checkbox = self.getstatusprefixstring(hunk) + for line in hunk.after: + linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line + outstr += self.printstring(self.chunkpad, linestr, towin=towin) + + return outstr + + def printhunkchangedline(self, hunkline, selected=False, towin=True): + outstr = "" + checkbox = self.getstatusprefixstring(hunkline) + + linestr = hunkline.prettystr().strip("\n") + + # select color-pair based on whether line is an addition/removal + if selected: + colorpair = self.getcolorpair(name="selected") + elif linestr.startswith("+"): + colorpair = self.getcolorpair(name="addition") + elif linestr.startswith("-"): + colorpair = self.getcolorpair(name="deletion") + elif linestr.startswith("\\"): + colorpair = self.getcolorpair(name="normal") + + lineprefix = " "*self.hunklineindentnumchars + checkbox + outstr += self.printstring(self.chunkpad, lineprefix, towin=towin, + align=False) # add uncolored checkbox/indent + outstr += self.printstring(self.chunkpad, linestr, pair=colorpair, + towin=towin, showwhtspc=True) + return outstr + + def printitem(self, item=None, ignorefolding=False, recursechildren=True, + towin=True): + """ + use __printitem() to print the the specified item.applied. + if item is not specified, then print the entire patch. + (hiding folded elements, etc. -- see __printitem() docstring) + """ + if item is None: + item = self.headerlist + if recursechildren: + self.linesprintedtopadsofar = 0 + + outstr = [] + self.__printitem(item, ignorefolding, recursechildren, outstr, + towin=towin) + return ''.join(outstr) + + def outofdisplayedarea(self): + y, _ = self.chunkpad.getyx() # cursor location + # * 2 here works but an optimization would be the max number of + # consecutive non selectable lines + # i.e the max number of context line for any hunk in the patch + miny = min(0, self.firstlineofpadtoprint - self.yscreensize) + maxy = self.firstlineofpadtoprint + self.yscreensize * 2 + return y < miny or y > maxy + + def handleselection(self, item, recursechildren): + selected = (item is self.currentselecteditem) + if selected and recursechildren: + # assumes line numbering starting from line 0 + self.selecteditemstartline = self.linesprintedtopadsofar + selecteditemlines = self.getnumlinesdisplayed(item, + recursechildren=False) + self.selecteditemendline = (self.selecteditemstartline + + selecteditemlines - 1) + return selected + + def __printitem(self, item, ignorefolding, recursechildren, outstr, + towin=True): + """ + recursive method for printing out patch/header/hunk/hunk-line data to + screen. also returns a string with all of the content of the displayed + patch (not including coloring, etc.). + + if ignorefolding is True, then folded items are printed out. + + if recursechildren is False, then only print the item without its + child items. + + """ + if towin and self.outofdisplayedarea(): + return + + selected = self.handleselection(item, recursechildren) + + # patch object is a list of headers + if isinstance(item, patch): + if recursechildren: + for hdr in item: + self.__printitem(hdr, ignorefolding, + recursechildren, outstr, towin) + # todo: eliminate all isinstance() calls + if isinstance(item, uiheader): + outstr.append(self.printheader(item, selected, towin=towin, + ignorefolding=ignorefolding)) + if recursechildren: + for hnk in item.hunks: + self.__printitem(hnk, ignorefolding, + recursechildren, outstr, towin) + elif (isinstance(item, uihunk) and + ((not item.header.folded) or ignorefolding)): + # print the hunk data which comes before the changed-lines + outstr.append(self.printhunklinesbefore(item, selected, towin=towin, + ignorefolding=ignorefolding)) + if recursechildren: + for l in item.changedlines: + self.__printitem(l, ignorefolding, + recursechildren, outstr, towin) + outstr.append(self.printhunklinesafter(item, towin=towin, + ignorefolding=ignorefolding)) + elif (isinstance(item, uihunkline) and + ((not item.hunk.folded) or ignorefolding)): + outstr.append(self.printhunkchangedline(item, selected, + towin=towin)) + + return outstr + + def getnumlinesdisplayed(self, item=None, ignorefolding=False, + recursechildren=True): + """ + return the number of lines which would be displayed if the item were + to be printed to the display. the item will not be printed to the + display (pad). + if no item is given, assume the entire patch. + if ignorefolding is True, folded items will be unfolded when counting + the number of lines. + + """ + # temporarily disable printing to windows by printstring + patchdisplaystring = self.printitem(item, ignorefolding, + recursechildren, towin=False) + numlines = len(patchdisplaystring) / self.xscreensize + return numlines + + def sigwinchhandler(self, n, frame): + "handle window resizing" + try: + curses.endwin() + self.yscreensize, self.xscreensize = gethw() + self.statuswin.resize(self.numstatuslines, self.xscreensize) + self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 + self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) + # todo: try to resize commit message window if possible + except curses.error: + pass + + def getcolorpair(self, fgcolor=None, bgcolor=None, name=None, + attrlist=None): + """ + get a curses color pair, adding it to self.colorpairs if it is not + already defined. an optional string, name, can be passed as a shortcut + for referring to the color-pair. by default, if no arguments are + specified, the white foreground / black background color-pair is + returned. + + it is expected that this function will be used exclusively for + initializing color pairs, and not curses.init_pair(). + + attrlist is used to 'flavor' the returned color-pair. this information + is not stored in self.colorpairs. it contains attribute values like + curses.A_BOLD. + + """ + if (name is not None) and name in self.colorpairnames: + # then get the associated color pair and return it + colorpair = self.colorpairnames[name] + else: + if fgcolor is None: + fgcolor = -1 + if bgcolor is None: + bgcolor = -1 + if (fgcolor, bgcolor) in self.colorpairs: + colorpair = self.colorpairs[(fgcolor, bgcolor)] + else: + pairindex = len(self.colorpairs) + 1 + curses.init_pair(pairindex, fgcolor, bgcolor) + colorpair = self.colorpairs[(fgcolor, bgcolor)] = ( + curses.color_pair(pairindex)) + if name is not None: + self.colorpairnames[name] = curses.color_pair(pairindex) + + # add attributes if possible + if attrlist is None: + attrlist = [] + if colorpair < 256: + # then it is safe to apply all attributes + for textattr in attrlist: + colorpair |= textattr + else: + # just apply a select few (safe?) attributes + for textattrib in (curses.A_UNDERLINE, curses.A_BOLD): + if textattrib in attrlist: + colorpair |= textattrib + return colorpair + + def initcolorpair(self, *args, **kwargs): + "same as getcolorpair." + self.getcolorpair(*args, **kwargs) + + def helpwindow(self): + "print a help window to the screen. exit after any keypress." + helptext = """ [press any key to return to the patch-display] + +crecord allows you to interactively choose among the changes you have made, +and commit only those changes you select. after committing the selected +changes, the unselected changes are still present in your working copy, so you +can use crecord multiple times to split large changes into smaller changesets. +the following are valid keystrokes: + + [space] : (un-)select item ([~]/[x] = partly/fully applied) + a : (un-)select all items + up/down-arrow [k/j] : go to previous/next unfolded item + pgup/pgdn [k/j] : go to previous/next item of same type + right/left-arrow [l/h] : go to child item / parent item + shift-left-arrow [h] : go to parent header / fold selected header + f : fold / unfold item, hiding/revealing its children + f : fold / unfold parent item and all of its ancestors + m : edit / resume editing the commit message + e : edit the currently selected hunk + a : toggle amend mode (hg rev >= 2.2) + c : commit selected changes + r : review/edit and commit selected changes + q : quit without committing (no changes will be made) + ? : help (what you're currently reading)""" + + helpwin = curses.newwin(self.yscreensize, 0, 0, 0) + helplines = helptext.split("\n") + helplines = helplines + [" "]*( + self.yscreensize - self.numstatuslines - len(helplines) - 1) + try: + for line in helplines: + self.printstring(helpwin, line, pairname="legend") + except curses.error: + pass + helpwin.refresh() + try: + helpwin.getkey() + except curses.error: + pass + + def confirmationwindow(self, windowtext): + "display an informational window, then wait for and return a keypress." + + confirmwin = curses.newwin(self.yscreensize, 0, 0, 0) + try: + lines = windowtext.split("\n") + for line in lines: + self.printstring(confirmwin, line, pairname="selected") + except curses.error: + pass + self.stdscr.refresh() + confirmwin.refresh() + try: + response = chr(self.stdscr.getch()) + except ValueError: + response = None + + return response + + def confirmcommit(self, review=False): + "ask for 'y' to be pressed to confirm commit. return True if confirmed." + if review: + confirmtext = ( +"""if you answer yes to the following, the your currently chosen patch chunks +will be loaded into an editor. you may modify the patch from the editor, and +save the changes if you wish to change the patch. otherwise, you can just +close the editor without saving to accept the current patch as-is. + +note: don't add/remove lines unless you also modify the range information. + failing to follow this rule will result in the commit aborting. + +are you sure you want to review/edit and commit the selected changes [yn]? """) + else: + confirmtext = ( + "are you sure you want to commit the selected changes [yn]? ") + + response = self.confirmationwindow(confirmtext) + if response is None: + response = "n" + if response.lower().startswith("y"): + return True + else: + return False + + def recenterdisplayedarea(self): + """ + once we scrolled with pg up pg down we can be pointing outside of the + display zone. we print the patch with towin=False to compute the + location of the selected item eventhough it is outside of the displayed + zone and then update the scroll. + """ + self.printitem(towin=False) + self.updatescroll() + + def toggleedit(self, item=None, test=False): + """ + edit the currently chelected chunk + """ + + def editpatchwitheditor(self, chunk): + if chunk is None: + self.ui.write(_('cannot edit patch for whole file')) + self.ui.write("\n") + return None + if chunk.header.binary(): + self.ui.write(_('cannot edit patch for binary file')) + self.ui.write("\n") + return None + # patch comment based on the git one (based on comment at end of + # http://mercurial.selenic.com/wiki/recordextension) + phelp = '---' + _(""" + to remove '-' lines, make them ' ' lines (context). + to remove '+' lines, delete them. + lines starting with # will be removed from the patch. + + if the patch applies cleanly, the edited hunk will immediately be + added to the record list. if it does not apply cleanly, a rejects + file will be generated: you can use that when you try again. if + all lines of the hunk are removed, then the edit is aborted and + the hunk is left unchanged. + """) + (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", + suffix=".diff", text=True) + ncpatchfp = None + try: + # write the initial patch + f = os.fdopen(patchfd, "w") + chunk.header.write(f) + chunk.write(f) + f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) + f.close() + # start the editor and wait for it to complete + editor = self.ui.geteditor() + self.ui.system("%s \"%s\"" % (editor, patchfn), + environ={'hguser': self.ui.username()}, + onerr=util.Abort, errprefix=_("edit failed")) + # remove comment lines + patchfp = open(patchfn) + ncpatchfp = cStringIO.StringIO() + for line in patchfp: + if not line.startswith('#'): + ncpatchfp.write(line) + patchfp.close() + ncpatchfp.seek(0) + newpatches = patchmod.parsepatch(ncpatchfp) + finally: + os.unlink(patchfn) + del ncpatchfp + return newpatches + if item is None: + item = self.currentselecteditem + if isinstance(item, uiheader): + return + if isinstance(item, uihunkline): + item = item.parentitem() + if not isinstance(item, uihunk): + return + + beforeadded, beforeremoved = item.added, item.removed + newpatches = editpatchwitheditor(self, item) + header = item.header + editedhunkindex = header.hunks.index(item) + hunksbefore = header.hunks[:editedhunkindex] + hunksafter = header.hunks[editedhunkindex + 1:] + newpatchheader = newpatches[0] + newhunks = [uihunk(h, header) for h in newpatchheader.hunks] + newadded = sum([h.added for h in newhunks]) + newremoved = sum([h.removed for h in newhunks]) + offset = (newadded - beforeadded) - (newremoved - beforeremoved) + + for h in hunksafter: + h.toline += offset + for h in newhunks: + h.folded = False + header.hunks = hunksbefore + newhunks + hunksafter + if self.emptypatch(): + header.hunks = hunksbefore + [item] + hunksafter + self.currentselecteditem = header + + if not test: + self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 + self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) + self.updatescroll() + self.stdscr.refresh() + self.statuswin.refresh() + self.stdscr.keypad(1) + + def emptypatch(self): + item = self.headerlist + if not item: + return True + for header in item: + if header.hunks: + return False + return True + + def handlekeypressed(self, keypressed, test=False): + if keypressed in ["k", "KEY_UP"]: + self.uparrowevent() + if keypressed in ["k", "KEY_PPAGE"]: + self.uparrowshiftevent() + elif keypressed in ["j", "KEY_DOWN"]: + self.downarrowevent() + elif keypressed in ["j", "KEY_NPAGE"]: + self.downarrowshiftevent() + elif keypressed in ["l", "KEY_RIGHT"]: + self.rightarrowevent() + elif keypressed in ["h", "KEY_LEFT"]: + self.leftarrowevent() + elif keypressed in ["h", "KEY_SLEFT"]: + self.leftarrowshiftevent() + elif keypressed in ["q"]: + raise util.Abort(_('user quit')) + elif keypressed in ["c"]: + if self.confirmcommit(): + return True + elif keypressed in ["r"]: + if self.confirmcommit(review=True): + return True + elif test and keypressed in ['X']: + return True + elif keypressed in [' '] or (test and keypressed in ["TOGGLE"]): + self.toggleapply() + elif keypressed in ['A']: + self.toggleall() + elif keypressed in ['e']: + self.toggleedit(test=test) + elif keypressed in ["f"]: + self.togglefolded() + elif keypressed in ["f"]: + self.togglefolded(foldparent=True) + elif keypressed in ["?"]: + self.helpwindow() + + def main(self, stdscr): + """ + method to be wrapped by curses.wrapper() for selecting chunks. + + """ + signal.signal(signal.SIGWINCH, self.sigwinchhandler) + self.stdscr = stdscr + self.yscreensize, self.xscreensize = self.stdscr.getmaxyx() + + curses.start_color() + curses.use_default_colors() + + # available colors: black, blue, cyan, green, magenta, white, yellow + # init_pair(color_id, foreground_color, background_color) + self.initcolorpair(None, None, name="normal") + self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA, + name="selected") + self.initcolorpair(curses.COLOR_RED, None, name="deletion") + self.initcolorpair(curses.COLOR_GREEN, None, name="addition") + self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend") + # newwin([height, width,] begin_y, begin_x) + self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0) + self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences + + # figure out how much space to allocate for the chunk-pad which is + # used for displaying the patch + + # stupid hack to prevent getnumlinesdisplayed from failing + self.chunkpad = curses.newpad(1, self.xscreensize) + + # add 1 so to account for last line text reaching end of line + self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1 + self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize) + + # initialize selecteitemendline (initial start-line is 0) + self.selecteditemendline = self.getnumlinesdisplayed( + self.currentselecteditem, recursechildren=False) + + while True: + self.updatescreen() + try: + keypressed = self.statuswin.getkey() + except curses.error: + keypressed = "foobar" + if self.handlekeypressed(keypressed): + break diff -r 612ed41ae359 -r e89f909edffa mercurial/dagutil.py --- a/mercurial/dagutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/dagutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -88,7 +88,10 @@ '''generic implementations for DAGs''' def ancestorset(self, starts, stops=None): - stops = stops and set(stops) or set() + if stops: + stops = set(stops) + else: + stops = set() seen = set() pending = list(starts) while pending: @@ -179,7 +182,10 @@ def ancestorset(self, starts, stops=None): rlog = self._revlog idx = rlog.index - stops = stops and set(stops) or set() + if stops: + stops = set(stops) + else: + stops = set() seen = set() pending = list(starts) while pending: diff -r 612ed41ae359 -r e89f909edffa mercurial/default.d/mergetools.rc --- a/mercurial/default.d/mergetools.rc Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/default.d/mergetools.rc Thu Apr 16 20:57:51 2015 -0500 @@ -102,6 +102,13 @@ bcompare.priority=-1 bcompare.diffargs=-lro -lefttitle=$plabel1 -righttitle=$clabel -solo -expandall $parent $child +; OS X version of Beyond Compare +bcomposx.executable = /Applications/Beyond Compare.app/Contents/MacOS/bcomp +bcomposx.args=$local $other $base -mergeoutput=$output -ro -lefttitle=parent1 -centertitle=base -righttitle=parent2 -outputtitle=merged -automerge -reviewconflicts -solo +bcomposx.gui=True +bcomposx.priority=-1 +bcomposx.diffargs=-lro -lefttitle=$plabel1 -righttitle=$clabel -solo -expandall $parent $child + winmerge.args=/e /x /wl /ub /dl other /dr local $other $local $output winmerge.regkey=Software\Thingamahoochie\WinMerge winmerge.regkeyalt=Software\Wow6432Node\Thingamahoochie\WinMerge\ diff -r 612ed41ae359 -r e89f909edffa mercurial/dirs.c --- a/mercurial/dirs.c Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/dirs.c Thu Apr 16 20:57:51 2015 -0500 @@ -9,6 +9,7 @@ #define PY_SSIZE_T_CLEAN #include +#include #include "util.h" /* @@ -32,23 +33,19 @@ { const char *s = PyString_AS_STRING(path); - while (pos != -1) { - if (s[pos] == '/') - break; - pos -= 1; - } - - return pos; + const char *ret = strchr(s + pos, '/'); + return (ret != NULL) ? (ret - s) : -1; } static int _addpath(PyObject *dirs, PyObject *path) { - const char *cpath = PyString_AS_STRING(path); - Py_ssize_t pos = PyString_GET_SIZE(path); + char *cpath = PyString_AS_STRING(path); + Py_ssize_t len = PyString_GET_SIZE(path); + Py_ssize_t pos = -1; PyObject *key = NULL; int ret = -1; - while ((pos = _finddir(path, pos - 1)) != -1) { + while ((pos = _finddir(path, pos + 1)) != -1) { PyObject *val; /* It's likely that every prefix already has an entry @@ -56,10 +53,18 @@ deallocating a string for each prefix we check. */ if (key != NULL) ((PyStringObject *)key)->ob_shash = -1; - else { - /* Force Python to not reuse a small shared string. */ - key = PyString_FromStringAndSize(cpath, - pos < 2 ? 2 : pos); + else if (pos != 0) { + /* pos >= 1, which means that len >= 2. This is + guaranteed to produce a non-interned string. */ + key = PyString_FromStringAndSize(cpath, len); + if (key == NULL) + goto bail; + } else { + /* pos == 0, which means we need to increment the dir + count for the empty string. We need to make sure we + don't muck around with interned strings, so throw it + away later. */ + key = PyString_FromString(""); if (key == NULL) goto bail; } @@ -69,6 +74,10 @@ val = PyDict_GetItem(dirs, key); if (val != NULL) { PyInt_AS_LONG(val) += 1; + if (pos != 0) + PyString_AS_STRING(key)[pos] = '/'; + else + key = NULL; continue; } @@ -83,6 +92,9 @@ Py_DECREF(val); if (ret == -1) goto bail; + + /* Clear the key out since we've already exposed it to Python + and can't mutate it further. */ Py_CLEAR(key); } ret = 0; @@ -95,11 +107,11 @@ static int _delpath(PyObject *dirs, PyObject *path) { - Py_ssize_t pos = PyString_GET_SIZE(path); + Py_ssize_t pos = -1; PyObject *key = NULL; int ret = -1; - while ((pos = _finddir(path, pos - 1)) != -1) { + while ((pos = _finddir(path, pos + 1)) != -1) { PyObject *val; key = PyString_FromStringAndSize(PyString_AS_STRING(path), pos); diff -r 612ed41ae359 -r e89f909edffa mercurial/dirstate.py --- a/mercurial/dirstate.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/dirstate.py Thu Apr 16 20:57:51 2015 -0500 @@ -87,15 +87,29 @@ return self._copymap @propertycache - def _foldmap(self): + def _filefoldmap(self): + try: + makefilefoldmap = parsers.make_file_foldmap + except AttributeError: + pass + else: + return makefilefoldmap(self._map, util.normcasespec, + util.normcasefallback) + f = {} normcase = util.normcase for name, s in self._map.iteritems(): if s[0] != 'r': f[normcase(name)] = name + f['.'] = '.' # prevents useless util.fspath() invocation + return f + + @propertycache + def _dirfoldmap(self): + f = {} + normcase = util.normcase for name in self._dirs: f[normcase(name)] = name - f['.'] = '.' # prevents useless util.fspath() invocation return f @repocache('branch') @@ -125,7 +139,7 @@ @propertycache def _dirs(self): - return scmutil.dirs(self._map, 'r') + return util.dirs(self._map, 'r') def dirs(self): return self._dirs @@ -332,8 +346,8 @@ self._pl = p def invalidate(self): - for a in ("_map", "_copymap", "_foldmap", "_branch", "_pl", "_dirs", - "_ignore"): + for a in ("_map", "_copymap", "_filefoldmap", "_dirfoldmap", "_branch", + "_pl", "_dirs", "_ignore"): if a in self.__dict__: delattr(self, a) self._lastnormaltime = 0 @@ -367,7 +381,7 @@ if f in self._dirs: raise util.Abort(_('directory %r already in dirstate') % f) # shadows - for d in scmutil.finddirs(f): + for d in util.finddirs(f): if d in self._dirs: break if d in self._map and self[d] != 'r': @@ -464,36 +478,56 @@ self._droppath(f) del self._map[f] - def _normalize(self, path, isknown, ignoremissing=False, exists=None): + def _discoverpath(self, path, normed, ignoremissing, exists, storemap): + if exists is None: + exists = os.path.lexists(os.path.join(self._root, path)) + if not exists: + # Maybe a path component exists + if not ignoremissing and '/' in path: + d, f = path.rsplit('/', 1) + d = self._normalize(d, False, ignoremissing, None) + folded = d + "/" + f + else: + # No path components, preserve original case + folded = path + else: + # recursively normalize leading directory components + # against dirstate + if '/' in normed: + d, f = normed.rsplit('/', 1) + d = self._normalize(d, False, ignoremissing, True) + r = self._root + "/" + d + folded = d + "/" + util.fspath(f, r) + else: + folded = util.fspath(normed, self._root) + storemap[normed] = folded + + return folded + + def _normalizefile(self, path, isknown, ignoremissing=False, exists=None): normed = util.normcase(path) - folded = self._foldmap.get(normed, None) + folded = self._filefoldmap.get(normed, None) if folded is None: if isknown: folded = path else: - if exists is None: - exists = os.path.lexists(os.path.join(self._root, path)) - if not exists: - # Maybe a path component exists - if not ignoremissing and '/' in path: - d, f = path.rsplit('/', 1) - d = self._normalize(d, isknown, ignoremissing, None) - folded = d + "/" + f - else: - # No path components, preserve original case - folded = path - else: - # recursively normalize leading directory components - # against dirstate - if '/' in normed: - d, f = normed.rsplit('/', 1) - d = self._normalize(d, isknown, ignoremissing, True) - r = self._root + "/" + d - folded = d + "/" + util.fspath(f, r) - else: - folded = util.fspath(normed, self._root) - self._foldmap[normed] = folded + folded = self._discoverpath(path, normed, ignoremissing, exists, + self._filefoldmap) + return folded + def _normalize(self, path, isknown, ignoremissing=False, exists=None): + normed = util.normcase(path) + folded = self._filefoldmap.get(normed, None) + if folded is None: + folded = self._dirfoldmap.get(normed, None) + if folded is None: + if isknown: + folded = path + else: + # store discovered result in dirfoldmap so that future + # normalizefile calls don't start matching directories + folded = self._discoverpath(path, normed, ignoremissing, exists, + self._dirfoldmap) return folded def normalize(self, path, isknown=False, ignoremissing=False): @@ -567,7 +601,7 @@ return False if self._ignore(f): return True - for p in scmutil.finddirs(f): + for p in util.finddirs(f): if self._ignore(p): return True return False @@ -599,7 +633,6 @@ matchedir = match.explicitdir badfn = match.bad dmap = self._map - normpath = util.normpath lstat = os.lstat getkind = stat.S_IFMT dirkind = stat.S_IFDIR @@ -611,7 +644,7 @@ dirsnotfound = [] notfoundadd = dirsnotfound.append - if match.matchfn != match.exact and self._checkcase: + if not match.isexact() and self._checkcase: normalize = self._normalize else: normalize = None @@ -629,16 +662,18 @@ j += 1 if not files or '.' in files: - files = [''] + files = ['.'] results = dict.fromkeys(subrepos) results['.hg'] = None alldirs = None for ff in files: - if normalize: - nf = normalize(normpath(ff), False, True) + # constructing the foldmap is expensive, so don't do it for the + # common case where files is ['.'] + if normalize and ff != '.': + nf = normalize(ff, False, True) else: - nf = normpath(ff) + nf = ff if nf in results: continue @@ -663,7 +698,7 @@ results[nf] = None else: # does it match a missing directory? if alldirs is None: - alldirs = scmutil.dirs(dmap) + alldirs = util.dirs(dmap) if nf in alldirs: if matchedir: matchedir(nf) @@ -711,7 +746,7 @@ join = self._join exact = skipstep3 = False - if matchfn == match.exact: # match.exact + if match.isexact(): # match.exact exact = True dirignore = util.always # skip step 2 elif match.files() and not match.anypats(): # match.match, no patterns @@ -719,56 +754,70 @@ if not exact and self._checkcase: normalize = self._normalize + normalizefile = self._normalizefile skipstep3 = False else: - normalize = None + normalize = self._normalize + normalizefile = None # step 1: find all explicit files results, work, dirsnotfound = self._walkexplicit(match, subrepos) skipstep3 = skipstep3 and not (work or dirsnotfound) work = [d for d in work if not dirignore(d[0])] - wadd = work.append # step 2: visit subdirectories - while work: - nd, d = work.pop() - skip = None - if nd == '.': - nd = '' - d = '' - else: - skip = '.hg' - try: - entries = listdir(join(nd), stat=True, skip=skip) - except OSError, inst: - if inst.errno in (errno.EACCES, errno.ENOENT): - match.bad(self.pathto(nd), inst.strerror) - continue - raise - for f, kind, st in entries: - if normalize: - nf = normalize(nd and (nd + "/" + f) or f, True, True) - f = d and (d + "/" + f) or f + def traverse(work, alreadynormed): + wadd = work.append + while work: + nd = work.pop() + skip = None + if nd == '.': + nd = '' else: - nf = nd and (nd + "/" + f) or f - f = nf - if nf not in results: - if kind == dirkind: - if not ignore(nf): - if matchtdir: - matchtdir(nf) - wadd((nf, f)) - if nf in dmap and (matchalways or matchfn(nf)): + skip = '.hg' + try: + entries = listdir(join(nd), stat=True, skip=skip) + except OSError, inst: + if inst.errno in (errno.EACCES, errno.ENOENT): + match.bad(self.pathto(nd), inst.strerror) + continue + raise + for f, kind, st in entries: + if normalizefile: + # even though f might be a directory, we're only + # interested in comparing it to files currently in the + # dmap -- therefore normalizefile is enough + nf = normalizefile(nd and (nd + "/" + f) or f, True, + True) + else: + nf = nd and (nd + "/" + f) or f + if nf not in results: + if kind == dirkind: + if not ignore(nf): + if matchtdir: + matchtdir(nf) + wadd(nf) + if nf in dmap and (matchalways or matchfn(nf)): + results[nf] = None + elif kind == regkind or kind == lnkkind: + if nf in dmap: + if matchalways or matchfn(nf): + results[nf] = st + elif ((matchalways or matchfn(nf)) + and not ignore(nf)): + # unknown file -- normalize if necessary + if not alreadynormed: + nf = normalize(nf, False, True) + results[nf] = st + elif nf in dmap and (matchalways or matchfn(nf)): results[nf] = None - elif kind == regkind or kind == lnkkind: - if nf in dmap: - if matchalways or matchfn(nf): - results[nf] = st - elif (matchalways or matchfn(f)) and not ignore(nf): - results[nf] = st - elif nf in dmap and (matchalways or matchfn(nf)): - results[nf] = None + + for nd, d in work: + # alreadynormed means that processwork doesn't have to do any + # expensive directory normalization + alreadynormed = not normalize or nd == d + traverse([d], alreadynormed) for s in subrepos: del results[s] @@ -797,7 +846,8 @@ # different case, don't add one for this, since that would # make it appear as if the file exists under both names # on disk. - if normalize and normalize(nf, True, True) in results: + if (normalizefile and + normalizefile(nf, True, True) in results): results[nf] = None # Report ignored items in the dmap as long as they are not # under a symlink directory. @@ -896,9 +946,9 @@ elif time != mtime and time != mtime & _rangemask: ladd(fn) elif mtime == lastnormaltime: - # fn may have been changed in the same timeslot without - # changing its size. This can happen if we quickly do - # multiple commits in a single transaction. + # fn may have just been marked as normal and it may have + # changed in the same second without changing its size. + # This can happen if we quickly do multiple commits. # Force lookup, so we don't miss such a racy file change. ladd(fn) elif listclean: @@ -921,7 +971,7 @@ if match.always(): return dmap.keys() files = match.files() - if match.matchfn == match.exact: + if match.isexact(): # fast path -- filter the other way around, since typically files is # much smaller than dmap return [f for f in files if f in dmap] diff -r 612ed41ae359 -r e89f909edffa mercurial/discovery.py --- a/mercurial/discovery.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/discovery.py Thu Apr 16 20:57:51 2015 -0500 @@ -218,7 +218,10 @@ r = repo.set('heads(%ln + %ln)', oldheads, outgoing.missing) newheads = list(c.node() for c in r) # set some unsynced head to issue the "unsynced changes" warning - unsynced = inc and set([None]) or set() + if inc: + unsynced = set([None]) + else: + unsynced = set() return {None: (oldheads, newheads, unsynced)} def checkheads(repo, remote, outgoing, remoteheads, newbranch=False, inc=False, @@ -269,9 +272,13 @@ # If there are more heads after the push than before, a suitable # error message, depending on unsynced status, is displayed. error = None - allmissing = set(outgoing.missing) - allfuturecommon = set(c.node() for c in repo.set('%ld', outgoing.common)) - allfuturecommon.update(allmissing) + # If there is no obsstore, allfuturecommon won't be used, so no + # need to compute it. + if repo.obsstore: + allmissing = set(outgoing.missing) + cctx = repo.set('%ld', outgoing.common) + allfuturecommon = set(c.node() for c in cctx) + allfuturecommon.update(allmissing) for branch, heads in sorted(headssum.iteritems()): remoteheads, newheads, unsyncedheads = heads candidate_newhs = set(newheads) diff -r 612ed41ae359 -r e89f909edffa mercurial/dispatch.py --- a/mercurial/dispatch.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/dispatch.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,6 +7,7 @@ from i18n import _ import os, sys, atexit, signal, pdb, socket, errno, shlex, time, traceback, re +import difflib import util, commands, hg, fancyopts, extensions, hook, error import cmdutil, encoding import ui as uimod @@ -27,6 +28,31 @@ "run the command in sys.argv" sys.exit((dispatch(request(sys.argv[1:])) or 0) & 255) +def _getsimilar(symbols, value): + sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio() + # The cutoff for similarity here is pretty arbitrary. It should + # probably be investigated and tweaked. + return [s for s in symbols if sim(s) > 0.6] + +def _formatparse(write, inst): + similar = [] + if isinstance(inst, error.UnknownIdentifier): + # make sure to check fileset first, as revset can invoke fileset + similar = _getsimilar(inst.symbols, inst.function) + if len(inst.args) > 1: + write(_("hg: parse error at %s: %s\n") % + (inst.args[1], inst.args[0])) + if (inst.args[0][0] == ' '): + write(_("unexpected leading whitespace\n")) + else: + write(_("hg: parse error: %s\n") % inst.args[0]) + if similar: + if len(similar) == 1: + write(_("(did you mean %r?)\n") % similar[0]) + else: + ss = ", ".join(sorted(similar)) + write(_("(did you mean one of %s?)\n") % ss) + def dispatch(req): "run the command specified in req.args" if req.ferr: @@ -55,13 +81,7 @@ ferr.write(_("(%s)\n") % inst.hint) return -1 except error.ParseError, inst: - if len(inst.args) > 1: - ferr.write(_("hg: parse error at %s: %s\n") % - (inst.args[1], inst.args[0])) - if (inst.args[0][0] == ' '): - ferr.write(_("unexpected leading whitespace\n")) - else: - ferr.write(_("hg: parse error: %s\n") % inst.args[0]) + _formatparse(ferr.write, inst) return -1 msg = ' '.join(' ' in a and repr(a) or a for a in req.args) @@ -154,13 +174,7 @@ ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") % (inst.args[0], " ".join(inst.args[1]))) except error.ParseError, inst: - if len(inst.args) > 1: - ui.warn(_("hg: parse error at %s: %s\n") % - (inst.args[1], inst.args[0])) - if (inst.args[0][0] == ' '): - ui.warn(_("unexpected leading whitespace\n")) - else: - ui.warn(_("hg: parse error: %s\n") % inst.args[0]) + _formatparse(ui.warn, inst) return -1 except error.LockHeld, inst: if inst.errno == errno.ETIMEDOUT: @@ -206,7 +220,15 @@ # (but don't check for extensions themselves) commands.help_(ui, inst.args[0], unknowncmd=True) except error.UnknownCommand: - commands.help_(ui, 'shortlist') + suggested = False + if len(inst.args) == 2: + sim = _getsimilar(inst.args[1], inst.args[0]) + if sim: + ui.warn(_('(did you mean one of %s?)\n') % + ', '.join(sorted(sim))) + suggested = True + if not suggested: + commands.help_(ui, 'shortlist') except error.InterventionRequired, inst: ui.warn("%s\n" % inst) return 1 @@ -804,7 +826,7 @@ if cmdoptions.get('insecure', False): for ui_ in uis: - ui_.setconfig('web', 'cacerts', '', '--insecure') + ui_.setconfig('web', 'cacerts', '!', '--insecure') if options['version']: return commands.version_(ui) diff -r 612ed41ae359 -r e89f909edffa mercurial/encoding.py --- a/mercurial/encoding.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/encoding.py Thu Apr 16 20:57:51 2015 -0500 @@ -296,6 +296,22 @@ asciilower = impl return impl(s) +def _asciiupper(s): + '''convert a string to uppercase if ASCII + + Raises UnicodeDecodeError if non-ASCII characters are found.''' + s.decode('ascii') + return s.upper() + +def asciiupper(s): + # delay importing avoids cyclic dependency around "parsers" in + # pure Python build (util => i18n => encoding => parsers => util) + import parsers + impl = getattr(parsers, 'asciiupper', _asciiupper) + global asciiupper + asciiupper = impl + return impl(s) + def lower(s): "best-effort encoding-aware case-folding of local string s" try: @@ -320,10 +336,11 @@ def upper(s): "best-effort encoding-aware case-folding of local string s" try: - s.decode('ascii') # throw exception for non-ASCII character - return s.upper() + return asciiupper(s) except UnicodeDecodeError: - pass + return upperfallback(s) + +def upperfallback(s): try: if isinstance(s, localstr): u = s._utf8.decode("utf-8") @@ -339,6 +356,21 @@ except LookupError, k: raise error.Abort(k, hint="please check your locale settings") +class normcasespecs(object): + '''what a platform's normcase does to ASCII strings + + This is specified per platform, and should be consistent with what normcase + on that platform actually does. + + lower: normcase lowercases ASCII strings + upper: normcase uppercases ASCII strings + other: the fallback function should always be called + + This should be kept in sync with normcase_spec in util.h.''' + lower = -1 + upper = 1 + other = 0 + _jsonmap = {} def jsonescape(s): diff -r 612ed41ae359 -r e89f909edffa mercurial/error.py --- a/mercurial/error.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/error.py Thu Apr 16 20:57:51 2015 -0500 @@ -22,6 +22,10 @@ class LookupError(RevlogError, KeyError): def __init__(self, name, index, message): self.name = name + self.index = index + # this can't be called 'message' because at least some installs of + # Python 2.6+ complain about the 'message' property being deprecated + self.lookupmessage = message if isinstance(name, str) and len(name) == 20: from node import short name = short(name) @@ -61,7 +65,16 @@ """Exception raised when a remote repo reports failure""" class ParseError(Exception): - """Exception raised when parsing config files (msg[, pos])""" + """Raised when parsing config files and {rev,file}sets (msg[, pos])""" + +class UnknownIdentifier(ParseError): + """Exception raised when a {rev,file}set references an unknown identifier""" + + def __init__(self, function, symbols): + from i18n import _ + ParseError.__init__(self, _("unknown identifier: %s") % function) + self.function = function + self.symbols = symbols class RepoError(Exception): def __init__(self, *args, **kw): @@ -134,8 +147,20 @@ pass class CensoredNodeError(RevlogError): - """error raised when content verification fails on a censored node""" + """error raised when content verification fails on a censored node - def __init__(self, filename, node): + Also contains the tombstone data substituted for the uncensored data. + """ + + def __init__(self, filename, node, tombstone): from node import short RevlogError.__init__(self, '%s:%s' % (filename, short(node))) + self.tombstone = tombstone + +class CensoredBaseError(RevlogError): + """error raised when a delta is rejected because its base is censored + + A delta based on a censored revision must be formed as single patch + operation which replaces the entire base with new content. This ensures + the delta may be applied by clones which have not censored the base. + """ diff -r 612ed41ae359 -r e89f909edffa mercurial/exchange.py --- a/mercurial/exchange.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/exchange.py Thu Apr 16 20:57:51 2015 -0500 @@ -10,6 +10,7 @@ import errno, urllib import util, scmutil, changegroup, base85, error import discovery, phases, obsolete, bookmarks as bookmod, bundle2, pushkey +import lock as lockmod def readbundle(ui, fh, fname, vfs=None): header = changegroup.readexactly(fh, 4) @@ -32,8 +33,8 @@ if alg is None: alg = changegroup.readexactly(fh, 2) return changegroup.cg1unpacker(fh, alg) - elif version == '2Y': - return bundle2.unbundle20(ui, fh, header=magic + version) + elif version.startswith('2'): + return bundle2.getunbundler(ui, fh, header=magic + version) else: raise util.Abort(_('%s: unknown bundle version %s') % (fname, version)) @@ -49,9 +50,17 @@ if version is None: raise ValueError('bundler do not support common obsmarker format') stream = obsolete.encodemarkers(markers, True, version=version) - return bundler.newpart('b2x:obsmarkers', data=stream) + return bundler.newpart('obsmarkers', data=stream) return None +def _canusebundle2(op): + """return true if a pull/push can use bundle2 + + Feel free to nuke this function when we drop the experimental option""" + return (op.repo.ui.configbool('experimental', 'bundle2-exp', False) + and op.remote.capable('bundle2')) + + class pushoperation(object): """A object that represent a single push operation @@ -192,8 +201,13 @@ if not pushop.remote.canpush(): raise util.Abort(_("destination does not support push")) # get local lock as we might write phase data - locallock = None + localwlock = locallock = None try: + # bundle2 push may receive a reply bundle touching bookmarks or other + # things requiring the wlock. Take it now to ensure proper ordering. + maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback') + if _canusebundle2(pushop) and maypushback: + localwlock = pushop.repo.wlock() locallock = pushop.repo.lock() pushop.locallocked = True except IOError, err: @@ -217,9 +231,7 @@ lock = pushop.remote.lock() try: _pushdiscovery(pushop) - if (pushop.repo.ui.configbool('experimental', 'bundle2-exp', - False) - and pushop.remote.capable('bundle2-exp')): + if _canusebundle2(pushop): _pushbundle2(pushop) _pushchangeset(pushop) _pushsyncphase(pushop) @@ -235,6 +247,8 @@ pushop.trmanager.release() if locallock is not None: locallock.release() + if localwlock is not None: + localwlock.release() return pushop @@ -421,7 +435,7 @@ # This exists to help extensions wrap steps if necessary b2partsgenmapping = {} -def b2partsgenerator(stepname): +def b2partsgenerator(stepname, idx=None): """decorator for function generating bundle2 part The function is added to the step -> function mapping and appended to the @@ -433,7 +447,10 @@ def dec(func): assert stepname not in b2partsgenmapping b2partsgenmapping[stepname] = func - b2partsgenorder.append(stepname) + if idx is None: + b2partsgenorder.append(stepname) + else: + b2partsgenorder.insert(idx, stepname) return func return dec @@ -453,10 +470,10 @@ pushop.remote, pushop.outgoing) if not pushop.force: - bundler.newpart('b2x:check:heads', data=iter(pushop.remoteheads)) + bundler.newpart('check:heads', data=iter(pushop.remoteheads)) b2caps = bundle2.bundle2caps(pushop.remote) version = None - cgversions = b2caps.get('b2x:changegroup') + cgversions = b2caps.get('changegroup') if not cgversions: # 3.1 and 3.2 ship with an empty value cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push', pushop.outgoing) @@ -468,7 +485,7 @@ cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push', pushop.outgoing, version=version) - cgpart = bundler.newpart('b2x:changegroup', data=cg) + cgpart = bundler.newpart('changegroup', data=cg) if version is not None: cgpart.addparam('version', version) def handlereply(op): @@ -484,13 +501,13 @@ if 'phases' in pushop.stepsdone: return b2caps = bundle2.bundle2caps(pushop.remote) - if not 'b2x:pushkey' in b2caps: + if not 'pushkey' in b2caps: return pushop.stepsdone.add('phases') part2node = [] enc = pushkey.encode for newremotehead in pushop.outdatedphases: - part = bundler.newpart('b2x:pushkey') + part = bundler.newpart('pushkey') part.addparam('namespace', enc('phases')) part.addparam('key', enc(newremotehead.hex())) part.addparam('old', enc(str(phases.draft))) @@ -527,13 +544,13 @@ if 'bookmarks' in pushop.stepsdone: return b2caps = bundle2.bundle2caps(pushop.remote) - if 'b2x:pushkey' not in b2caps: + if 'pushkey' not in b2caps: return pushop.stepsdone.add('bookmarks') part2book = [] enc = pushkey.encode for book, old, new in pushop.outbookmarks: - part = bundler.newpart('b2x:pushkey') + part = bundler.newpart('pushkey') part.addparam('namespace', enc('bookmarks')) part.addparam('key', enc(book)) part.addparam('old', enc(old)) @@ -577,7 +594,7 @@ # create reply capability capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo, allowpushback=pushback)) - bundler.newpart('b2x:replycaps', data=capsblob) + bundler.newpart('replycaps', data=capsblob) replyhandlers = [] for partgenname in b2partsgenorder: partgen = b2partsgenmapping[partgenname] @@ -845,15 +862,6 @@ def close(self): """close transaction if created""" if self._tr is not None: - repo = self.repo - p = lambda: self._tr.writepending() and repo.root or "" - repo.hook('b2x-pretransactionclose', throw=True, pending=p, - **self._tr.hookargs) - hookargs = dict(self._tr.hookargs) - def runhooks(): - repo.hook('b2x-transactionclose', **hookargs) - self._tr.addpostclose('b2x-hook-transactionclose', - lambda tr: repo._afterlock(runhooks)) self._tr.close() def release(self): @@ -876,8 +884,7 @@ try: pullop.trmanager = transactionmanager(repo, 'pull', remote.url()) _pulldiscovery(pullop) - if (pullop.repo.ui.configbool('experimental', 'bundle2-exp', False) - and pullop.remote.capable('bundle2-exp')): + if _canusebundle2(pullop): _pullbundle2(pullop) _pullchangeset(pullop) _pullphase(pullop) @@ -970,7 +977,7 @@ kwargs['common'] = pullop.common kwargs['heads'] = pullop.heads or pullop.rheads kwargs['cg'] = pullop.fetch - if 'b2x:listkeys' in remotecaps: + if 'listkeys' in remotecaps: kwargs['listkeys'] = ['phase', 'bookmarks'] if not pullop.fetch: pullop.repo.ui.status(_("no changes found\n")) @@ -984,8 +991,6 @@ kwargs['obsmarkers'] = True pullop.stepsdone.add('obsmarkers') _pullbundle2extraprepare(pullop, kwargs) - if kwargs.keys() == ['format']: - return # nothing to pull bundle = pullop.remote.getbundle('pull', **kwargs) try: op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction) @@ -1125,7 +1130,7 @@ def caps20to10(repo): """return a set with appropriate options to use bundle20 during getbundle""" - caps = set(['HG2Y']) + caps = set(['HG20']) capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo)) caps.add('bundle2=' + urllib.quote(capsblob)) return caps @@ -1138,7 +1143,7 @@ # This exists to help extensions wrap steps if necessary getbundle2partsmapping = {} -def getbundle2partsgenerator(stepname): +def getbundle2partsgenerator(stepname, idx=None): """decorator for function generating bundle2 part for getbundle The function is added to the step -> function mapping and appended to the @@ -1150,7 +1155,10 @@ def dec(func): assert stepname not in getbundle2partsmapping getbundle2partsmapping[stepname] = func - getbundle2partsorder.append(stepname) + if idx is None: + getbundle2partsorder.append(stepname) + else: + getbundle2partsorder.insert(idx, stepname) return func return dec @@ -1158,7 +1166,7 @@ **kwargs): """return a full bundle (with potentially multiple kind of parts) - Could be a bundle HG10 or a bundle HG2Y depending on bundlecaps + Could be a bundle HG10 or a bundle HG20 depending on bundlecaps passed. For now, the bundle can contain only changegroup, but this will changes when more part type will be available for bundle2. @@ -1170,7 +1178,10 @@ when the API of bundle is refined. """ # bundle10 case - if bundlecaps is None or 'HG2Y' not in bundlecaps: + usebundle2 = False + if bundlecaps is not None: + usebundle2 = util.any((cap.startswith('HG2') for cap in bundlecaps)) + if not usebundle2: if bundlecaps and not kwargs.get('cg', True): raise ValueError(_('request for bundle10 must include changegroup')) @@ -1206,7 +1217,7 @@ if kwargs.get('cg', True): # build changegroup bundle here. version = None - cgversions = b2caps.get('b2x:changegroup') + cgversions = b2caps.get('changegroup') if not cgversions: # 3.1 and 3.2 ship with an empty value cg = changegroup.getchangegroupraw(repo, source, heads=heads, common=common, @@ -1222,7 +1233,7 @@ version=version) if cg: - part = bundler.newpart('b2x:changegroup', data=cg) + part = bundler.newpart('changegroup', data=cg) if version is not None: part.addparam('version', version) @@ -1232,7 +1243,7 @@ """add parts containing listkeys namespaces to the requested bundle""" listkeys = kwargs.get('listkeys', ()) for namespace in listkeys: - part = bundler.newpart('b2x:listkeys') + part = bundler.newpart('listkeys') part.addparam('namespace', namespace) keys = repo.listkeys(namespace).items() part.data = pushkey.encodekeys(keys) @@ -1272,34 +1283,29 @@ If the push was raced as PushRaced exception is raised.""" r = 0 # need a transaction when processing a bundle2 stream - tr = None - lock = repo.lock() + wlock = lock = tr = None try: check_heads(repo, heads, 'uploading changes') # push can proceed if util.safehasattr(cg, 'params'): + r = None try: - tr = repo.transaction('unbundle') + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction(source) tr.hookargs['source'] = source tr.hookargs['url'] = url - tr.hookargs['bundle2-exp'] = '1' + tr.hookargs['bundle2'] = '1' r = bundle2.processbundle(repo, cg, lambda: tr).reply - p = lambda: tr.writepending() and repo.root or "" - repo.hook('b2x-pretransactionclose', throw=True, pending=p, - **tr.hookargs) - hookargs = dict(tr.hookargs) - def runhooks(): - repo.hook('b2x-transactionclose', **hookargs) - tr.addpostclose('b2x-hook-transactionclose', - lambda tr: repo._afterlock(runhooks)) tr.close() except Exception, exc: exc.duringunbundle2 = True + if r is not None: + exc._bundle2salvagedoutput = r.salvageoutput() raise else: + lock = repo.lock() r = changegroup.addchangegroup(repo, cg, source, url) finally: - if tr is not None: - tr.release() - lock.release() + lockmod.release(tr, lock, wlock) return r diff -r 612ed41ae359 -r e89f909edffa mercurial/extensions.py --- a/mercurial/extensions.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/extensions.py Thu Apr 16 20:57:51 2015 -0500 @@ -10,6 +10,7 @@ from i18n import _, gettext _extensions = {} +_aftercallbacks = {} _order = [] _ignore = ['hbisect', 'bookmarks', 'parentrevspec', 'interhg', 'inotify'] @@ -87,6 +88,8 @@ mod = importh(name) _extensions[shortname] = mod _order.append(shortname) + for fn in _aftercallbacks.get(shortname, []): + fn(loaded=True) return mod def loadall(ui): @@ -123,7 +126,45 @@ raise extsetup() # old extsetup with no ui argument -def wrapcommand(table, command, wrapper): + # Call aftercallbacks that were never met. + for shortname in _aftercallbacks: + if shortname in _extensions: + continue + + for fn in _aftercallbacks[shortname]: + fn(loaded=False) + +def afterloaded(extension, callback): + '''Run the specified function after a named extension is loaded. + + If the named extension is already loaded, the callback will be called + immediately. + + If the named extension never loads, the callback will be called after + all extensions have been loaded. + + The callback receives the named argument ``loaded``, which is a boolean + indicating whether the dependent extension actually loaded. + ''' + + if extension in _extensions: + callback(loaded=True) + else: + _aftercallbacks.setdefault(extension, []).append(callback) + +def bind(func, *args): + '''Partial function application + + Returns a new function that is the partial application of args and kwargs + to func. For example, + + f(1, 2, bar=3) === bind(f, 1)(2, bar=3)''' + assert callable(func) + def closure(*a, **kw): + return func(*(args + a), **kw) + return closure + +def wrapcommand(table, command, wrapper, synopsis=None, docstring=None): '''Wrap the command named `command' in table Replace command in the command table with wrapper. The wrapped command will @@ -135,6 +176,22 @@ where orig is the original (wrapped) function, and *args, **kwargs are the arguments passed to it. + + Optionally append to the command synopsis and docstring, used for help. + For example, if your extension wraps the ``bookmarks`` command to add the + flags ``--remote`` and ``--all`` you might call this function like so: + + synopsis = ' [-a] [--remote]' + docstring = """ + + The ``remotenames`` extension adds the ``--remote`` and ``--all`` (``-a``) + flags to the bookmarks command. Either flag will show the remote bookmarks + known to the repository; ``--remote`` will also supress the output of the + local bookmarks. + """ + + extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks, + synopsis, docstring) ''' assert callable(wrapper) aliases, entry = cmdutil.findcmd(command, table) @@ -144,15 +201,19 @@ break origfn = entry[0] - def wrap(*args, **kwargs): - return util.checksignature(wrapper)( - util.checksignature(origfn), *args, **kwargs) + wrap = bind(util.checksignature(wrapper), util.checksignature(origfn)) + + wrap.__module__ = getattr(origfn, '__module__') - wrap.__doc__ = getattr(origfn, '__doc__') - wrap.__module__ = getattr(origfn, '__module__') + doc = getattr(origfn, '__doc__') + if docstring is not None: + doc += docstring + wrap.__doc__ = doc newentry = list(entry) newentry[0] = wrap + if synopsis is not None: + newentry[2] += synopsis table[key] = tuple(newentry) return entry @@ -190,12 +251,10 @@ subclass trick. ''' assert callable(wrapper) - def wrap(*args, **kwargs): - return wrapper(origfn, *args, **kwargs) origfn = getattr(container, funcname) assert callable(origfn) - setattr(container, funcname, wrap) + setattr(container, funcname, bind(wrapper, origfn)) return origfn def _disabledpaths(strip_init=False): diff -r 612ed41ae359 -r e89f909edffa mercurial/filelog.py --- a/mercurial/filelog.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/filelog.py Thu Apr 16 20:57:51 2015 -0500 @@ -5,8 +5,8 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import error, revlog -import re +import error, mdiff, revlog +import re, struct _mdre = re.compile('\1\n') def parsemeta(text): @@ -29,7 +29,7 @@ def _censoredtext(text): m, offs = parsemeta(text) - return m and "censored" in m and not text[offs:] + return m and "censored" in m class filelog(revlog.revlog): def __init__(self, opener, path): @@ -64,7 +64,7 @@ node = self.node(rev) if self.renamed(node): return len(self.read(node)) - if self._iscensored(rev): + if self.iscensored(rev): return 0 # XXX if self.read(node).startswith("\1\n"), this returns (size+4) @@ -85,7 +85,7 @@ return False # censored files compare against the empty file - if self._iscensored(self.rev(node)): + if self.iscensored(self.rev(node)): return text != '' # renaming a file produces a different hash, even if the data @@ -101,12 +101,29 @@ super(filelog, self).checkhash(text, p1, p2, node, rev=rev) except error.RevlogError: if _censoredtext(text): - raise error.CensoredNodeError(self.indexfile, node) + raise error.CensoredNodeError(self.indexfile, node, text) raise - def _file(self, f): - return filelog(self.opener, f) - - def _iscensored(self, rev): + def iscensored(self, rev): """Check if a file revision is censored.""" return self.flags(rev) & revlog.REVIDX_ISCENSORED + + def _peek_iscensored(self, baserev, delta, flush): + """Quickly check if a delta produces a censored revision.""" + # Fragile heuristic: unless new file meta keys are added alphabetically + # preceding "censored", all censored revisions are prefixed by + # "\1\ncensored:". A delta producing such a censored revision must be a + # full-replacement delta, so we inspect the first and only patch in the + # delta for this prefix. + hlen = struct.calcsize(">lll") + if len(delta) <= hlen: + return False + + oldlen = self.rawsize(baserev) + newlen = len(delta) - hlen + if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen): + return False + + add = "\1\ncensored:" + addlen = len(add) + return newlen >= addlen and delta[hlen:hlen + addlen] == add diff -r 612ed41ae359 -r e89f909edffa mercurial/filemerge.py --- a/mercurial/filemerge.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/filemerge.py Thu Apr 16 20:57:51 2015 -0500 @@ -21,6 +21,8 @@ return ui.configlist("merge-tools", tool + "." + part, default) internals = {} +# Merge tools to document. +internalsdoc = {} def internaltool(name, trymerge, onfailure=None): '''return a decorator for populating internal merge tool table''' @@ -29,6 +31,7 @@ func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip() internals[fullname] = func internals['internal:' + name] = func + internalsdoc[fullname] = func func.trymerge = trymerge func.onfailure = onfailure return func @@ -301,7 +304,10 @@ replace = {'local': a, 'base': b, 'other': c, 'output': out} args = util.interpolate(r'\$', replace, args, lambda s: util.shellquote(util.localpath(s))) - r = ui.system(toolpath + ' ' + args, cwd=repo.root, environ=env) + cmd = toolpath + ' ' + args + repo.ui.debug('launching merge tool: %s\n' % cmd) + r = ui.system(cmd, cwd=repo.root, environ=env) + repo.ui.debug('merge tool returned: %s\n' % r) return True, r return False, 0 diff -r 612ed41ae359 -r e89f909edffa mercurial/fileset.py --- a/mercurial/fileset.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/fileset.py Thu Apr 16 20:57:51 2015 -0500 @@ -186,7 +186,7 @@ def func(mctx, a, b): if a[0] == 'symbol' and a[1] in symbols: return symbols[a[1]](mctx, b) - raise error.ParseError(_("not a function: %s") % a[1]) + raise error.UnknownIdentifier(a[1], symbols.keys()) def getlist(x): if not x: @@ -233,7 +233,7 @@ getargs(x, 0, 0, _("resolved takes no arguments")) if mctx.ctx.rev() is not None: return [] - ms = merge.mergestate(mctx.ctx._repo) + ms = merge.mergestate(mctx.ctx.repo()) return [f for f in mctx.subset if f in ms and ms[f] == 'r'] def unresolved(mctx, x): @@ -244,7 +244,7 @@ getargs(x, 0, 0, _("unresolved takes no arguments")) if mctx.ctx.rev() is not None: return [] - ms = merge.mergestate(mctx.ctx._repo) + ms = merge.mergestate(mctx.ctx.repo()) return [f for f in mctx.subset if f in ms and ms[f] == 'u'] def hgignore(mctx, x): @@ -253,9 +253,19 @@ """ # i18n: "hgignore" is a keyword getargs(x, 0, 0, _("hgignore takes no arguments")) - ignore = mctx.ctx._repo.dirstate._ignore + ignore = mctx.ctx.repo().dirstate._ignore return [f for f in mctx.subset if ignore(f)] +def portable(mctx, x): + """``portable()`` + File that has a portable name. (This doesn't include filenames with case + collisions.) + """ + # i18n: "portable" is a keyword + getargs(x, 0, 0, _("portable takes no arguments")) + checkwinfilename = util.checkwinfilename + return [f for f in mctx.subset if checkwinfilename(f) is None] + def grep(mctx, x): """``grep(regex)`` File contains the given regular expression. @@ -398,7 +408,7 @@ def m(s): return (s == pat) else: - m = matchmod.match(ctx._repo.root, '', [pat], ctx=ctx) + m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx) return [sub for sub in sstate if m(sub)] else: return [sub for sub in sstate] @@ -416,6 +426,7 @@ 'ignored': ignored, 'hgignore': hgignore, 'modified': modified, + 'portable': portable, 'removed': removed, 'resolved': resolved, 'size': size, @@ -493,7 +504,7 @@ unknown = _intree(['unknown'], tree) ignored = _intree(['ignored'], tree) - r = ctx._repo + r = ctx.repo() status = r.status(ctx.p1(), ctx, unknown=unknown, ignored=ignored, clean=True) subset = [] diff -r 612ed41ae359 -r e89f909edffa mercurial/formatter.py --- a/mercurial/formatter.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/formatter.py Thu Apr 16 20:57:51 2015 -0500 @@ -98,6 +98,8 @@ def _jsonifyobj(v): if isinstance(v, tuple): return '[' + ', '.join(_jsonifyobj(e) for e in v) + ']' + elif v is None: + return 'null' elif v is True: return 'true' elif v is False: diff -r 612ed41ae359 -r e89f909edffa mercurial/graphmod.py --- a/mercurial/graphmod.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/graphmod.py Thu Apr 16 20:57:51 2015 -0500 @@ -122,7 +122,7 @@ heappush(pendingheap, -currentrev) pendingset.add(currentrev) # iterates on pending rev until after the current rev have been - # processeed. + # processed. rev = None while rev != currentrev: rev = -heappop(pendingheap) diff -r 612ed41ae359 -r e89f909edffa mercurial/help.py --- a/mercurial/help.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,11 +6,13 @@ # GNU General Public License version 2 or any later version. from i18n import gettext, _ -import itertools, os +import itertools, os, textwrap import error import extensions, revset, fileset, templatekw, templatefilters, filemerge +import templater import encoding, util, minirst import cmdutil +import hgweb.webcommands as webcommands def listexts(header, exts, indent=1, showdeprecated=False): '''return a text listing of the given extensions''' @@ -171,7 +173,7 @@ def addtopichook(topic, rewriter): helphooks.setdefault(topic, []).append(rewriter) -def makeitemsdoc(topic, doc, marker, items): +def makeitemsdoc(topic, doc, marker, items, dedent=False): """Extract docstring from the items key to function mapping, build a .single documentation block and use it to overwrite the marker in doc """ @@ -181,27 +183,36 @@ if not text: continue text = gettext(text) + if dedent: + text = textwrap.dedent(text) lines = text.splitlines() doclines = [(lines[0])] for l in lines[1:]: # Stop once we find some Python doctest if l.strip().startswith('>>>'): break - doclines.append(' ' + l.strip()) + if dedent: + doclines.append(l.rstrip()) + else: + doclines.append(' ' + l.strip()) entries.append('\n'.join(doclines)) entries = '\n\n'.join(entries) return doc.replace(marker, entries) -def addtopicsymbols(topic, marker, symbols): +def addtopicsymbols(topic, marker, symbols, dedent=False): def add(topic, doc): - return makeitemsdoc(topic, doc, marker, symbols) + return makeitemsdoc(topic, doc, marker, symbols, dedent=dedent) addtopichook(topic, add) addtopicsymbols('filesets', '.. predicatesmarker', fileset.symbols) -addtopicsymbols('merge-tools', '.. internaltoolsmarker', filemerge.internals) +addtopicsymbols('merge-tools', '.. internaltoolsmarker', + filemerge.internalsdoc) addtopicsymbols('revsets', '.. predicatesmarker', revset.symbols) addtopicsymbols('templates', '.. keywordsmarker', templatekw.dockeywords) addtopicsymbols('templates', '.. filtersmarker', templatefilters.filters) +addtopicsymbols('templates', '.. functionsmarker', templater.funcs) +addtopicsymbols('hgweb', '.. webcommandsmarker', webcommands.commands, + dedent=True) def help_(ui, name, unknowncmd=False, full=True, **opts): ''' diff -r 612ed41ae359 -r e89f909edffa mercurial/help/config.txt --- a/mercurial/help/config.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/config.txt Thu Apr 16 20:57:51 2015 -0500 @@ -808,6 +808,35 @@ changeset to tag is in ``$HG_NODE``. Name of tag is in ``$HG_TAG``. Tag is local if ``$HG_LOCAL=1``, in repository if ``$HG_LOCAL=0``. +``pretxnopen`` + Run before any new repository transaction is open. The reason for the + transaction will be in ``$HG_TXNNAME`` and a unique identifier for the + transaction will be in ``HG_TXNID``. A non-zero status will prevent the + transaction from being opened. + +``pretxnclose`` + Run right before the transaction is actually finalized. Any + repository change will be visible to the hook program. This lets you + validate the transaction content or change it. Exit status 0 allows + the commit to proceed. Non-zero status will cause the transaction to + be rolled back. The reason for the transaction opening will be in + ``$HG_TXNNAME`` and a unique identifier for the transaction will be in + ``HG_TXNID``. The rest of the available data will vary according the + transaction type. New changesets will add ``$HG_NODE`` (id of the + first added changeset), ``$HG_URL`` and ``$HG_SOURCE`` variables, + bookmarks and phases changes will set ``HG_BOOKMARK_MOVED`` and + ``HG_PHASES_MOVED`` to ``1``, etc. + +``txnclose`` + Run after any repository transaction has been commited. At this + point, the transaction can no longer be rolled back. The hook will run + after the lock is released. see ``pretxnclose`` docs for details about + available variables. + +``txnabort`` + Run when a transaction is aborted. see ``pretxnclose`` docs for details about + available variables. + ``pretxnchangegroup`` Run after a changegroup has been added via push, pull or unbundle, but before the transaction has been committed. Changegroup is @@ -1409,6 +1438,9 @@ backslash character (``\``)). Default is False. +``statuscopies`` + Display copies in the status command. + ``ssh`` command to use for SSH connections. Default is ``ssh``. diff -r 612ed41ae359 -r e89f909edffa mercurial/help/hg.1.txt --- a/mercurial/help/hg.1.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/hg.1.txt Thu Apr 16 20:57:51 2015 -0500 @@ -112,7 +112,7 @@ Copying """"""" -Copyright (C) 2005-2014 Matt Mackall. +Copyright (C) 2005-2015 Matt Mackall. Free use of this software is granted under the terms of the GNU General Public License version 2 or any later version. diff -r 612ed41ae359 -r e89f909edffa mercurial/help/hgignore.5.txt --- a/mercurial/help/hgignore.5.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/hgignore.5.txt Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@ Copying ======= This manual page is copyright 2006 Vadim Gelfer. -Mercurial is copyright 2005-2014 Matt Mackall. +Mercurial is copyright 2005-2015 Matt Mackall. Free use of this software is granted under the terms of the GNU General Public License version 2 or any later version. diff -r 612ed41ae359 -r e89f909edffa mercurial/help/hgrc.5.txt --- a/mercurial/help/hgrc.5.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/hgrc.5.txt Thu Apr 16 20:57:51 2015 -0500 @@ -34,7 +34,7 @@ Copying ======= This manual page is copyright 2005 Bryan O'Sullivan. -Mercurial is copyright 2005-2014 Matt Mackall. +Mercurial is copyright 2005-2015 Matt Mackall. Free use of this software is granted under the terms of the GNU General Public License version 2 or any later version. diff -r 612ed41ae359 -r e89f909edffa mercurial/help/hgweb.txt --- a/mercurial/help/hgweb.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/hgweb.txt Thu Apr 16 20:57:51 2015 -0500 @@ -48,3 +48,39 @@ The ``collections`` section is deprecated and has been superseded by ``paths``. + +URLs and Common Arguments +========================= + +URLs under each repository have the form ``/{command}[/{arguments}]`` +where ``{command}`` represents the name of a command or handler and +``{arguments}`` represents any number of additional URL parameters +to that command. + +The web server has a default style associated with it. Styles map to +a collection of named templates. Each template is used to render a +specific piece of data, such as a changeset or diff. + +The style for the current request can be overwritten two ways. First, +if ``{command}`` contains a hyphen (``-``), the text before the hyphen +defines the style. For example, ``/atom-log`` will render the ``log`` +command handler with the ``atom`` style. The second way to set the +style is with the ``style`` query string argument. For example, +``/log?style=atom``. The hyphenated URL parameter is preferred. + +Not all templates are available for all styles. Attempting to use +a style that doesn't have all templates defined may result in an error +rendering the page. + +Many commands take a ``{revision}`` URL parameter. This defines the +changeset to operate on. This is commonly specified as the short, +12 digit hexidecimal abbreviation for the full 40 character unique +revision identifier. However, any value described by +:hg:`help revisions` typically works. + +Commands and URLs +================= + +The following web commands and their URLs are available: + + .. webcommandsmarker diff -r 612ed41ae359 -r e89f909edffa mercurial/help/subrepos.txt --- a/mercurial/help/subrepos.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/subrepos.txt Thu Apr 16 20:57:51 2015 -0500 @@ -78,7 +78,7 @@ :add: add does not recurse in subrepos unless -S/--subrepos is specified. However, if you specify the full path of a file in a subrepo, it will be added even without -S/--subrepos specified. - Git and Subversion subrepositories are currently silently + Subversion subrepositories are currently silently ignored. :addremove: addremove does not recurse into subrepos unless @@ -91,7 +91,7 @@ -S/--subrepos is specified. :cat: cat currently only handles exact file matches in subrepos. - Git and Subversion subrepositories are currently ignored. + Subversion subrepositories are currently ignored. :commit: commit creates a consistent snapshot of the state of the entire project and its subrepositories. If any subrepositories @@ -109,6 +109,10 @@ elements. Git subrepositories do not support --include/--exclude. Subversion subrepositories are currently silently ignored. +:files: files does not recurse into subrepos unless -S/--subrepos is + specified. Git and Subversion subrepositories are currently + silently ignored. + :forget: forget currently only handles exact file matches in subrepos. Git and Subversion subrepositories are currently silently ignored. diff -r 612ed41ae359 -r e89f909edffa mercurial/help/templates.txt --- a/mercurial/help/templates.txt Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/help/templates.txt Thu Apr 16 20:57:51 2015 -0500 @@ -41,39 +41,7 @@ In addition to filters, there are some basic built-in functions: -- date(date[, fmt]) - -- diff([includepattern [, excludepattern]]) - -- fill(text[, width]) - -- get(dict, key) - -- if(expr, then[, else]) - -- ifcontains(expr, expr, then[, else]) - -- ifeq(expr, expr, then[, else]) - -- join(list, sep) - -- label(label, expr) - -- pad(text, width[, fillchar, right]) - -- revset(query[, formatargs]) - -- rstdoc(text, style) - -- shortest(node) - -- startswith(string, text) - -- strip(text[, chars]) - -- sub(pat, repl, expr) - -- word(number, text[, separator]) +.. functionsmarker Also, for any expression that returns a list, there is a list operator: diff -r 612ed41ae359 -r e89f909edffa mercurial/hg.py --- a/mercurial/hg.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/hg.py Thu Apr 16 20:57:51 2015 -0500 @@ -34,7 +34,11 @@ else: y = None return x, y - revs = revs and list(revs) or [] + if revs: + revs = list(revs) + else: + revs = [] + if not peer.capable('branchmap'): if branches: raise util.Abort(_("remote branch lookup not supported")) @@ -239,6 +243,12 @@ try: hardlink = None num = 0 + closetopic = [None] + def prog(topic, pos): + if pos is None: + closetopic[0] = topic + else: + ui.progress(topic, pos + num) srcpublishing = srcrepo.ui.configbool('phases', 'publish', True) srcvfs = scmutil.vfs(srcrepo.sharedpath) dstvfs = scmutil.vfs(destpath) @@ -255,12 +265,16 @@ # lock to avoid premature writing to the target destlock = lock.lock(dstvfs, lockfile) hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f), - hardlink) + hardlink, progress=prog) num += n if hardlink: ui.debug("linked %d files\n" % num) + if closetopic[0]: + ui.progress(closetopic[0], None) else: ui.debug("copied %d files\n" % num) + if closetopic[0]: + ui.progress(closetopic[0], None) return destlock except: # re-raises release(destlock) @@ -672,7 +686,9 @@ for key, val in src.configitems(sect): dst.setconfig(sect, key, val, 'copied') v = src.config('web', 'cacerts') - if v: + if v == '!': + dst.setconfig('web', 'cacerts', v, 'copied') + elif v: dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied') return dst diff -r 612ed41ae359 -r e89f909edffa mercurial/hgweb/webcommands.py --- a/mercurial/hgweb/webcommands.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/hgweb/webcommands.py Thu Apr 16 20:57:51 2015 -0500 @@ -13,27 +13,58 @@ from common import paritygen, staticfile, get_contact, ErrorResponse from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND from mercurial import graphmod, patch -from mercurial import help as helpmod from mercurial import scmutil from mercurial.i18n import _ from mercurial.error import ParseError, RepoLookupError, Abort from mercurial import revset -# __all__ is populated with the allowed commands. Be sure to add to it if -# you're adding a new command, or the new command won't work. +__all__ = [] +commands = {} + +class webcommand(object): + """Decorator used to register a web command handler. + + The decorator takes as its positional arguments the name/path the + command should be accessible under. + + Usage: + + @webcommand('mycommand') + def mycommand(web, req, tmpl): + pass + """ + + def __init__(self, name): + self.name = name -__all__ = [ - 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev', - 'manifest', 'tags', 'bookmarks', 'branches', 'summary', 'filediff', 'diff', - 'comparison', 'annotate', 'filelog', 'archive', 'static', 'graph', 'help', -] + def __call__(self, func): + __all__.append(self.name) + commands[self.name] = func + return func + +@webcommand('log') +def log(web, req, tmpl): + """ + /log[/{revision}[/{path}]] + -------------------------- -def log(web, req, tmpl): + Show repository or file history. + + For URLs of the form ``/log/{revision}``, a list of changesets starting at + the specified changeset identifier is shown. If ``{revision}`` is not + defined, the default is ``tip``. This form is equivalent to the + ``changelog`` handler. + + For URLs of the form ``/log/{revision}/{file}``, the history for a specific + file will be shown. This form is equivalent to the ``filelog`` handler. + """ + if 'file' in req.form and req.form['file'][0]: return filelog(web, req, tmpl) else: return changelog(web, req, tmpl) +@webcommand('rawfile') def rawfile(web, req, tmpl): guessmime = web.configbool('web', 'guessmime', False) @@ -59,7 +90,10 @@ if guessmime: mt = mimetypes.guess_type(path)[0] if mt is None: - mt = util.binary(text) and 'application/binary' or 'text/plain' + if util.binary(text): + mt = 'application/binary' + else: + mt = 'text/plain' if mt.startswith('text/'): mt += '; charset="%s"' % encoding.encoding @@ -98,7 +132,26 @@ rename=webutil.renamelink(fctx), permissions=fctx.manifest().flags(f)) +@webcommand('file') def file(web, req, tmpl): + """ + /file/{revision}[/{path}] + ------------------------- + + Show information about a directory or file in the repository. + + Info about the ``path`` given as a URL parameter will be rendered. + + If ``path`` is a directory, information about the entries in that + directory will be rendered. This form is equivalent to the ``manifest`` + handler. + + If ``path`` is a file, information about that file will be shown via + the ``filerevision`` template. + + If ``path`` is not defined, information about the root directory will + be rendered. + """ path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0]) if not path: return manifest(web, req, tmpl) @@ -187,7 +240,7 @@ mfunc = revset.match(web.repo.ui, revdef) try: - revs = mfunc(web.repo, revset.baseset(web.repo)) + revs = mfunc(web.repo) return MODE_REVSET, revs # ParseError: wrongly placed tokens, wrongs arguments, etc # RepoLookupError: no such revision, e.g. in 'revision:' @@ -267,7 +320,31 @@ modedesc=searchfunc[1], showforcekw=showforcekw, showunforcekw=showunforcekw) +@webcommand('changelog') def changelog(web, req, tmpl, shortlog=False): + """ + /changelog[/{revision}] + ----------------------- + + Show information about multiple changesets. + + If the optional ``revision`` URL argument is absent, information about + all changesets starting at ``tip`` will be rendered. If the ``revision`` + argument is present, changesets will be shown starting from the specified + revision. + + If ``revision`` is absent, the ``rev`` query string argument may be + defined. This will perform a search for changesets. + + The argument for ``rev`` can be a single revision, a revision set, + or a literal keyword to search for in changeset data (equivalent to + :hg:`log -k`. + + The ``revcount`` query string argument defines the maximum numbers of + changesets to render. + + For non-searches, the ``changelog`` template will be rendered. + """ query = '' if 'node' in req.form: @@ -291,7 +368,11 @@ entry['parity'] = parity.next() yield entry - revcount = shortlog and web.maxshortchanges or web.maxchanges + if shortlog: + revcount = web.maxshortchanges + else: + revcount = web.maxchanges + if 'revcount' in req.form: try: revcount = int(req.form.get('revcount', [revcount])[0]) @@ -326,63 +407,41 @@ archives=web.archivelist("tip"), revcount=revcount, morevars=morevars, lessvars=lessvars, query=query) +@webcommand('shortlog') def shortlog(web, req, tmpl): + """ + /shortlog + --------- + + Show basic information about a set of changesets. + + This accepts the same parameters as the ``changelog`` handler. The only + difference is the ``shortlog`` template will be rendered instead of the + ``changelog`` template. + """ return changelog(web, req, tmpl, shortlog=True) +@webcommand('changeset') def changeset(web, req, tmpl): - ctx = webutil.changectx(web.repo, req) - basectx = webutil.basechangectx(web.repo, req) - if basectx is None: - basectx = ctx.p1() - showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node()) - showbookmarks = webutil.showbookmark(web.repo, tmpl, 'changesetbookmark', - ctx.node()) - showbranch = webutil.nodebranchnodefault(ctx) + """ + /changeset[/{revision}] + ----------------------- - files = [] - parity = paritygen(web.stripecount) - for blockno, f in enumerate(ctx.files()): - template = f in ctx and 'filenodelink' or 'filenolink' - files.append(tmpl(template, - node=ctx.hex(), file=f, blockno=blockno + 1, - parity=parity.next())) - - style = web.config('web', 'style', 'paper') - if 'style' in req.form: - style = req.form['style'][0] - - parity = paritygen(web.stripecount) - diffs = webutil.diffs(web.repo, tmpl, ctx, basectx, None, parity, style) + Show information about a single changeset. - parity = paritygen(web.stripecount) - diffstatgen = webutil.diffstatgen(ctx, basectx) - diffstat = webutil.diffstat(tmpl, ctx, diffstatgen, parity) + A URL path argument is the changeset identifier to show. See ``hg help + revisions`` for possible values. If not defined, the ``tip`` changeset + will be shown. - return tmpl('changeset', - diff=diffs, - rev=ctx.rev(), - node=ctx.hex(), - parent=tuple(webutil.parents(ctx)), - child=webutil.children(ctx), - basenode=basectx.hex(), - changesettag=showtags, - changesetbookmark=showbookmarks, - changesetbranch=showbranch, - author=ctx.user(), - desc=ctx.description(), - extra=ctx.extra(), - date=ctx.date(), - files=files, - diffsummary=lambda **x: webutil.diffsummary(diffstatgen), - diffstat=diffstat, - archives=web.archivelist(ctx.hex()), - tags=webutil.nodetagsdict(web.repo, ctx.node()), - bookmarks=webutil.nodebookmarksdict(web.repo, ctx.node()), - branch=webutil.nodebranchnodefault(ctx), - inbranch=webutil.nodeinbranch(web.repo, ctx), - branches=webutil.nodebranchdict(web.repo, ctx)) + The ``changeset`` template is rendered. Contents of the ``changesettag``, + ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many + templates related to diffs may all be used to produce the output. + """ + ctx = webutil.changectx(web.repo, req) -rev = changeset + return tmpl('changeset', **webutil.changesetentry(web, req, tmpl, ctx)) + +rev = webcommand('rev')(changeset) def decodepath(path): """Hook for mapping a path in the repository to a path in the @@ -392,7 +451,23 @@ the virtual file system presented by the manifest command below.""" return path +@webcommand('manifest') def manifest(web, req, tmpl): + """ + /manifest[/{revision}[/{path}]] + ------------------------------- + + Show information about a directory. + + If the URL path arguments are defined, information about the root + directory for the ``tip`` changeset will be shown. + + Because this handler can only show information for directories, it + is recommended to use the ``file`` handler instead, as it can handle both + directories and files. + + The ``manifest`` template will be rendered for this handler. + """ ctx = webutil.changectx(web.repo, req) path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0]) mf = ctx.manifest() @@ -474,7 +549,18 @@ inbranch=webutil.nodeinbranch(web.repo, ctx), branches=webutil.nodebranchdict(web.repo, ctx)) +@webcommand('tags') def tags(web, req, tmpl): + """ + /tags + ----- + + Show information about tags. + + No arguments are accepted. + + The ``tags`` template is rendered. + """ i = list(reversed(web.repo.tagslist())) parity = paritygen(web.stripecount) @@ -496,7 +582,18 @@ entriesnotip=lambda **x: entries(True, False, **x), latestentry=lambda **x: entries(True, True, **x)) +@webcommand('bookmarks') def bookmarks(web, req, tmpl): + """ + /bookmarks + ---------- + + Show information about bookmarks. + + No arguments are accepted. + + The ``bookmarks`` template is rendered. + """ i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo] parity = paritygen(web.stripecount) @@ -516,7 +613,20 @@ entries=lambda **x: entries(latestonly=False, **x), latestentry=lambda **x: entries(latestonly=True, **x)) +@webcommand('branches') def branches(web, req, tmpl): + """ + /branches + --------- + + Show information about branches. + + All known branches are contained in the output, even closed branches. + + No arguments are accepted. + + The ``branches`` template is rendered. + """ tips = [] heads = web.repo.heads() parity = paritygen(web.stripecount) @@ -547,7 +657,19 @@ entries=lambda **x: entries(0, **x), latestentry=lambda **x: entries(1, **x)) +@webcommand('summary') def summary(web, req, tmpl): + """ + /summary + -------- + + Show a summary of repository state. + + Information about the latest changesets, bookmarks, tags, and branches + is captured by this handler. + + The ``summary`` template is rendered. + """ i = reversed(web.repo.tagslist()) def tagentries(**map): @@ -632,7 +754,19 @@ node=tip.hex(), archives=web.archivelist("tip")) +@webcommand('filediff') def filediff(web, req, tmpl): + """ + /diff/{revision}/{path} + ----------------------- + + Show how a file changed in a particular commit. + + The ``filediff`` template is rendered. + + This hander is registered under both the ``/diff`` and ``/filediff`` + paths. ``/diff`` is used in modern code. + """ fctx, ctx = None, None try: fctx = webutil.filectx(web.repo, req) @@ -656,8 +790,12 @@ style = req.form['style'][0] diffs = webutil.diffs(web.repo, tmpl, ctx, None, [path], parity, style) - rename = fctx and webutil.renamelink(fctx) or [] - ctx = fctx and fctx or ctx + if fctx: + rename = webutil.renamelink(fctx) + ctx = fctx + else: + rename = [] + ctx = ctx return tmpl("filediff", file=path, node=hex(n), @@ -672,9 +810,25 @@ child=webutil.children(ctx), diff=diffs) -diff = filediff +diff = webcommand('diff')(filediff) + +@webcommand('comparison') +def comparison(web, req, tmpl): + """ + /comparison/{revision}/{path} + ----------------------------- -def comparison(web, req, tmpl): + Show a comparison between the old and new versions of a file from changes + made on a particular revision. + + This is similar to the ``diff`` handler. However, this form features + a split or side-by-side diff rather than a unified diff. + + The ``context`` query string argument can be used to control the lines of + context in the diff. + + The ``filecomparison`` template is rendered. + """ ctx = webutil.changectx(web.repo, req) if 'file' not in req.form: raise ErrorResponse(HTTP_NOT_FOUND, 'file not given') @@ -732,7 +886,16 @@ rightnode=hex(rightnode), comparison=comparison) +@webcommand('annotate') def annotate(web, req, tmpl): + """ + /annotate/{revision}/{path} + --------------------------- + + Show changeset information for each line in a file. + + The ``fileannotate`` template is rendered. + """ fctx = webutil.filectx(web.repo, req) f = fctx.path() parity = paritygen(web.stripecount) @@ -764,6 +927,7 @@ "file": f.path(), "targetline": targetline, "line": l, + "lineno": lineno + 1, "lineid": "l%d" % (lineno + 1), "linenumber": "% 6d" % (lineno + 1), "revdate": f.date()} @@ -784,7 +948,19 @@ child=webutil.children(fctx), permissions=fctx.manifest().flags(f)) +@webcommand('filelog') def filelog(web, req, tmpl): + """ + /filelog/{revision}/{path} + -------------------------- + + Show information about the history of a file in the repository. + + The ``revcount`` query string argument can be defined to control the + maximum number of entries to show. + + The ``filelog`` template will be rendered. + """ try: fctx = webutil.filectx(web.repo, req) @@ -862,7 +1038,27 @@ latestentry=latestentry, revcount=revcount, morevars=morevars, lessvars=lessvars) +@webcommand('archive') def archive(web, req, tmpl): + """ + /archive/{revision}.{format}[/{path}] + ------------------------------------- + + Obtain an archive of repository content. + + The content and type of the archive is defined by a URL path parameter. + ``format`` is the file extension of the archive type to be generated. e.g. + ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your + server configuration. + + The optional ``path`` URL parameter controls content to include in the + archive. If omitted, every file in the specified revision is present in the + archive. If included, only the specified file or contents of the specified + directory will be included in the archive. + + No template is used for this handler. Raw, binary content is generated. + """ + type_ = req.form.get('type', [None])[0] allowed = web.configlist("web", "allow_archive") key = req.form['node'][0] @@ -911,6 +1107,7 @@ return [] +@webcommand('static') def static(web, req, tmpl): fname = req.form['file'][0] # a repo owner may set web.static in .hg/hgrc to get any file @@ -924,7 +1121,24 @@ staticfile(static, fname, req) return [] +@webcommand('graph') def graph(web, req, tmpl): + """ + /graph[/{revision}] + ------------------- + + Show information about the graphical topology of the repository. + + Information rendered by this handler can be used to create visual + representations of repository topology. + + The ``revision`` URL parameter controls the starting changeset. + + The ``revcount`` query string argument can define the number of changesets + to show information for. + + This handler will render the ``graph`` template. + """ ctx = webutil.changectx(web.repo, req) rev = ctx.rev() @@ -1047,8 +1261,23 @@ doc = _('(no help text available)') return doc +@webcommand('help') def help(web, req, tmpl): + """ + /help[/{topic}] + --------------- + + Render help documentation. + + This web command is roughly equivalent to :hg:`help`. If a ``topic`` + is defined, that help topic will be rendered. If not, an index of + available help topics will be rendered. + + The ``help`` template will be rendered when requesting help for a topic. + ``helptopics`` will be rendered for the index of help topics. + """ from mercurial import commands # avoid cycle + from mercurial import help as helpmod # avoid cycle topicname = req.form.get('node', [None])[0] if not topicname: diff -r 612ed41ae359 -r e89f909edffa mercurial/hgweb/webutil.py --- a/mercurial/hgweb/webutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/hgweb/webutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -10,7 +10,7 @@ from mercurial import match, patch, error, ui, util, pathutil, context from mercurial.i18n import _ from mercurial.node import hex, nullid -from common import ErrorResponse +from common import ErrorResponse, paritygen from common import HTTP_NOT_FOUND import difflib @@ -138,9 +138,10 @@ yield d def parents(ctx, hide=None): - if (isinstance(ctx, context.basefilectx) and - ctx.changectx().rev() != ctx.linkrev()): - return _siblings([ctx._repo[ctx.linkrev()]], hide) + if isinstance(ctx, context.basefilectx): + introrev = ctx.introrev() + if ctx.changectx().rev() != introrev: + return _siblings([ctx.repo()[introrev]], hide) return _siblings(ctx.parents(), hide) def children(ctx, hide=None): @@ -278,6 +279,62 @@ "branches": nodebranchdict(repo, ctx) } +def changesetentry(web, req, tmpl, ctx): + '''Obtain a dictionary to be used to render the "changeset" template.''' + + showtags = showtag(web.repo, tmpl, 'changesettag', ctx.node()) + showbookmarks = showbookmark(web.repo, tmpl, 'changesetbookmark', + ctx.node()) + showbranch = nodebranchnodefault(ctx) + + files = [] + parity = paritygen(web.stripecount) + for blockno, f in enumerate(ctx.files()): + template = f in ctx and 'filenodelink' or 'filenolink' + files.append(tmpl(template, + node=ctx.hex(), file=f, blockno=blockno + 1, + parity=parity.next())) + + basectx = basechangectx(web.repo, req) + if basectx is None: + basectx = ctx.p1() + + style = web.config('web', 'style', 'paper') + if 'style' in req.form: + style = req.form['style'][0] + + parity = paritygen(web.stripecount) + diff = diffs(web.repo, tmpl, ctx, basectx, None, parity, style) + + parity = paritygen(web.stripecount) + diffstatsgen = diffstatgen(ctx, basectx) + diffstats = diffstat(tmpl, ctx, diffstatsgen, parity) + + return dict( + diff=diff, + rev=ctx.rev(), + node=ctx.hex(), + parent=tuple(parents(ctx)), + child=children(ctx), + basenode=basectx.hex(), + changesettag=showtags, + changesetbookmark=showbookmarks, + changesetbranch=showbranch, + author=ctx.user(), + desc=ctx.description(), + extra=ctx.extra(), + date=ctx.date(), + phase=ctx.phasestr(), + files=files, + diffsummary=lambda **x: diffsummary(diffstatsgen), + diffstat=diffstats, + archives=web.archivelist(ctx.hex()), + tags=nodetagsdict(web.repo, ctx.node()), + bookmarks=nodebookmarksdict(web.repo, ctx.node()), + branch=nodebranchnodefault(ctx), + inbranch=nodeinbranch(web.repo, ctx), + branches=nodebranchdict(web.repo, ctx)) + def listfilediffs(tmpl, files, node, max): for f in files[:max]: yield tmpl('filedifflink', node=hex(node), file=f) @@ -295,7 +352,7 @@ blockcount = countgen() def prettyprintlines(diff, blockno): for lineno, l in enumerate(diff.splitlines(True)): - lineno = "%d.%d" % (blockno, lineno + 1) + difflineno = "%d.%d" % (blockno, lineno + 1) if l.startswith('+'): ltype = "difflineplus" elif l.startswith('-'): @@ -306,8 +363,9 @@ ltype = "diffline" yield tmpl(ltype, line=l, - lineid="l%s" % lineno, - linenumber="% 8s" % lineno) + lineno=lineno + 1, + lineid="l%s" % difflineno, + linenumber="% 8s" % difflineno) if files: m = match.exact(repo.root, repo.getcwd(), files) @@ -317,7 +375,10 @@ diffopts = patch.diffopts(repo.ui, untrusted=True) if basectx is None: parents = ctx.parents() - node1 = parents and parents[0].node() or nullid + if parents: + node1 = parents[0].node() + else: + node1 = nullid else: node1 = basectx.node() node2 = ctx.node() @@ -345,8 +406,10 @@ return tmpl('comparisonline', type=type, lineid=lineid, + leftlineno=leftlineno, leftlinenumber="% 6s" % (leftlineno or ''), leftline=leftline or '', + rightlineno=rightlineno, rightlinenumber="% 6s" % (rightlineno or ''), rightline=rightline or '') diff -r 612ed41ae359 -r e89f909edffa mercurial/hook.py --- a/mercurial/hook.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/hook.py Thu Apr 16 20:57:51 2015 -0500 @@ -200,6 +200,11 @@ r = _pythonhook(ui, repo, name, hname, hookfn, args, throw) or r else: r = _exthook(ui, repo, hname, cmd, args, throw) or r + + # The stderr is fully buffered on Windows when connected to a pipe. + # A forcible flush is required to make small stderr data in the + # remote side available to the client immediately. + sys.stderr.flush() finally: if _redirect and oldstdout >= 0: os.dup2(oldstdout, stdoutno) diff -r 612ed41ae359 -r e89f909edffa mercurial/httpclient/__init__.py --- a/mercurial/httpclient/__init__.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/httpclient/__init__.py Thu Apr 16 20:57:51 2015 -0500 @@ -330,7 +330,10 @@ elif use_ssl is None: use_ssl = (port == 443) elif port is None: - port = (use_ssl and 443 or 80) + if use_ssl: + port = 443 + else: + port = 80 self.port = port if use_ssl and not socketutil.have_ssl: raise Exception('ssl requested but unavailable on this Python') diff -r 612ed41ae359 -r e89f909edffa mercurial/localrepo.py --- a/mercurial/localrepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/localrepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -107,14 +107,14 @@ return self._repo.known(nodes) def getbundle(self, source, heads=None, common=None, bundlecaps=None, - format='HG10', **kwargs): + **kwargs): cg = exchange.getbundle(self._repo, source, heads=heads, common=common, bundlecaps=bundlecaps, **kwargs) - if bundlecaps is not None and 'HG2Y' in bundlecaps: + if bundlecaps is not None and 'HG20' in bundlecaps: # When requesting a bundle2, getbundle returns a stream to make the # wire level function happier. We need to build a proper object # from it in local peer. - cg = bundle2.unbundle20(self.ui, cg) + cg = bundle2.getunbundler(self.ui, cg) return cg # TODO We might want to move the next two calls into legacypeer and add @@ -125,15 +125,33 @@ This function handles the repo locking itself.""" try: - cg = exchange.readbundle(self.ui, cg, None) - ret = exchange.unbundle(self._repo, cg, heads, 'push', url) - if util.safehasattr(ret, 'getchunks'): - # This is a bundle20 object, turn it into an unbundler. - # This little dance should be dropped eventually when the API - # is finally improved. - stream = util.chunkbuffer(ret.getchunks()) - ret = bundle2.unbundle20(self.ui, stream) - return ret + try: + cg = exchange.readbundle(self.ui, cg, None) + ret = exchange.unbundle(self._repo, cg, heads, 'push', url) + if util.safehasattr(ret, 'getchunks'): + # This is a bundle20 object, turn it into an unbundler. + # This little dance should be dropped eventually when the + # API is finally improved. + stream = util.chunkbuffer(ret.getchunks()) + ret = bundle2.getunbundler(self.ui, stream) + return ret + except Exception, exc: + # If the exception contains output salvaged from a bundle2 + # reply, we need to make sure it is printed before continuing + # to fail. So we build a bundle2 with such output and consume + # it directly. + # + # This is not very elegant but allows a "simple" solution for + # issue4594 + output = getattr(exc, '_bundle2salvagedoutput', ()) + if output: + bundler = bundle2.bundle20(self._repo.ui) + for out in output: + bundler.addpart(out) + stream = util.chunkbuffer(bundler.getchunks()) + b = bundle2.getunbundler(self.ui, stream) + bundle2.processbundle(self._repo, b) + raise except error.PushRaced, exc: raise error.ResponseError(_('push failed:'), str(exc)) @@ -174,10 +192,10 @@ class localrepository(object): - supportedformats = set(('revlogv1', 'generaldelta')) + supportedformats = set(('revlogv1', 'generaldelta', 'manifestv2')) _basesupported = supportedformats | set(('store', 'fncache', 'shared', 'dotencode')) - openerreqs = set(('revlogv1', 'generaldelta')) + openerreqs = set(('revlogv1', 'generaldelta', 'manifestv2')) requirements = ['revlogv1'] filtername = None @@ -241,6 +259,8 @@ ) if self.ui.configbool('format', 'generaldelta', False): requirements.append("generaldelta") + if self.ui.configbool('experimental', 'manifestv2', False): + requirements.append("manifestv2") requirements = set(requirements) else: raise error.RepoError(_("repository %s not found") % path) @@ -279,6 +299,7 @@ self._branchcaches = {} + self._revbranchcache = None self.filterpats = {} self._datafilters = {} self._transref = self._lockref = self._wlockref = None @@ -302,15 +323,17 @@ self.names = namespaces.namespaces() def close(self): - pass + self._writecaches() + + def _writecaches(self): + if self._revbranchcache: + self._revbranchcache.write() def _restrictcapabilities(self, caps): - # bundle2 is not ready for prime time, drop it unless explicitly - # required by the tests (or some brave tester) - if self.ui.configbool('experimental', 'bundle2-exp', False): + if self.ui.configbool('experimental', 'bundle2-advertise', True): caps = set(caps) capsblob = bundle2.encodecaps(bundle2.getrepocaps(self)) - caps.add('bundle2-exp=' + urllib.quote(capsblob)) + caps.add('bundle2=' + urllib.quote(capsblob)) return caps def _applyrequirements(self, requirements): @@ -323,6 +346,12 @@ maxchainlen = self.ui.configint('format', 'maxchainlen') if maxchainlen is not None: self.svfs.options['maxchainlen'] = maxchainlen + manifestcachesize = self.ui.configint('format', 'manifestcachesize') + if manifestcachesize is not None: + self.svfs.options['manifestcachesize'] = manifestcachesize + usetreemanifest = self.ui.configbool('experimental', 'treemanifest') + if usetreemanifest is not None: + self.svfs.options['usetreemanifest'] = usetreemanifest def _writerequirements(self): reqfile = self.vfs("requires", "w") @@ -417,9 +446,9 @@ store = obsolete.obsstore(self.svfs, readonly=readonly, **kwargs) if store and readonly: - # message is rare enough to not be translated - msg = 'obsolete feature not enabled but %i markers found!\n' - self.ui.warn(msg % len(list(store))) + self.ui.warn( + _('obsolete feature not enabled but %i markers found!\n') + % len(list(store))) return store @storecache('00changelog.i') @@ -462,7 +491,8 @@ def __contains__(self, changeid): try: - return bool(self.lookup(changeid)) + self[changeid] + return True except error.RepoLookupError: return False @@ -479,7 +509,7 @@ '''Return a list of revisions matching the given revset''' expr = revset.formatspec(expr, *args) m = revset.match(None, expr) - return m(self, revset.spanset(self)) + return m(self) def set(self, expr, *args): ''' @@ -520,7 +550,11 @@ if prevtags and prevtags[-1] != '\n': fp.write('\n') for name in names: - m = munge and munge(name) or name + if munge: + m = munge(name) + else: + m = name + if (self._tagscache.tagtypes and name in self._tagscache.tagtypes): old = self.tags().get(name, nullid) @@ -718,6 +752,12 @@ branchmap.updatecache(self) return self._branchcaches[self.filtername] + @unfilteredmethod + def revbranchcache(self): + if not self._revbranchcache: + self._revbranchcache = branchmap.revbranchcache(self.unfiltered()) + return self._revbranchcache + def branchtip(self, branch, ignoremissing=False): '''return the tip node for a given branch @@ -890,12 +930,21 @@ def currenttransaction(self): """return the current transaction or None if non exists""" - tr = self._transref and self._transref() or None + if self._transref: + tr = self._transref() + else: + tr = None + if tr and tr.running(): return tr return None def transaction(self, desc, report=None): + if (self.ui.configbool('devel', 'all') + or self.ui.configbool('devel', 'check-locks')): + l = self._lockref and self._lockref() + if l is None or not l.held: + scmutil.develwarn(self.ui, 'transaction with no lock') tr = self.currenttransaction() if tr is not None: return tr.nest() @@ -906,19 +955,50 @@ _("abandoned transaction found"), hint=_("run 'hg recover' to clean up transaction")) + self.hook('pretxnopen', throw=True, txnname=desc) + self._writejournal(desc) renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()] - rp = report and report or self.ui.warn + if report: + rp = report + else: + rp = self.ui.warn vfsmap = {'plain': self.vfs} # root of .hg/ - tr = transaction.transaction(rp, self.svfs, vfsmap, + # we must avoid cyclic reference between repo and transaction. + reporef = weakref.ref(self) + def validate(tr): + """will run pre-closing hooks""" + pending = lambda: tr.writepending() and self.root or "" + reporef().hook('pretxnclose', throw=True, pending=pending, + xnname=desc, **tr.hookargs) + + tr = transaction.transaction(rp, self.sopener, vfsmap, "journal", "undo", aftertrans(renames), - self.store.createmode) + self.store.createmode, + validator=validate) + + trid = 'TXN:' + util.sha1("%s#%f" % (id(tr), time.time())).hexdigest() + tr.hookargs['TXNID'] = trid # note: writing the fncache only during finalize mean that the file is # outdated when running hooks. As fncache is used for streaming clone, # this is not expected to break anything that happen during the hooks. tr.addfinalize('flush-fncache', self.store.write) + def txnclosehook(tr2): + """To be run if transaction is successful, will schedule a hook run + """ + def hook(): + reporef().hook('txnclose', throw=False, txnname=desc, + **tr2.hookargs) + reporef()._afterlock(hook) + tr.addfinalize('txnclose-hook', txnclosehook) + def txnaborthook(tr2): + """To be run if transaction is aborted + """ + reporef().hook('txnabort', throw=False, txnname=desc, + **tr2.hookargs) + tr.addabort('txnabort-hook', txnaborthook) self._transref = weakref.ref(tr) return tr @@ -1036,6 +1116,9 @@ else: ui.status(_('working directory now based on ' 'revision %d\n') % parents) + ms = mergemod.mergestate(self) + ms.reset(self['.'].node()) + # TODO: if we know which new heads may result from this rollback, pass # them to destroy(), which will prevent the branchhead cache from being # invalidated. @@ -1123,7 +1206,10 @@ def lock(self, wait=True): '''Lock the repository store (.hg/store) and return a weak reference to the lock. Use this before modifying the store (e.g. committing or - stripping). If you are opening a transaction, get a lock as well.)''' + stripping). If you are opening a transaction, get a lock as well.) + + If both 'lock' and 'wlock' must be acquired, ensure you always acquires + 'wlock' first to avoid a dead-lock hazard.''' l = self._lockref and self._lockref() if l is not None and l.held: l.lock() @@ -1143,12 +1229,24 @@ def wlock(self, wait=True): '''Lock the non-store parts of the repository (everything under .hg except .hg/store) and return a weak reference to the lock. - Use this before modifying files in .hg.''' + + Use this before modifying files in .hg. + + If both 'lock' and 'wlock' must be acquired, ensure you always acquires + 'wlock' first to avoid a dead-lock hazard.''' l = self._wlockref and self._wlockref() if l is not None and l.held: l.lock() return l + # We do not need to check for non-waiting lock aquisition. Such + # acquisition would not cause dead-lock as they would just fail. + if wait and (self.ui.configbool('devel', 'all') + or self.ui.configbool('devel', 'check-locks')): + l = self._lockref and self._lockref() + if l is not None and l.held: + scmutil.develwarn(self.ui, '"wlock" acquired after "lock"') + def unlock(): if self.dirstate.pendingparentchange(): self.dirstate.invalidate() @@ -1169,11 +1267,15 @@ """ fname = fctx.path() - text = fctx.data() - flog = self.file(fname) fparent1 = manifest1.get(fname, nullid) fparent2 = manifest2.get(fname, nullid) + if isinstance(fctx, context.filectx): + node = fctx.filenode() + if node in [fparent1, fparent2]: + self.ui.debug('reusing %s filelog entry\n' % fname) + return node + flog = self.file(fname) meta = {} copy = fctx.renamed() if copy and copy[0] != fname: @@ -1208,7 +1310,7 @@ # Here, we used to search backwards through history to try to find # where the file copy came from if the source of a copy was not in - # the parent diretory. However, this doesn't actually make sense to + # the parent directory. However, this doesn't actually make sense to # do (what does a copy from something not in your working copy even # mean?) and it causes bugs (eg, issue4476). Instead, we will warn # the user that copy information was dropped, so if they didn't @@ -1235,6 +1337,7 @@ fparent2 = nullid # is the file changed? + text = fctx.data() if fparent2 != nullid or flog.cmp(fparent1, text) or meta: changelist.append(fname) return flog.add(text, meta, tr, linkrev, fparent1, fparent2) @@ -1270,8 +1373,7 @@ wctx = self[None] merge = len(wctx.parents()) > 1 - if (not force and merge and match and - (match.files() or match.anypats())): + if not force and merge and not match.always(): raise util.Abort(_('cannot partially commit a merge ' '(do not specify files or patterns)')) @@ -1302,10 +1404,10 @@ if not force: raise util.Abort( _("commit with new subrepo %s excluded") % s) - if wctx.sub(s).dirty(True): + dirtyreason = wctx.sub(s).dirtyreason(True) + if dirtyreason: if not self.ui.configbool('ui', 'commitsubrepos'): - raise util.Abort( - _("uncommitted changes in subrepo %s") % s, + raise util.Abort(dirtyreason, hint=_("use --subrepos for recursive commit")) subs.append(s) commitsubs.add(s) diff -r 612ed41ae359 -r e89f909edffa mercurial/manifest.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/manifest.c Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,921 @@ +/* + * manifest.c - manifest type that does on-demand parsing. + * + * Copyright 2015, Google Inc. + * + * This software may be used and distributed according to the terms of + * the GNU General Public License, incorporated herein by reference. + */ +#include + +#include +#include +#include + +#include "util.h" + +#define DEFAULT_LINES 100000 + +typedef struct { + char *start; + Py_ssize_t len; /* length of line including terminal newline */ + char hash_suffix; + bool from_malloc; + bool deleted; +} line; + +typedef struct { + PyObject_HEAD + PyObject *pydata; + line *lines; + int numlines; /* number of line entries */ + int livelines; /* number of non-deleted lines */ + int maxlines; /* allocated number of lines */ + bool dirty; +} lazymanifest; + +#define MANIFEST_OOM -1 +#define MANIFEST_NOT_SORTED -2 +#define MANIFEST_MALFORMED -3 + +/* defined in parsers.c */ +PyObject *unhexlify(const char *str, int len); + +/* get the length of the path for a line */ +static size_t pathlen(line *l) { + return strlen(l->start); +} + +/* get the node value of a single line */ +static PyObject *nodeof(line *l) { + char *s = l->start; + ssize_t llen = pathlen(l); + PyObject *hash = unhexlify(s + llen + 1, 40); + if (!hash) { + return NULL; + } + if (l->hash_suffix != '\0') { + char newhash[21]; + memcpy(newhash, PyString_AsString(hash), 20); + Py_DECREF(hash); + newhash[20] = l->hash_suffix; + hash = PyString_FromStringAndSize(newhash, 21); + } + return hash; +} + +/* get the node hash and flags of a line as a tuple */ +static PyObject *hashflags(line *l) +{ + char *s = l->start; + size_t plen = pathlen(l); + PyObject *hash = nodeof(l); + + /* 40 for hash, 1 for null byte, 1 for newline */ + size_t hplen = plen + 42; + Py_ssize_t flen = l->len - hplen; + PyObject *flags; + PyObject *tup; + + if (!hash) + return NULL; + flags = PyString_FromStringAndSize(s + hplen - 1, flen); + if (!flags) { + Py_DECREF(hash); + return NULL; + } + tup = PyTuple_Pack(2, hash, flags); + Py_DECREF(flags); + Py_DECREF(hash); + return tup; +} + +/* if we're about to run out of space in the line index, add more */ +static bool realloc_if_full(lazymanifest *self) +{ + if (self->numlines == self->maxlines) { + self->maxlines *= 2; + self->lines = realloc(self->lines, self->maxlines * sizeof(line)); + } + return !!self->lines; +} + +/* + * Find the line boundaries in the manifest that 'data' points to and store + * information about each line in 'self'. + */ +static int find_lines(lazymanifest *self, char *data, Py_ssize_t len) +{ + char *prev = NULL; + while (len > 0) { + line *l; + char *next = memchr(data, '\n', len); + if (!next) { + return MANIFEST_MALFORMED; + } + next++; /* advance past newline */ + if (!realloc_if_full(self)) { + return MANIFEST_OOM; /* no memory */ + } + if (prev && strcmp(prev, data) > -1) { + /* This data isn't sorted, so we have to abort. */ + return MANIFEST_NOT_SORTED; + } + l = self->lines + ((self->numlines)++); + l->start = data; + l->len = next - data; + l->hash_suffix = '\0'; + l->from_malloc = false; + l->deleted = false; + len = len - l->len; + prev = data; + data = next; + } + self->livelines = self->numlines; + return 0; +} + +static int lazymanifest_init(lazymanifest *self, PyObject *args) +{ + char *data; + Py_ssize_t len; + int err, ret; + PyObject *pydata; + if (!PyArg_ParseTuple(args, "S", &pydata)) { + return -1; + } + err = PyString_AsStringAndSize(pydata, &data, &len); + + self->dirty = false; + if (err == -1) + return -1; + self->pydata = pydata; + Py_INCREF(self->pydata); + Py_BEGIN_ALLOW_THREADS + self->lines = malloc(DEFAULT_LINES * sizeof(line)); + self->maxlines = DEFAULT_LINES; + self->numlines = 0; + if (!self->lines) + ret = MANIFEST_OOM; + else + ret = find_lines(self, data, len); + Py_END_ALLOW_THREADS + switch (ret) { + case 0: + break; + case MANIFEST_OOM: + PyErr_NoMemory(); + break; + case MANIFEST_NOT_SORTED: + PyErr_Format(PyExc_ValueError, + "Manifest lines not in sorted order."); + break; + case MANIFEST_MALFORMED: + PyErr_Format(PyExc_ValueError, + "Manifest did not end in a newline."); + break; + default: + PyErr_Format(PyExc_ValueError, + "Unknown problem parsing manifest."); + } + return ret == 0 ? 0 : -1; +} + +static void lazymanifest_dealloc(lazymanifest *self) +{ + /* free any extra lines we had to allocate */ + int i; + for (i = 0; i < self->numlines; i++) { + if (self->lines[i].from_malloc) { + free(self->lines[i].start); + } + } + if (self->lines) { + free(self->lines); + self->lines = NULL; + } + if (self->pydata) { + Py_DECREF(self->pydata); + self->pydata = NULL; + } + PyObject_Del(self); +} + +/* iteration support */ + +typedef struct { + PyObject_HEAD lazymanifest *m; + Py_ssize_t pos; +} lmIter; + +static void lmiter_dealloc(PyObject *o) +{ + lmIter *self = (lmIter *)o; + Py_DECREF(self->m); + PyObject_Del(self); +} + +static line *lmiter_nextline(lmIter *self) +{ + do { + self->pos++; + if (self->pos >= self->m->numlines) { + return NULL; + } + /* skip over deleted manifest entries */ + } while (self->m->lines[self->pos].deleted); + return self->m->lines + self->pos; +} + +static PyObject *lmiter_iterentriesnext(PyObject *o) +{ + size_t pl; + line *l; + Py_ssize_t consumed; + PyObject *ret = NULL, *path = NULL, *hash = NULL, *flags = NULL; + l = lmiter_nextline((lmIter *)o); + if (!l) { + goto bail; + } + pl = pathlen(l); + path = PyString_FromStringAndSize(l->start, pl); + hash = nodeof(l); + consumed = pl + 41; + flags = PyString_FromStringAndSize(l->start + consumed, + l->len - consumed - 1); + if (!path || !hash || !flags) { + goto bail; + } + ret = PyTuple_Pack(3, path, hash, flags); + bail: + Py_XDECREF(path); + Py_XDECREF(hash); + Py_XDECREF(flags); + return ret; +} + +static PyTypeObject lazymanifestEntriesIterator = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size */ + "parsers.lazymanifest.entriesiterator", /*tp_name */ + sizeof(lmIter), /*tp_basicsize */ + 0, /*tp_itemsize */ + lmiter_dealloc, /*tp_dealloc */ + 0, /*tp_print */ + 0, /*tp_getattr */ + 0, /*tp_setattr */ + 0, /*tp_compare */ + 0, /*tp_repr */ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash */ + 0, /*tp_call */ + 0, /*tp_str */ + 0, /*tp_getattro */ + 0, /*tp_setattro */ + 0, /*tp_as_buffer */ + /* tp_flags: Py_TPFLAGS_HAVE_ITER tells python to + use tp_iter and tp_iternext fields. */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_ITER, + "Iterator for 3-tuples in a lazymanifest.", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + PyObject_SelfIter, /* tp_iter: __iter__() method */ + lmiter_iterentriesnext, /* tp_iternext: next() method */ +}; + +static PyObject *lmiter_iterkeysnext(PyObject *o) +{ + size_t pl; + line *l = lmiter_nextline((lmIter *)o); + if (!l) { + return NULL; + } + pl = pathlen(l); + return PyString_FromStringAndSize(l->start, pl); +} + +static PyTypeObject lazymanifestKeysIterator = { + PyObject_HEAD_INIT(NULL) + 0, /*ob_size */ + "parsers.lazymanifest.keysiterator", /*tp_name */ + sizeof(lmIter), /*tp_basicsize */ + 0, /*tp_itemsize */ + lmiter_dealloc, /*tp_dealloc */ + 0, /*tp_print */ + 0, /*tp_getattr */ + 0, /*tp_setattr */ + 0, /*tp_compare */ + 0, /*tp_repr */ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash */ + 0, /*tp_call */ + 0, /*tp_str */ + 0, /*tp_getattro */ + 0, /*tp_setattro */ + 0, /*tp_as_buffer */ + /* tp_flags: Py_TPFLAGS_HAVE_ITER tells python to + use tp_iter and tp_iternext fields. */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_ITER, + "Keys iterator for a lazymanifest.", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + PyObject_SelfIter, /* tp_iter: __iter__() method */ + lmiter_iterkeysnext, /* tp_iternext: next() method */ +}; + +static lazymanifest *lazymanifest_copy(lazymanifest *self); + +static PyObject *lazymanifest_getentriesiter(lazymanifest *self) +{ + lmIter *i = NULL; + lazymanifest *t = lazymanifest_copy(self); + if (!t) { + PyErr_NoMemory(); + return NULL; + } + i = PyObject_New(lmIter, &lazymanifestEntriesIterator); + if (i) { + i->m = t; + i->pos = -1; + } else { + Py_DECREF(t); + PyErr_NoMemory(); + } + return (PyObject *)i; +} + +static PyObject *lazymanifest_getkeysiter(lazymanifest *self) +{ + lmIter *i = NULL; + lazymanifest *t = lazymanifest_copy(self); + if (!t) { + PyErr_NoMemory(); + return NULL; + } + i = PyObject_New(lmIter, &lazymanifestKeysIterator); + if (i) { + i->m = t; + i->pos = -1; + } else { + Py_DECREF(t); + PyErr_NoMemory(); + } + return (PyObject *)i; +} + +/* __getitem__ and __setitem__ support */ + +static Py_ssize_t lazymanifest_size(lazymanifest *self) +{ + return self->livelines; +} + +static int linecmp(const void *left, const void *right) +{ + return strcmp(((const line *)left)->start, + ((const line *)right)->start); +} + +static PyObject *lazymanifest_getitem(lazymanifest *self, PyObject *key) +{ + line needle; + line *hit; + if (!PyString_Check(key)) { + PyErr_Format(PyExc_TypeError, + "getitem: manifest keys must be a string."); + return NULL; + } + needle.start = PyString_AsString(key); + hit = bsearch(&needle, self->lines, self->numlines, sizeof(line), + &linecmp); + if (!hit || hit->deleted) { + PyErr_Format(PyExc_KeyError, "No such manifest entry."); + return NULL; + } + return hashflags(hit); +} + +static int lazymanifest_delitem(lazymanifest *self, PyObject *key) +{ + line needle; + line *hit; + if (!PyString_Check(key)) { + PyErr_Format(PyExc_TypeError, + "delitem: manifest keys must be a string."); + return -1; + } + needle.start = PyString_AsString(key); + hit = bsearch(&needle, self->lines, self->numlines, sizeof(line), + &linecmp); + if (!hit || hit->deleted) { + PyErr_Format(PyExc_KeyError, + "Tried to delete nonexistent manifest entry."); + return -1; + } + self->dirty = true; + hit->deleted = true; + self->livelines--; + return 0; +} + +/* Do a binary search for the insertion point for new, creating the + * new entry if needed. */ +static int internalsetitem(lazymanifest *self, line *new) { + int start = 0, end = self->numlines; + while (start < end) { + int pos = start + (end - start) / 2; + int c = linecmp(new, self->lines + pos); + if (c < 0) + end = pos; + else if (c > 0) + start = pos + 1; + else { + if (self->lines[pos].deleted) + self->livelines++; + if (self->lines[pos].from_malloc) + free(self->lines[pos].start); + start = pos; + goto finish; + } + } + /* being here means we need to do an insert */ + if (!realloc_if_full(self)) { + PyErr_NoMemory(); + return -1; + } + memmove(self->lines + start + 1, self->lines + start, + (self->numlines - start) * sizeof(line)); + self->numlines++; + self->livelines++; +finish: + self->lines[start] = *new; + self->dirty = true; + return 0; +} + +static int lazymanifest_setitem( + lazymanifest *self, PyObject *key, PyObject *value) +{ + char *path; + Py_ssize_t plen; + PyObject *pyhash; + Py_ssize_t hlen; + char *hash; + PyObject *pyflags; + char *flags; + Py_ssize_t flen; + size_t dlen; + char *dest; + int i; + line new; + if (!PyString_Check(key)) { + PyErr_Format(PyExc_TypeError, + "setitem: manifest keys must be a string."); + return -1; + } + if (!value) { + return lazymanifest_delitem(self, key); + } + if (!PyTuple_Check(value) || PyTuple_Size(value) != 2) { + PyErr_Format(PyExc_TypeError, + "Manifest values must be a tuple of (node, flags)."); + return -1; + } + if (PyString_AsStringAndSize(key, &path, &plen) == -1) { + return -1; + } + + pyhash = PyTuple_GetItem(value, 0); + if (!PyString_Check(pyhash)) { + PyErr_Format(PyExc_TypeError, + "node must be a 20-byte string"); + return -1; + } + hlen = PyString_Size(pyhash); + /* Some parts of the codebase try and set 21 or 22 + * byte "hash" values in order to perturb things for + * status. We have to preserve at least the 21st + * byte. Sigh. If there's a 22nd byte, we drop it on + * the floor, which works fine. + */ + if (hlen != 20 && hlen != 21 && hlen != 22) { + PyErr_Format(PyExc_TypeError, + "node must be a 20-byte string"); + return -1; + } + hash = PyString_AsString(pyhash); + + pyflags = PyTuple_GetItem(value, 1); + if (!PyString_Check(pyflags) || PyString_Size(pyflags) > 1) { + PyErr_Format(PyExc_TypeError, + "flags must a 0 or 1 byte string"); + return -1; + } + if (PyString_AsStringAndSize(pyflags, &flags, &flen) == -1) { + return -1; + } + /* one null byte and one newline */ + dlen = plen + 41 + flen + 1; + dest = malloc(dlen); + if (!dest) { + PyErr_NoMemory(); + return -1; + } + memcpy(dest, path, plen + 1); + for (i = 0; i < 20; i++) { + /* Cast to unsigned, so it will not get sign-extended when promoted + * to int (as is done when passing to a variadic function) + */ + sprintf(dest + plen + 1 + (i * 2), "%02x", (unsigned char)hash[i]); + } + memcpy(dest + plen + 41, flags, flen); + dest[plen + 41 + flen] = '\n'; + new.start = dest; + new.len = dlen; + new.hash_suffix = '\0'; + if (hlen > 20) { + new.hash_suffix = hash[20]; + } + new.from_malloc = true; /* is `start` a pointer we allocated? */ + new.deleted = false; /* is this entry deleted? */ + if (internalsetitem(self, &new)) { + return -1; + } + return 0; +} + +static PyMappingMethods lazymanifest_mapping_methods = { + (lenfunc)lazymanifest_size, /* mp_length */ + (binaryfunc)lazymanifest_getitem, /* mp_subscript */ + (objobjargproc)lazymanifest_setitem, /* mp_ass_subscript */ +}; + +/* sequence methods (important or __contains__ builds an iterator */ + +static int lazymanifest_contains(lazymanifest *self, PyObject *key) +{ + line needle; + line *hit; + if (!PyString_Check(key)) { + /* Our keys are always strings, so if the contains + * check is for a non-string, just return false. */ + return 0; + } + needle.start = PyString_AsString(key); + hit = bsearch(&needle, self->lines, self->numlines, sizeof(line), + &linecmp); + if (!hit || hit->deleted) { + return 0; + } + return 1; +} + +static PySequenceMethods lazymanifest_seq_meths = { + (lenfunc)lazymanifest_size, /* sq_length */ + 0, /* sq_concat */ + 0, /* sq_repeat */ + 0, /* sq_item */ + 0, /* sq_slice */ + 0, /* sq_ass_item */ + 0, /* sq_ass_slice */ + (objobjproc)lazymanifest_contains, /* sq_contains */ + 0, /* sq_inplace_concat */ + 0, /* sq_inplace_repeat */ +}; + + +/* Other methods (copy, diff, etc) */ +static PyTypeObject lazymanifestType; + +/* If the manifest has changes, build the new manifest text and reindex it. */ +static int compact(lazymanifest *self) { + int i; + ssize_t need = 0; + char *data; + line *src, *dst; + PyObject *pydata; + if (!self->dirty) + return 0; + for (i = 0; i < self->numlines; i++) { + if (!self->lines[i].deleted) { + need += self->lines[i].len; + } + } + pydata = PyString_FromStringAndSize(NULL, need); + if (!pydata) + return -1; + data = PyString_AsString(pydata); + if (!data) { + return -1; + } + src = self->lines; + dst = self->lines; + for (i = 0; i < self->numlines; i++, src++) { + char *tofree = NULL; + if (src->from_malloc) { + tofree = src->start; + } + if (!src->deleted) { + memcpy(data, src->start, src->len); + *dst = *src; + dst->start = data; + dst->from_malloc = false; + data += dst->len; + dst++; + } + free(tofree); + } + Py_DECREF(self->pydata); + self->pydata = pydata; + self->numlines = self->livelines; + self->dirty = false; + return 0; +} + +static PyObject *lazymanifest_text(lazymanifest *self) +{ + if (compact(self) != 0) { + PyErr_NoMemory(); + return NULL; + } + Py_INCREF(self->pydata); + return self->pydata; +} + +static lazymanifest *lazymanifest_copy(lazymanifest *self) +{ + lazymanifest *copy = NULL; + if (compact(self) != 0) { + goto nomem; + } + copy = PyObject_New(lazymanifest, &lazymanifestType); + if (!copy) { + goto nomem; + } + copy->numlines = self->numlines; + copy->livelines = self->livelines; + copy->dirty = false; + copy->lines = malloc(self->maxlines *sizeof(line)); + if (!copy->lines) { + goto nomem; + } + memcpy(copy->lines, self->lines, self->numlines * sizeof(line)); + copy->maxlines = self->maxlines; + copy->pydata = self->pydata; + Py_INCREF(copy->pydata); + return copy; + nomem: + PyErr_NoMemory(); + Py_XDECREF(copy); + return NULL; +} + +static lazymanifest *lazymanifest_filtercopy( + lazymanifest *self, PyObject *matchfn) +{ + lazymanifest *copy = NULL; + int i; + if (!PyCallable_Check(matchfn)) { + PyErr_SetString(PyExc_TypeError, "matchfn must be callable"); + return NULL; + } + /* compact ourselves first to avoid double-frees later when we + * compact tmp so that it doesn't have random pointers to our + * underlying from_malloc-data (self->pydata is safe) */ + if (compact(self) != 0) { + goto nomem; + } + copy = PyObject_New(lazymanifest, &lazymanifestType); + copy->dirty = true; + copy->lines = malloc(self->maxlines * sizeof(line)); + if (!copy->lines) { + goto nomem; + } + copy->maxlines = self->maxlines; + copy->numlines = 0; + copy->pydata = self->pydata; + Py_INCREF(self->pydata); + for (i = 0; i < self->numlines; i++) { + PyObject *arg = PyString_FromString(self->lines[i].start); + PyObject *arglist = PyTuple_Pack(1, arg); + PyObject *result = PyObject_CallObject(matchfn, arglist); + Py_DECREF(arglist); + Py_DECREF(arg); + /* if the callback raised an exception, just let it + * through and give up */ + if (!result) { + free(copy->lines); + Py_DECREF(self->pydata); + return NULL; + } + if (PyObject_IsTrue(result)) { + assert(!(self->lines[i].from_malloc)); + copy->lines[copy->numlines++] = self->lines[i]; + } + Py_DECREF(result); + } + copy->livelines = copy->numlines; + return copy; + nomem: + PyErr_NoMemory(); + Py_XDECREF(copy); + return NULL; +} + +static PyObject *lazymanifest_diff(lazymanifest *self, PyObject *args) +{ + lazymanifest *other; + PyObject *pyclean = NULL; + bool listclean; + PyObject *emptyTup = NULL, *ret = NULL; + PyObject *es; + int sneedle = 0, oneedle = 0; + if (!PyArg_ParseTuple(args, "O!|O", &lazymanifestType, &other, &pyclean)) { + return NULL; + } + listclean = (!pyclean) ? false : PyObject_IsTrue(pyclean); + es = PyString_FromString(""); + if (!es) { + goto nomem; + } + emptyTup = PyTuple_Pack(2, Py_None, es); + Py_DECREF(es); + if (!emptyTup) { + goto nomem; + } + ret = PyDict_New(); + if (!ret) { + goto nomem; + } + while (sneedle != self->numlines || oneedle != other->numlines) { + line *left = self->lines + sneedle; + line *right = other->lines + oneedle; + int result; + PyObject *key; + PyObject *outer; + /* If we're looking at a deleted entry and it's not + * the end of the manifest, just skip it. */ + if (left->deleted && sneedle < self->numlines) { + sneedle++; + continue; + } + if (right->deleted && oneedle < other->numlines) { + oneedle++; + continue; + } + /* if we're at the end of either manifest, then we + * know the remaining items are adds so we can skip + * the strcmp. */ + if (sneedle == self->numlines) { + result = 1; + } else if (oneedle == other->numlines) { + result = -1; + } else { + result = linecmp(left, right); + } + key = result <= 0 ? + PyString_FromString(left->start) : + PyString_FromString(right->start); + if (!key) + goto nomem; + if (result < 0) { + PyObject *l = hashflags(left); + if (!l) { + goto nomem; + } + outer = PyTuple_Pack(2, l, emptyTup); + Py_DECREF(l); + if (!outer) { + goto nomem; + } + PyDict_SetItem(ret, key, outer); + Py_DECREF(outer); + sneedle++; + } else if (result > 0) { + PyObject *r = hashflags(right); + if (!r) { + goto nomem; + } + outer = PyTuple_Pack(2, emptyTup, r); + Py_DECREF(r); + if (!outer) { + goto nomem; + } + PyDict_SetItem(ret, key, outer); + Py_DECREF(outer); + oneedle++; + } else { + /* file exists in both manifests */ + if (left->len != right->len + || memcmp(left->start, right->start, left->len) + || left->hash_suffix != right->hash_suffix) { + PyObject *l = hashflags(left); + PyObject *r; + if (!l) { + goto nomem; + } + r = hashflags(right); + if (!r) { + Py_DECREF(l); + goto nomem; + } + outer = PyTuple_Pack(2, l, r); + Py_DECREF(l); + Py_DECREF(r); + if (!outer) { + goto nomem; + } + PyDict_SetItem(ret, key, outer); + Py_DECREF(outer); + } else if (listclean) { + PyDict_SetItem(ret, key, Py_None); + } + sneedle++; + oneedle++; + } + Py_DECREF(key); + } + Py_DECREF(emptyTup); + return ret; + nomem: + PyErr_NoMemory(); + Py_XDECREF(ret); + Py_XDECREF(emptyTup); + return NULL; +} + +static PyMethodDef lazymanifest_methods[] = { + {"iterkeys", (PyCFunction)lazymanifest_getkeysiter, METH_NOARGS, + "Iterate over file names in this lazymanifest."}, + {"iterentries", (PyCFunction)lazymanifest_getentriesiter, METH_NOARGS, + "Iterate over (path, nodeid, flags) typles in this lazymanifest."}, + {"copy", (PyCFunction)lazymanifest_copy, METH_NOARGS, + "Make a copy of this lazymanifest."}, + {"filtercopy", (PyCFunction)lazymanifest_filtercopy, METH_O, + "Make a copy of this manifest filtered by matchfn."}, + {"diff", (PyCFunction)lazymanifest_diff, METH_VARARGS, + "Compare this lazymanifest to another one."}, + {"text", (PyCFunction)lazymanifest_text, METH_NOARGS, + "Encode this manifest to text."}, + {NULL}, +}; + +static PyTypeObject lazymanifestType = { + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ + "parsers.lazymanifest", /* tp_name */ + sizeof(lazymanifest), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)lazymanifest_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + &lazymanifest_seq_meths, /* tp_as_sequence */ + &lazymanifest_mapping_methods, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_SEQUENCE_IN, /* tp_flags */ + "TODO(augie)", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + (getiterfunc)lazymanifest_getkeysiter, /* tp_iter */ + 0, /* tp_iternext */ + lazymanifest_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)lazymanifest_init, /* tp_init */ + 0, /* tp_alloc */ +}; + +void manifest_module_init(PyObject * mod) +{ + lazymanifestType.tp_new = PyType_GenericNew; + if (PyType_Ready(&lazymanifestType) < 0) + return; + Py_INCREF(&lazymanifestType); + + PyModule_AddObject(mod, "lazymanifest", + (PyObject *)&lazymanifestType); +} diff -r 612ed41ae359 -r e89f909edffa mercurial/manifest.py --- a/mercurial/manifest.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/manifest.py Thu Apr 16 20:57:51 2015 -0500 @@ -8,53 +8,271 @@ from i18n import _ import mdiff, parsers, error, revlog, util import array, struct +import os -class manifestdict(dict): - def __init__(self, mapping=None, flags=None): - if mapping is None: - mapping = {} - if flags is None: - flags = {} - dict.__init__(self, mapping) - self._flags = flags +propertycache = util.propertycache + +def _parsev1(data): + # This method does a little bit of excessive-looking + # precondition checking. This is so that the behavior of this + # class exactly matches its C counterpart to try and help + # prevent surprise breakage for anyone that develops against + # the pure version. + if data and data[-1] != '\n': + raise ValueError('Manifest did not end in a newline.') + prev = None + for l in data.splitlines(): + if prev is not None and prev > l: + raise ValueError('Manifest lines not in sorted order.') + prev = l + f, n = l.split('\0') + if len(n) > 40: + yield f, revlog.bin(n[:40]), n[40:] + else: + yield f, revlog.bin(n), '' + +def _parsev2(data): + metadataend = data.find('\n') + # Just ignore metadata for now + pos = metadataend + 1 + prevf = '' + while pos < len(data): + end = data.find('\n', pos + 1) # +1 to skip stem length byte + if end == -1: + raise ValueError('Manifest ended with incomplete file entry.') + stemlen = ord(data[pos]) + items = data[pos + 1:end].split('\0') + f = prevf[:stemlen] + items[0] + if prevf > f: + raise ValueError('Manifest entries not in sorted order.') + fl = items[1] + # Just ignore metadata (items[2:] for now) + n = data[end + 1:end + 21] + yield f, n, fl + pos = end + 22 + prevf = f + +def _parse(data): + """Generates (path, node, flags) tuples from a manifest text""" + if data.startswith('\0'): + return iter(_parsev2(data)) + else: + return iter(_parsev1(data)) + +def _text(it, usemanifestv2): + """Given an iterator over (path, node, flags) tuples, returns a manifest + text""" + if usemanifestv2: + return _textv2(it) + else: + return _textv1(it) + +def _textv1(it): + files = [] + lines = [] + _hex = revlog.hex + for f, n, fl in it: + files.append(f) + # if this is changed to support newlines in filenames, + # be sure to check the templates/ dir again (especially *-raw.tmpl) + lines.append("%s\0%s%s\n" % (f, _hex(n), fl)) + + _checkforbidden(files) + return ''.join(lines) + +def _textv2(it): + files = [] + lines = ['\0\n'] + prevf = '' + for f, n, fl in it: + files.append(f) + stem = os.path.commonprefix([prevf, f]) + stemlen = min(len(stem), 255) + lines.append("%c%s\0%s\n%s\n" % (stemlen, f[stemlen:], fl, n)) + prevf = f + _checkforbidden(files) + return ''.join(lines) + +class _lazymanifest(dict): + """This is the pure implementation of lazymanifest. + + It has not been optimized *at all* and is not lazy. + """ + + def __init__(self, data): + dict.__init__(self) + for f, n, fl in _parse(data): + self[f] = n, fl + def __setitem__(self, k, v): - assert v is not None - dict.__setitem__(self, k, v) - def flags(self, f): - return self._flags.get(f, "") - def setflag(self, f, flags): - """Set the flags (symlink, executable) for path f.""" - self._flags[f] = flags + node, flag = v + assert node is not None + if len(node) > 21: + node = node[:21] # match c implementation behavior + dict.__setitem__(self, k, (node, flag)) + + def __iter__(self): + return iter(sorted(dict.keys(self))) + + def iterkeys(self): + return iter(sorted(dict.keys(self))) + + def iterentries(self): + return ((f, e[0], e[1]) for f, e in sorted(self.iteritems())) + def copy(self): - return manifestdict(self, dict.copy(self._flags)) - def intersectfiles(self, files): - '''make a new manifestdict with the intersection of self with files + c = _lazymanifest('') + c.update(self) + return c + + def diff(self, m2, clean=False): + '''Finds changes between the current manifest and m2.''' + diff = {} + + for fn, e1 in self.iteritems(): + if fn not in m2: + diff[fn] = e1, (None, '') + else: + e2 = m2[fn] + if e1 != e2: + diff[fn] = e1, e2 + elif clean: + diff[fn] = None + + for fn, e2 in m2.iteritems(): + if fn not in self: + diff[fn] = (None, ''), e2 + + return diff + + def filtercopy(self, filterfn): + c = _lazymanifest('') + for f, n, fl in self.iterentries(): + if filterfn(f): + c[f] = n, fl + return c + + def text(self): + """Get the full data of this manifest as a bytestring.""" + return _textv1(self.iterentries()) + +try: + _lazymanifest = parsers.lazymanifest +except AttributeError: + pass + +class manifestdict(object): + def __init__(self, data=''): + if data.startswith('\0'): + #_lazymanifest can not parse v2 + self._lm = _lazymanifest('') + for f, n, fl in _parsev2(data): + self._lm[f] = n, fl + else: + self._lm = _lazymanifest(data) + + def __getitem__(self, key): + return self._lm[key][0] + + def find(self, key): + return self._lm[key] + + def __len__(self): + return len(self._lm) + + def __setitem__(self, key, node): + self._lm[key] = node, self.flags(key, '') + + def __contains__(self, key): + return key in self._lm + + def __delitem__(self, key): + del self._lm[key] - The algorithm assumes that files is much smaller than self.''' - ret = manifestdict() - for fn in files: - if fn in self: - ret[fn] = self[fn] - flags = self._flags.get(fn, None) - if flags: - ret._flags[fn] = flags - return ret + def __iter__(self): + return self._lm.__iter__() + + def iterkeys(self): + return self._lm.iterkeys() + + def keys(self): + return list(self.iterkeys()) + + def filesnotin(self, m2): + '''Set of files in this manifest that are not in the other''' + files = set(self) + files.difference_update(m2) + return files + + @propertycache + def _dirs(self): + return util.dirs(self) + + def dirs(self): + return self._dirs + + def hasdir(self, dir): + return dir in self._dirs + + def _filesfastpath(self, match): + '''Checks whether we can correctly and quickly iterate over matcher + files instead of over manifest files.''' + files = match.files() + return (len(files) < 100 and (match.isexact() or + (not match.anypats() and util.all(fn in self for fn in files)))) + + def walk(self, match): + '''Generates matching file names. + + Equivalent to manifest.matches(match).iterkeys(), but without creating + an entirely new manifest. + + It also reports nonexistent files by marking them bad with match.bad(). + ''' + if match.always(): + for f in iter(self): + yield f + return + + fset = set(match.files()) + + # avoid the entire walk if we're only looking for specific files + if self._filesfastpath(match): + for fn in sorted(fset): + yield fn + return + + for fn in self: + if fn in fset: + # specified pattern is the exact name + fset.remove(fn) + if match(fn): + yield fn + + # for dirstate.walk, files=['.'] means "walk the whole tree". + # follow that here, too + fset.discard('.') + + for fn in sorted(fset): + if not self.hasdir(fn): + match.bad(fn, None) def matches(self, match): '''generate a new manifest filtered by the match argument''' if match.always(): return self.copy() - files = match.files() - if (match.matchfn == match.exact or - (not match.anypats() and util.all(fn in self for fn in files))): - return self.intersectfiles(files) + if self._filesfastpath(match): + m = manifestdict() + lm = self._lm + for fn in match.files(): + if fn in lm: + m._lm[fn] = lm[fn] + return m - mf = self.copy() - for fn in mf.keys(): - if not match(fn): - del mf[fn] - return mf + m = manifestdict() + m._lm = self._lm.filtercopy(match) + return m def diff(self, m2, clean=False): '''Finds changes between the current manifest and m2. @@ -71,35 +289,37 @@ the nodeid will be None and the flags will be the empty string. ''' - diff = {} + return self._lm.diff(m2._lm, clean) + + def setflag(self, key, flag): + self._lm[key] = self[key], flag - for fn, n1 in self.iteritems(): - fl1 = self._flags.get(fn, '') - n2 = m2.get(fn, None) - fl2 = m2._flags.get(fn, '') - if n2 is None: - fl2 = '' - if n1 != n2 or fl1 != fl2: - diff[fn] = ((n1, fl1), (n2, fl2)) - elif clean: - diff[fn] = None + def get(self, key, default=None): + try: + return self._lm[key][0] + except KeyError: + return default - for fn, n2 in m2.iteritems(): - if fn not in self: - fl2 = m2._flags.get(fn, '') - diff[fn] = ((None, ''), (n2, fl2)) - - return diff + def flags(self, key, default=''): + try: + return self._lm[key][1] + except KeyError: + return default - def text(self): - """Get the full data of this manifest as a bytestring.""" - fl = sorted(self) - _checkforbidden(fl) + def copy(self): + c = manifestdict() + c._lm = self._lm.copy() + return c - hex, flags = revlog.hex, self.flags - # if this is changed to support newlines in filenames, - # be sure to check the templates/ dir again (especially *-raw.tmpl) - return ''.join("%s\0%s%s\n" % (f, hex(self[f]), flags(f)) for f in fl) + def iteritems(self): + return (x[:2] for x in self._lm.iterentries()) + + def text(self, usemanifestv2=False): + if usemanifestv2: + return _textv2(self._lm.iterentries()) + else: + # use (probably) native version for v1 + return self._lm.text() def fastdelta(self, base, changes): """Given a base manifest text as an array.array and a list of changes @@ -119,7 +339,8 @@ # bs will either be the index of the item or the insert point start, end = _msearch(addbuf, f, start) if not todelete: - l = "%s\0%s%s\n" % (f, revlog.hex(self[f]), self.flags(f)) + h, fl = self._lm[f] + l = "%s\0%s%s\n" % (f, revlog.hex(h), fl) else: if start == end: # item we want to delete was not found, error out @@ -213,21 +434,363 @@ + content for start, end, content in x) return deltatext, newaddlist -def _parse(lines): - mfdict = manifestdict() - parsers.parse_manifest(mfdict, mfdict._flags, lines) - return mfdict +def _splittopdir(f): + if '/' in f: + dir, subpath = f.split('/', 1) + return dir + '/', subpath + else: + return '', f + +class treemanifest(object): + def __init__(self, dir='', text=''): + self._dir = dir + self._dirs = {} + # Using _lazymanifest here is a little slower than plain old dicts + self._files = {} + self._flags = {} + self.parse(text) + + def _subpath(self, path): + return self._dir + path + + def __len__(self): + size = len(self._files) + for m in self._dirs.values(): + size += m.__len__() + return size + + def _isempty(self): + return (not self._files and (not self._dirs or + util.all(m._isempty() for m in self._dirs.values()))) + + def __str__(self): + return '' % self._dir + + def iteritems(self): + for p, n in sorted(self._dirs.items() + self._files.items()): + if p in self._files: + yield self._subpath(p), n + else: + for f, sn in n.iteritems(): + yield f, sn + + def iterkeys(self): + for p in sorted(self._dirs.keys() + self._files.keys()): + if p in self._files: + yield self._subpath(p) + else: + for f in self._dirs[p].iterkeys(): + yield f + + def keys(self): + return list(self.iterkeys()) + + def __iter__(self): + return self.iterkeys() + + def __contains__(self, f): + if f is None: + return False + dir, subpath = _splittopdir(f) + if dir: + if dir not in self._dirs: + return False + return self._dirs[dir].__contains__(subpath) + else: + return f in self._files + + def get(self, f, default=None): + dir, subpath = _splittopdir(f) + if dir: + if dir not in self._dirs: + return default + return self._dirs[dir].get(subpath, default) + else: + return self._files.get(f, default) + + def __getitem__(self, f): + dir, subpath = _splittopdir(f) + if dir: + return self._dirs[dir].__getitem__(subpath) + else: + return self._files[f] + + def flags(self, f): + dir, subpath = _splittopdir(f) + if dir: + if dir not in self._dirs: + return '' + return self._dirs[dir].flags(subpath) + else: + if f in self._dirs: + return '' + return self._flags.get(f, '') + + def find(self, f): + dir, subpath = _splittopdir(f) + if dir: + return self._dirs[dir].find(subpath) + else: + return self._files[f], self._flags.get(f, '') + + def __delitem__(self, f): + dir, subpath = _splittopdir(f) + if dir: + self._dirs[dir].__delitem__(subpath) + # If the directory is now empty, remove it + if self._dirs[dir]._isempty(): + del self._dirs[dir] + else: + del self._files[f] + if f in self._flags: + del self._flags[f] + + def __setitem__(self, f, n): + assert n is not None + dir, subpath = _splittopdir(f) + if dir: + if dir not in self._dirs: + self._dirs[dir] = treemanifest(self._subpath(dir)) + self._dirs[dir].__setitem__(subpath, n) + else: + self._files[f] = n[:21] # to match manifestdict's behavior + + def setflag(self, f, flags): + """Set the flags (symlink, executable) for path f.""" + dir, subpath = _splittopdir(f) + if dir: + if dir not in self._dirs: + self._dirs[dir] = treemanifest(self._subpath(dir)) + self._dirs[dir].setflag(subpath, flags) + else: + self._flags[f] = flags + + def copy(self): + copy = treemanifest(self._dir) + for d in self._dirs: + copy._dirs[d] = self._dirs[d].copy() + copy._files = dict.copy(self._files) + copy._flags = dict.copy(self._flags) + return copy + + def filesnotin(self, m2): + '''Set of files in this manifest that are not in the other''' + files = set() + def _filesnotin(t1, t2): + for d, m1 in t1._dirs.iteritems(): + if d in t2._dirs: + m2 = t2._dirs[d] + _filesnotin(m1, m2) + else: + files.update(m1.iterkeys()) + + for fn in t1._files.iterkeys(): + if fn not in t2._files: + files.add(t1._subpath(fn)) + + _filesnotin(self, m2) + return files + + @propertycache + def _alldirs(self): + return util.dirs(self) + + def dirs(self): + return self._alldirs + + def hasdir(self, dir): + topdir, subdir = _splittopdir(dir) + if topdir: + if topdir in self._dirs: + return self._dirs[topdir].hasdir(subdir) + return False + return (dir + '/') in self._dirs + + def walk(self, match): + '''Generates matching file names. + + Equivalent to manifest.matches(match).iterkeys(), but without creating + an entirely new manifest. + + It also reports nonexistent files by marking them bad with match.bad(). + ''' + if match.always(): + for f in iter(self): + yield f + return + + fset = set(match.files()) + + for fn in self._walk(match): + if fn in fset: + # specified pattern is the exact name + fset.remove(fn) + yield fn + + # for dirstate.walk, files=['.'] means "walk the whole tree". + # follow that here, too + fset.discard('.') + + for fn in sorted(fset): + if not self.hasdir(fn): + match.bad(fn, None) + + def _walk(self, match, alldirs=False): + '''Recursively generates matching file names for walk(). + + Will visit all subdirectories if alldirs is True, otherwise it will + only visit subdirectories for which match.visitdir is True.''' + + if not alldirs: + # substring to strip trailing slash + visit = match.visitdir(self._dir[:-1] or '.') + if not visit: + return + alldirs = (visit == 'all') + + # yield this dir's files and walk its submanifests + for p in sorted(self._dirs.keys() + self._files.keys()): + if p in self._files: + fullp = self._subpath(p) + if match(fullp): + yield fullp + else: + for f in self._dirs[p]._walk(match, alldirs): + yield f + + def matches(self, match): + '''generate a new manifest filtered by the match argument''' + if match.always(): + return self.copy() + + return self._matches(match) + + def _matches(self, match, alldirs=False): + '''recursively generate a new manifest filtered by the match argument. + + Will visit all subdirectories if alldirs is True, otherwise it will + only visit subdirectories for which match.visitdir is True.''' + + ret = treemanifest(self._dir) + if not alldirs: + # substring to strip trailing slash + visit = match.visitdir(self._dir[:-1] or '.') + if not visit: + return ret + alldirs = (visit == 'all') + + for fn in self._files: + fullp = self._subpath(fn) + if not match(fullp): + continue + ret._files[fn] = self._files[fn] + if fn in self._flags: + ret._flags[fn] = self._flags[fn] + + for dir, subm in self._dirs.iteritems(): + m = subm._matches(match, alldirs) + if not m._isempty(): + ret._dirs[dir] = m + + return ret + + def diff(self, m2, clean=False): + '''Finds changes between the current manifest and m2. + + Args: + m2: the manifest to which this manifest should be compared. + clean: if true, include files unchanged between these manifests + with a None value in the returned dictionary. + + The result is returned as a dict with filename as key and + values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the + nodeid in the current/other manifest and fl1/fl2 is the flag + in the current/other manifest. Where the file does not exist, + the nodeid will be None and the flags will be the empty + string. + ''' + result = {} + emptytree = treemanifest() + def _diff(t1, t2): + for d, m1 in t1._dirs.iteritems(): + m2 = t2._dirs.get(d, emptytree) + _diff(m1, m2) + + for d, m2 in t2._dirs.iteritems(): + if d not in t1._dirs: + _diff(emptytree, m2) + + for fn, n1 in t1._files.iteritems(): + fl1 = t1._flags.get(fn, '') + n2 = t2._files.get(fn, None) + fl2 = t2._flags.get(fn, '') + if n1 != n2 or fl1 != fl2: + result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2)) + elif clean: + result[t1._subpath(fn)] = None + + for fn, n2 in t2._files.iteritems(): + if fn not in t1._files: + fl2 = t2._flags.get(fn, '') + result[t2._subpath(fn)] = ((None, ''), (n2, fl2)) + + _diff(self, m2) + return result + + def parse(self, text): + for f, n, fl in _parse(text): + self[f] = n + if fl: + self.setflag(f, fl) + + def text(self, usemanifestv2=False): + """Get the full data of this manifest as a bytestring.""" + flags = self.flags + return _text(((f, self[f], flags(f)) for f in self.keys()), + usemanifestv2) class manifest(revlog.revlog): def __init__(self, opener): - # we expect to deal with not more than four revs at a time, - # during a commit --amend - self._mancache = util.lrucachedict(4) + # During normal operations, we expect to deal with not more than four + # revs at a time (such as during commit --amend). When rebasing large + # stacks of commits, the number can go up, hence the config knob below. + cachesize = 4 + usetreemanifest = False + usemanifestv2 = False + opts = getattr(opener, 'options', None) + if opts is not None: + cachesize = opts.get('manifestcachesize', cachesize) + usetreemanifest = opts.get('usetreemanifest', usetreemanifest) + usemanifestv2 = opts.get('manifestv2', usemanifestv2) + self._mancache = util.lrucachedict(cachesize) revlog.revlog.__init__(self, opener, "00manifest.i") + self._treeinmem = usetreemanifest + self._treeondisk = usetreemanifest + self._usemanifestv2 = usemanifestv2 + + def _newmanifest(self, data=''): + if self._treeinmem: + return treemanifest('', data) + return manifestdict(data) + + def _slowreaddelta(self, node): + r0 = self.deltaparent(self.rev(node)) + m0 = self.read(self.node(r0)) + m1 = self.read(node) + md = self._newmanifest() + for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).iteritems(): + if n1: + md[f] = n1 + if fl1: + md.setflag(f, fl1) + return md def readdelta(self, node): + if self._usemanifestv2 or self._treeondisk: + return self._slowreaddelta(node) r = self.rev(node) - return _parse(mdiff.patchtext(self.revdiff(self.deltaparent(r), r))) + d = mdiff.patchtext(self.revdiff(self.deltaparent(r), r)) + return self._newmanifest(d) def readfast(self, node): '''use the faster of readdelta or read''' @@ -239,31 +802,27 @@ def read(self, node): if node == revlog.nullid: - return manifestdict() # don't upset local cache + return self._newmanifest() # don't upset local cache if node in self._mancache: return self._mancache[node][0] text = self.revision(node) arraytext = array.array('c', text) - mapping = _parse(text) - self._mancache[node] = (mapping, arraytext) - return mapping + m = self._newmanifest(text) + self._mancache[node] = (m, arraytext) + return m def find(self, node, f): '''look up entry for a single file efficiently. return (node, flags) pair if found, (None, None) if not.''' - if node in self._mancache: - mapping = self._mancache[node][0] - return mapping.get(f), mapping.flags(f) - text = self.revision(node) - start, end = _msearch(text, f) - if start == end: + m = self.read(node) + try: + return m.find(f) + except KeyError: return None, None - l = text[start:end] - f, n = l.split('\0') - return revlog.bin(n[:40]), n[40:-1] - def add(self, map, transaction, link, p1, p2, added, removed): - if p1 in self._mancache: + def add(self, m, transaction, link, p1, p2, added, removed): + if (p1 in self._mancache and not self._treeinmem + and not self._usemanifestv2): # If our first parent is in the manifest cache, we can # compute a delta here using properties we know about the # manifest up-front, which may save time later for the @@ -277,19 +836,19 @@ # since the lists are already sorted work.sort() - arraytext, deltatext = map.fastdelta(self._mancache[p1][1], work) + arraytext, deltatext = m.fastdelta(self._mancache[p1][1], work) cachedelta = self.rev(p1), deltatext text = util.buffer(arraytext) + n = self.addrevision(text, transaction, link, p1, p2, cachedelta) else: # The first parent manifest isn't already loaded, so we'll # just encode a fulltext of the manifest and pass that # through to the revlog layer, and let it handle the delta # process. - text = map.text() + text = m.text(self._usemanifestv2) arraytext = array.array('c', text) - cachedelta = None + n = self.addrevision(text, transaction, link, p1, p2) - n = self.addrevision(text, transaction, link, p1, p2, cachedelta) - self._mancache[n] = (map, arraytext) + self._mancache[n] = (m, arraytext) return n diff -r 612ed41ae359 -r e89f909edffa mercurial/match.py --- a/mercurial/match.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/match.py Thu Apr 16 20:57:51 2015 -0500 @@ -9,6 +9,8 @@ import util, pathutil from i18n import _ +propertycache = util.propertycache + def _rematcher(regex): '''compile the regexp with the best available regexp engine and return a matcher function''' @@ -34,6 +36,15 @@ other.append((kind, pat)) return fset, other +def _kindpatsalwaysmatch(kindpats): + """"Checks whether the kindspats match everything, as e.g. + 'relpath:.' does. + """ + for kind, pat in kindpats: + if pat != '' or kind not in ['relpath', 'glob']: + return False + return True + class match(object): def __init__(self, root, cwd, patterns, include=[], exclude=[], default='glob', exact=False, auditor=None, ctx=None): @@ -63,17 +74,16 @@ self._cwd = cwd self._files = [] # exact files and roots of patterns self._anypats = bool(include or exclude) - self._ctx = ctx self._always = False self._pathrestricted = bool(include or exclude or patterns) matchfns = [] if include: - kindpats = _normalize(include, 'glob', root, cwd, auditor) + kindpats = self._normalize(include, 'glob', root, cwd, auditor) self.includepat, im = _buildmatch(ctx, kindpats, '(?:/|$)') matchfns.append(im) if exclude: - kindpats = _normalize(exclude, 'glob', root, cwd, auditor) + kindpats = self._normalize(exclude, 'glob', root, cwd, auditor) self.excludepat, em = _buildmatch(ctx, kindpats, '(?:/|$)') matchfns.append(lambda f: not em(f)) if exact: @@ -83,11 +93,12 @@ self._files = list(patterns) matchfns.append(self.exact) elif patterns: - kindpats = _normalize(patterns, default, root, cwd, auditor) - self._files = _roots(kindpats) - self._anypats = self._anypats or _anypats(kindpats) - self.patternspat, pm = _buildmatch(ctx, kindpats, '$') - matchfns.append(pm) + kindpats = self._normalize(patterns, default, root, cwd, auditor) + if not _kindpatsalwaysmatch(kindpats): + self._files = _roots(kindpats) + self._anypats = self._anypats or _anypats(kindpats) + self.patternspat, pm = _buildmatch(ctx, kindpats, '$') + matchfns.append(pm) if not matchfns: m = util.always @@ -148,6 +159,20 @@ else: optimal roots''' return self._files + @propertycache + def _dirs(self): + return set(util.dirs(self._fmap)) | set(['.']) + + def visitdir(self, dir): + '''Helps while traversing a directory tree. Returns the string 'all' if + the given directory and all subdirectories should be visited. Otherwise + returns True or False indicating whether the given directory should be + visited. If 'all' is returned, calling this method on a subdirectory + gives an undefined result.''' + if not self._fmap or self.exact(dir): + return 'all' + return dir in self._dirs + def exact(self, f): '''Returns True if f is in .files().''' return f in self._fmap @@ -161,6 +186,34 @@ - optimization might be possible and necessary.''' return self._always + def isexact(self): + return self.matchfn == self.exact + + def _normalize(self, patterns, default, root, cwd, auditor): + '''Convert 'kind:pat' from the patterns list to tuples with kind and + normalized and rooted patterns and with listfiles expanded.''' + kindpats = [] + for kind, pat in [_patsplit(p, default) for p in patterns]: + if kind in ('glob', 'relpath'): + pat = pathutil.canonpath(root, cwd, pat, auditor) + elif kind in ('relglob', 'path'): + pat = util.normpath(pat) + elif kind in ('listfile', 'listfile0'): + try: + files = util.readfile(pat) + if kind == 'listfile0': + files = files.split('\0') + else: + files = files.splitlines() + files = [f for f in files if f] + except EnvironmentError: + raise util.Abort(_("unable to read file list (%s)") % pat) + kindpats += self._normalize(files, default, root, cwd, auditor) + continue + # else: re or relre - which cannot be normalized + kindpats.append((kind, pat)) + return kindpats + def exact(root, cwd, files): return match(root, cwd, files, exact=True) @@ -220,6 +273,34 @@ def rel(self, f): return self._matcher.rel(self._path + "/" + f) +class icasefsmatcher(match): + """A matcher for wdir on case insensitive filesystems, which normalizes the + given patterns to the case in the filesystem. + """ + + def __init__(self, root, cwd, patterns, include, exclude, default, auditor, + ctx): + init = super(icasefsmatcher, self).__init__ + self._dsnormalize = ctx.repo().dirstate.normalize + + init(root, cwd, patterns, include, exclude, default, auditor=auditor, + ctx=ctx) + + # m.exact(file) must be based off of the actual user input, otherwise + # inexact case matches are treated as exact, and not noted without -v. + if self._files: + self._fmap = set(_roots(self._kp)) + + def _normalize(self, patterns, default, root, cwd, auditor): + self._kp = super(icasefsmatcher, self)._normalize(patterns, default, + root, cwd, auditor) + kindpats = [] + for kind, pats in self._kp: + if kind not in ('re', 'relre'): # regex can't be normalized + pats = self._dsnormalize(pats) + kindpats.append((kind, pats)) + return kindpats + def patkind(pattern, default=None): '''If pattern is 'kind:pat' with a known kind, return kind.''' return _patsplit(pattern, default)[0] @@ -370,31 +451,6 @@ raise util.Abort(_("invalid pattern (%s): %s") % (k, p)) raise util.Abort(_("invalid pattern")) -def _normalize(patterns, default, root, cwd, auditor): - '''Convert 'kind:pat' from the patterns list to tuples with kind and - normalized and rooted patterns and with listfiles expanded.''' - kindpats = [] - for kind, pat in [_patsplit(p, default) for p in patterns]: - if kind in ('glob', 'relpath'): - pat = pathutil.canonpath(root, cwd, pat, auditor) - elif kind in ('relglob', 'path'): - pat = util.normpath(pat) - elif kind in ('listfile', 'listfile0'): - try: - files = util.readfile(pat) - if kind == 'listfile0': - files = files.split('\0') - else: - files = files.splitlines() - files = [f for f in files if f] - except EnvironmentError: - raise util.Abort(_("unable to read file list (%s)") % pat) - kindpats += _normalize(files, default, root, cwd, auditor) - continue - # else: re or relre - which cannot be normalized - kindpats.append((kind, pat)) - return kindpats - def _roots(kindpats): '''return roots and exact explicitly listed files from patterns diff -r 612ed41ae359 -r e89f909edffa mercurial/mdiff.py --- a/mercurial/mdiff.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/mdiff.py Thu Apr 16 20:57:51 2015 -0500 @@ -367,6 +367,9 @@ def trivialdiffheader(length): return struct.pack(">lll", 0, 0, length) +def replacediffheader(oldlen, newlen): + return struct.pack(">lll", 0, oldlen, newlen) + patches = mpatch.patches patchedsize = mpatch.patchedsize textdiff = bdiff.bdiff diff -r 612ed41ae359 -r e89f909edffa mercurial/merge.py --- a/mercurial/merge.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/merge.py Thu Apr 16 20:57:51 2015 -0500 @@ -1045,9 +1045,7 @@ raise util.Abort(_("uncommitted changes"), hint=_("use 'hg status' to list changes")) for s in sorted(wc.substate): - if wc.sub(s).dirty(): - raise util.Abort(_("uncommitted changes in " - "subrepository '%s'") % s) + wc.sub(s).bailifchanged() elif not overwrite: if p1 == p2: # no-op update @@ -1186,9 +1184,17 @@ labels - merge labels eg ['local', 'graft'] """ + # If we're grafting a descendant onto an ancestor, be sure to pass + # mergeancestor=True to update. This does two things: 1) allows the merge if + # the destination is the same as the parent of the ctx (so we can use graft + # to copy commits), and 2) informs update that the incoming changes are + # newer than the destination so it doesn't prompt about "remote changed foo + # which local deleted". + mergeancestor = repo.changelog.isancestor(repo['.'].node(), ctx.node()) stats = update(repo, ctx.node(), True, True, False, pctx.node(), - labels=labels) + mergeancestor=mergeancestor, labels=labels) + # drop the second merge parent repo.dirstate.beginparentchange() repo.setparents(repo['.'].node(), nullid) diff -r 612ed41ae359 -r e89f909edffa mercurial/namespaces.py --- a/mercurial/namespaces.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/namespaces.py Thu Apr 16 20:57:51 2015 -0500 @@ -142,7 +142,7 @@ is used colorname: the name to use for colored log output; if not specified logname is used - logfmt: the format to use for (l10n-ed) log output; if not specified + logfmt: the format to use for (i18n-ed) log output; if not specified it is composed from logname listnames: function to list all names namemap: function that inputs a node, output name(s) diff -r 612ed41ae359 -r e89f909edffa mercurial/obsolete.py --- a/mercurial/obsolete.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/obsolete.py Thu Apr 16 20:57:51 2015 -0500 @@ -68,15 +68,14 @@ """ import struct -import util, base85, node +import util, base85, node, parsers import phases from i18n import _ _pack = struct.pack _unpack = struct.unpack _calcsize = struct.calcsize - -_SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5 +propertycache = util.propertycache # the obsolete feature is not mature enough to be enabled by default. # you have to rely on third party extension extension to enable this. @@ -146,7 +145,7 @@ _fm0fsize = _calcsize(_fm0fixed) _fm0fnodesize = _calcsize(_fm0node) -def _fm0readmarkers(data, off=0): +def _fm0readmarkers(data, off): # Loop on markers l = len(data) while off + _fm0fsize <= l: @@ -285,7 +284,7 @@ _fm1metapair = 'BB' _fm1metapairsize = _calcsize('BB') -def _fm1readmarkers(data, off=0): +def _fm1purereadmarkers(data, off): # make some global constants local for performance noneflag = _fm1parentnone sha2flag = usingsha256 @@ -301,6 +300,7 @@ # Loop on markers stop = len(data) - _fm1fsize ufixed = util.unpacker(_fm1fixed) + while off <= stop: # read fixed part o1 = off + fsize @@ -395,6 +395,13 @@ data.append(value) return ''.join(data) +def _fm1readmarkers(data, off): + native = getattr(parsers, 'fm1readmarkers', None) + if not native: + return _fm1purereadmarkers(data, off) + stop = len(data) - _fm1fsize + return native(data, off, stop) + # mapping to read/write various marker formats # -> (decoder, encoder) formats = {_fm0version: (_fm0readmarkers, _fm0encodeonemarker), @@ -462,15 +469,35 @@ """The flags field of the marker""" return self._data[2] -def _checkinvalidmarkers(obsstore): +@util.nogc +def _addsuccessors(successors, markers): + for mark in markers: + successors.setdefault(mark[0], set()).add(mark) + +@util.nogc +def _addprecursors(precursors, markers): + for mark in markers: + for suc in mark[1]: + precursors.setdefault(suc, set()).add(mark) + +@util.nogc +def _addchildren(children, markers): + for mark in markers: + parents = mark[5] + if parents is not None: + for p in parents: + children.setdefault(p, set()).add(mark) + +def _checkinvalidmarkers(markers): """search for marker with invalid data and raise error if needed Exist as a separated function to allow the evolve extension for a more subtle handling. """ - if node.nullid in obsstore.precursors: - raise util.Abort(_('bad obsolescence marker detected: ' - 'invalid successors nullid')) + for mark in markers: + if node.nullid in mark[1]: + raise util.Abort(_('bad obsolescence marker detected: ' + 'invalid successors nullid')) class obsstore(object): """Store obsolete markers @@ -494,16 +521,13 @@ # caches for various obsolescence related cache self.caches = {} self._all = [] - self.precursors = {} - self.successors = {} - self.children = {} self.sopener = sopener data = sopener.tryread('obsstore') self._version = defaultformat self._readonly = readonly if data: self._version, markers = _readmarkers(data) - self._load(markers) + self._addmarkers(markers) def __iter__(self): return iter(self._all) @@ -566,12 +590,6 @@ if new: f = self.sopener('obsstore', 'ab') try: - # Whether the file's current position is at the begin or at - # the end after opening a file for appending is implementation - # defined. So we must seek to the end before calling tell(), - # or we may get a zero offset for non-zero sized files on - # some platforms (issue3543). - f.seek(0, _SEEK_END) offset = f.tell() transaction.add('obsstore', offset) # offset == 0: new file - add the version header @@ -581,7 +599,7 @@ # XXX: f.close() == filecache invalidation == obsstore rebuilt. # call 'filecacheentry.refresh()' here f.close() - self._load(new) + self._addmarkers(new) # new marker *may* have changed several set. invalidate the cache. self.caches.clear() # records the number of new markers for the transaction hooks @@ -596,19 +614,37 @@ version, markers = _readmarkers(data) return self.add(transaction, markers) - @util.nogc - def _load(self, markers): - for mark in markers: - self._all.append(mark) - pre, sucs = mark[:2] - self.successors.setdefault(pre, set()).add(mark) - for suc in sucs: - self.precursors.setdefault(suc, set()).add(mark) - parents = mark[5] - if parents is not None: - for p in parents: - self.children.setdefault(p, set()).add(mark) - _checkinvalidmarkers(self) + @propertycache + def successors(self): + successors = {} + _addsuccessors(successors, self._all) + return successors + + @propertycache + def precursors(self): + precursors = {} + _addprecursors(precursors, self._all) + return precursors + + @propertycache + def children(self): + children = {} + _addchildren(children, self._all) + return children + + def _cached(self, attr): + return attr in self.__dict__ + + def _addmarkers(self, markers): + markers = list(markers) # to allow repeated iteration + self._all.extend(markers) + if self._cached('successors'): + _addsuccessors(self.successors, markers) + if self._cached('precursors'): + _addprecursors(self.precursors, markers) + if self._cached('children'): + _addchildren(self.children, markers) + _checkinvalidmarkers(markers) def relevantmarkers(self, nodes): """return a set of all obsolescence markers relevant to a set of nodes. @@ -726,13 +762,13 @@ def precursormarkers(ctx): """obsolete marker marking this changeset as a successors""" - for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()): - yield marker(ctx._repo, data) + for data in ctx.repo().obsstore.precursors.get(ctx.node(), ()): + yield marker(ctx.repo(), data) def successormarkers(ctx): """obsolete marker making this changeset obsolete""" - for data in ctx._repo.obsstore.successors.get(ctx.node(), ()): - yield marker(ctx._repo, data) + for data in ctx.repo().obsstore.successors.get(ctx.node(), ()): + yield marker(ctx.repo(), data) def allsuccessors(obsstore, nodes, ignoreflags=0): """Yield node for every successor of . @@ -1128,8 +1164,12 @@ for ctx in repo.set('(not public()) - obsolete()'): mark = obsstore.precursors.get(ctx.node(), ()) toprocess = set(mark) + seen = set() while toprocess: prec = toprocess.pop()[0] + if prec in seen: + continue # emergency cycle hanging prevention + seen.add(prec) if prec not in newermap: successorssets(repo, prec, newermap) newer = [n for n in newermap[prec] if n] diff -r 612ed41ae359 -r e89f909edffa mercurial/osutil.c --- a/mercurial/osutil.c Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/osutil.c Thu Apr 16 20:57:51 2015 -0500 @@ -24,6 +24,11 @@ #include #endif +#ifdef __APPLE__ +#include +#include +#endif + #include "util.h" /* some platforms lack the PATH_MAX definition (eg. GNU/Hurd) */ @@ -286,7 +291,8 @@ return stat; } -static PyObject *_listdir(char *path, int pathlen, int keepstat, char *skip) +static PyObject *_listdir_stat(char *path, int pathlen, int keepstat, + char *skip) { PyObject *list, *elem, *stat = NULL, *ret = NULL; char fullpath[PATH_MAX + 10]; @@ -337,7 +343,7 @@ #else strncpy(fullpath + pathlen + 1, ent->d_name, PATH_MAX - pathlen); - fullpath[PATH_MAX] = 0; + fullpath[PATH_MAX] = '\0'; err = lstat(fullpath, &st); #endif if (err == -1) { @@ -391,6 +397,198 @@ return ret; } +#ifdef __APPLE__ + +typedef struct { + u_int32_t length; + attrreference_t name; + fsobj_type_t obj_type; + struct timespec mtime; +#if __LITTLE_ENDIAN__ + mode_t access_mask; + uint16_t padding; +#else + uint16_t padding; + mode_t access_mask; +#endif + off_t size; +} __attribute__((packed)) attrbuf_entry; + +int attrkind(attrbuf_entry *entry) +{ + switch (entry->obj_type) { + case VREG: return S_IFREG; + case VDIR: return S_IFDIR; + case VLNK: return S_IFLNK; + case VBLK: return S_IFBLK; + case VCHR: return S_IFCHR; + case VFIFO: return S_IFIFO; + case VSOCK: return S_IFSOCK; + } + return -1; +} + +/* get these many entries at a time */ +#define LISTDIR_BATCH_SIZE 50 + +static PyObject *_listdir_batch(char *path, int pathlen, int keepstat, + char *skip, bool *fallback) +{ + PyObject *list, *elem, *stat = NULL, *ret = NULL; + int kind, err; + unsigned long index; + unsigned int count, old_state, new_state; + bool state_seen = false; + attrbuf_entry *entry; + /* from the getattrlist(2) man page: a path can be no longer than + (NAME_MAX * 3 + 1) bytes. Also, "The getattrlist() function will + silently truncate attribute data if attrBufSize is too small." So + pass in a buffer big enough for the worst case. */ + char attrbuf[LISTDIR_BATCH_SIZE * (sizeof(attrbuf_entry) + NAME_MAX * 3 + 1)]; + unsigned int basep_unused; + + struct stat st; + int dfd = -1; + + /* these must match the attrbuf_entry struct, otherwise you'll end up + with garbage */ + struct attrlist requested_attr = {0}; + requested_attr.bitmapcount = ATTR_BIT_MAP_COUNT; + requested_attr.commonattr = (ATTR_CMN_NAME | ATTR_CMN_OBJTYPE | + ATTR_CMN_MODTIME | ATTR_CMN_ACCESSMASK); + requested_attr.fileattr = ATTR_FILE_TOTALSIZE; + + *fallback = false; + + if (pathlen >= PATH_MAX) { + errno = ENAMETOOLONG; + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + goto error_value; + } + + dfd = open(path, O_RDONLY); + if (dfd == -1) { + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + goto error_value; + } + + list = PyList_New(0); + if (!list) + goto error_dir; + + do { + count = LISTDIR_BATCH_SIZE; + err = getdirentriesattr(dfd, &requested_attr, &attrbuf, + sizeof(attrbuf), &count, &basep_unused, + &new_state, 0); + if (err < 0) { + if (errno == ENOTSUP) { + /* We're on a filesystem that doesn't support + getdirentriesattr. Fall back to the + stat-based implementation. */ + *fallback = true; + } else + PyErr_SetFromErrnoWithFilename(PyExc_OSError, path); + goto error; + } + + if (!state_seen) { + old_state = new_state; + state_seen = true; + } else if (old_state != new_state) { + /* There's an edge case with getdirentriesattr. Consider + the following initial list of files: + + a + b + <-- + c + d + + If the iteration is paused at the arrow, and b is + deleted before it is resumed, getdirentriesattr will + not return d at all! Ordinarily we're expected to + restart the iteration from the beginning. To avoid + getting stuck in a retry loop here, fall back to + stat. */ + *fallback = true; + goto error; + } + + entry = (attrbuf_entry *)attrbuf; + + for (index = 0; index < count; index++) { + char *filename = ((char *)&entry->name) + + entry->name.attr_dataoffset; + + if (!strcmp(filename, ".") || !strcmp(filename, "..")) + continue; + + kind = attrkind(entry); + if (kind == -1) { + PyErr_Format(PyExc_OSError, + "unknown object type %u for file " + "%s%s!", + entry->obj_type, path, filename); + goto error; + } + + /* quit early? */ + if (skip && kind == S_IFDIR && !strcmp(filename, skip)) { + ret = PyList_New(0); + goto error; + } + + if (keepstat) { + /* from the getattrlist(2) man page: "Only the + permission bits ... are valid". */ + st.st_mode = (entry->access_mask & ~S_IFMT) | kind; + st.st_mtime = entry->mtime.tv_sec; + st.st_size = entry->size; + stat = makestat(&st); + if (!stat) + goto error; + elem = Py_BuildValue("siN", filename, kind, stat); + } else + elem = Py_BuildValue("si", filename, kind); + if (!elem) + goto error; + stat = NULL; + + PyList_Append(list, elem); + Py_DECREF(elem); + + entry = (attrbuf_entry *)((char *)entry + entry->length); + } + } while (err == 0); + + ret = list; + Py_INCREF(ret); + +error: + Py_DECREF(list); + Py_XDECREF(stat); +error_dir: + close(dfd); +error_value: + return ret; +} + +#endif /* __APPLE__ */ + +static PyObject *_listdir(char *path, int pathlen, int keepstat, char *skip) +{ +#ifdef __APPLE__ + PyObject *ret; + bool fallback = false; + + ret = _listdir_batch(path, pathlen, keepstat, skip, &fallback); + if (ret != NULL || !fallback) + return ret; +#endif + return _listdir_stat(path, pathlen, keepstat, skip); +} + static PyObject *statfiles(PyObject *self, PyObject *args) { PyObject *names, *stats; diff -r 612ed41ae359 -r e89f909edffa mercurial/parsers.c --- a/mercurial/parsers.c Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/parsers.c Thu Apr 16 20:57:51 2015 -0500 @@ -56,6 +56,27 @@ '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e', '\x7f' }; +static char uppertable[128] = { + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', + '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', + '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f', + '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', + '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f', + '\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', + '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', + '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57', + '\x58', '\x59', '\x5a', '\x5b', '\x5c', '\x5d', '\x5e', '\x5f', + '\x60', + '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', /* a-g */ + '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', /* h-o */ + '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57', /* p-w */ + '\x58', '\x59', '\x5a', /* x-z */ + '\x7b', '\x7c', '\x7d', '\x7e', '\x7f' +}; + static inline int hexdigit(const char *p, Py_ssize_t off) { int8_t val = hextable[(unsigned char)p[off]]; @@ -71,7 +92,7 @@ /* * Turn a hex-encoded string into binary. */ -static PyObject *unhexlify(const char *str, int len) +PyObject *unhexlify(const char *str, int len) { PyObject *ret; char *d; @@ -93,14 +114,17 @@ return ret; } -static PyObject *asciilower(PyObject *self, PyObject *args) +static inline PyObject *_asciitransform(PyObject *str_obj, + const char table[128], + PyObject *fallback_fn) { char *str, *newstr; - int i, len; + Py_ssize_t i, len; PyObject *newobj = NULL; + PyObject *ret = NULL; - if (!PyArg_ParseTuple(args, "s#", &str, &len)) - goto quit; + str = PyBytes_AS_STRING(str_obj); + len = PyBytes_GET_SIZE(str_obj); newobj = PyBytes_FromStringAndSize(NULL, len); if (!newobj) @@ -111,19 +135,120 @@ for (i = 0; i < len; i++) { char c = str[i]; if (c & 0x80) { - PyObject *err = PyUnicodeDecodeError_Create( - "ascii", str, len, i, (i + 1), - "unexpected code byte"); - PyErr_SetObject(PyExc_UnicodeDecodeError, err); - Py_XDECREF(err); + if (fallback_fn != NULL) { + ret = PyObject_CallFunctionObjArgs(fallback_fn, + str_obj, NULL); + } else { + PyObject *err = PyUnicodeDecodeError_Create( + "ascii", str, len, i, (i + 1), + "unexpected code byte"); + PyErr_SetObject(PyExc_UnicodeDecodeError, err); + Py_XDECREF(err); + } goto quit; } - newstr[i] = lowertable[(unsigned char)c]; + newstr[i] = table[(unsigned char)c]; } - return newobj; + ret = newobj; + Py_INCREF(ret); quit: Py_XDECREF(newobj); + return ret; +} + +static PyObject *asciilower(PyObject *self, PyObject *args) +{ + PyObject *str_obj; + if (!PyArg_ParseTuple(args, "O!:asciilower", &PyBytes_Type, &str_obj)) + return NULL; + return _asciitransform(str_obj, lowertable, NULL); +} + +static PyObject *asciiupper(PyObject *self, PyObject *args) +{ + PyObject *str_obj; + if (!PyArg_ParseTuple(args, "O!:asciiupper", &PyBytes_Type, &str_obj)) + return NULL; + return _asciitransform(str_obj, uppertable, NULL); +} + +static PyObject *make_file_foldmap(PyObject *self, PyObject *args) +{ + PyObject *dmap, *spec_obj, *normcase_fallback; + PyObject *file_foldmap = NULL; + enum normcase_spec spec; + PyObject *k, *v; + dirstateTupleObject *tuple; + Py_ssize_t pos = 0; + const char *table; + + if (!PyArg_ParseTuple(args, "O!O!O!:make_file_foldmap", + &PyDict_Type, &dmap, + &PyInt_Type, &spec_obj, + &PyFunction_Type, &normcase_fallback)) + goto quit; + + spec = (int)PyInt_AS_LONG(spec_obj); + switch (spec) { + case NORMCASE_LOWER: + table = lowertable; + break; + case NORMCASE_UPPER: + table = uppertable; + break; + case NORMCASE_OTHER: + table = NULL; + break; + default: + PyErr_SetString(PyExc_TypeError, "invalid normcasespec"); + goto quit; + } + +#if PY_VERSION_HEX >= 0x02060000 + /* _PyDict_NewPresized expects a minused parameter, but it actually + creates a dictionary that's the nearest power of two bigger than the + parameter. For example, with the initial minused = 1000, the + dictionary created has size 1024. Of course in a lot of cases that + can be greater than the maximum load factor Python's dict object + expects (= 2/3), so as soon as we cross the threshold we'll resize + anyway. So create a dictionary that's 3/2 the size. Also add some + more to deal with additions outside this function. */ + file_foldmap = _PyDict_NewPresized((PyDict_Size(dmap) / 5) * 8); +#else + file_foldmap = PyDict_New(); +#endif + + if (file_foldmap == NULL) + goto quit; + + while (PyDict_Next(dmap, &pos, &k, &v)) { + if (!dirstate_tuple_check(v)) { + PyErr_SetString(PyExc_TypeError, + "expected a dirstate tuple"); + goto quit; + } + + tuple = (dirstateTupleObject *)v; + if (tuple->state != 'r') { + PyObject *normed; + if (table != NULL) { + normed = _asciitransform(k, table, + normcase_fallback); + } else { + normed = PyObject_CallFunctionObjArgs( + normcase_fallback, k, NULL); + } + + if (normed == NULL) + goto quit; + if (PyDict_SetItem(file_foldmap, normed, k) == -1) + goto quit; + } + } + return file_foldmap; +quit: + Py_XDECREF(file_foldmap); return NULL; } @@ -911,6 +1036,111 @@ } } +static Py_ssize_t add_roots_get_min(indexObject *self, PyObject *list, + Py_ssize_t marker, char *phases) +{ + PyObject *iter = NULL; + PyObject *iter_item = NULL; + Py_ssize_t min_idx = index_length(self) + 1; + long iter_item_long; + + if (PyList_GET_SIZE(list) != 0) { + iter = PyObject_GetIter(list); + if (iter == NULL) + return -2; + while ((iter_item = PyIter_Next(iter))) + { + iter_item_long = PyInt_AS_LONG(iter_item); + Py_DECREF(iter_item); + if (iter_item_long < min_idx) + min_idx = iter_item_long; + phases[iter_item_long] = marker; + } + Py_DECREF(iter); + } + + return min_idx; +} + +static inline void set_phase_from_parents(char *phases, int parent_1, + int parent_2, Py_ssize_t i) +{ + if (parent_1 >= 0 && phases[parent_1] > phases[i]) + phases[i] = phases[parent_1]; + if (parent_2 >= 0 && phases[parent_2] > phases[i]) + phases[i] = phases[parent_2]; +} + +static PyObject *compute_phases(indexObject *self, PyObject *args) +{ + PyObject *roots = Py_None; + PyObject *phaseslist = NULL; + PyObject *phaseroots = NULL; + PyObject *rev = NULL; + PyObject *p1 = NULL; + PyObject *p2 = NULL; + Py_ssize_t addlen = self->added ? PyList_GET_SIZE(self->added) : 0; + Py_ssize_t len = index_length(self) - 1; + Py_ssize_t numphase = 0; + Py_ssize_t minrevallphases = 0; + Py_ssize_t minrevphase = 0; + Py_ssize_t i = 0; + int parent_1, parent_2; + char *phases = NULL; + const char *data; + + if (!PyArg_ParseTuple(args, "O", &roots)) + goto release_none; + if (roots == NULL || !PyList_Check(roots)) + goto release_none; + + phases = calloc(len, 1); /* phase per rev: {0: public, 1: draft, 2: secret} */ + if (phases == NULL) + goto release_none; + /* Put the phase information of all the roots in phases */ + numphase = PyList_GET_SIZE(roots)+1; + minrevallphases = len + 1; + for (i = 0; i < numphase-1; i++) { + phaseroots = PyList_GET_ITEM(roots, i); + if (!PyList_Check(phaseroots)) + goto release_phases; + minrevphase = add_roots_get_min(self, phaseroots, i+1, phases); + if (minrevphase == -2) /* Error from add_roots_get_min */ + goto release_phases; + minrevallphases = MIN(minrevallphases, minrevphase); + } + /* Propagate the phase information from the roots to the revs */ + if (minrevallphases != -1) { + for (i = minrevallphases; i < self->raw_length; i++) { + data = index_deref(self, i); + set_phase_from_parents(phases, getbe32(data+24), getbe32(data+28), i); + } + for (i = 0; i < addlen; i++) { + rev = PyList_GET_ITEM(self->added, i); + p1 = PyTuple_GET_ITEM(rev, 5); + p2 = PyTuple_GET_ITEM(rev, 6); + if (!PyInt_Check(p1) || !PyInt_Check(p2)) { + PyErr_SetString(PyExc_TypeError, "revlog parents are invalid"); + goto release_phases; + } + parent_1 = (int)PyInt_AS_LONG(p1); + parent_2 = (int)PyInt_AS_LONG(p2); + set_phase_from_parents(phases, parent_1, parent_2, i+self->raw_length); + } + } + /* Transform phase list to a python list */ + phaseslist = PyList_New(len); + if (phaseslist == NULL) + goto release_phases; + for (i = 0; i < len; i++) + PyList_SET_ITEM(phaseslist, i, PyInt_FromLong(phases[i])); + +release_phases: + free(phases); +release_none: + return phaseslist; +} + static PyObject *index_headrevs(indexObject *self, PyObject *args) { Py_ssize_t i, len, addlen; @@ -1102,6 +1332,11 @@ static int nt_new(indexObject *self) { if (self->ntlength == self->ntcapacity) { + if (self->ntcapacity >= INT_MAX / (sizeof(nodetree) * 2)) { + PyErr_SetString(PyExc_MemoryError, + "overflow in nt_new"); + return -1; + } self->ntcapacity *= 2; self->nt = realloc(self->nt, self->ntcapacity * sizeof(nodetree)); @@ -1163,7 +1398,7 @@ static int nt_init(indexObject *self) { if (self->nt == NULL) { - if (self->raw_length > INT_MAX) { + if (self->raw_length > INT_MAX / sizeof(nodetree)) { PyErr_SetString(PyExc_ValueError, "overflow in nt_init"); return -1; } @@ -1676,108 +1911,6 @@ } /* - * Given a (possibly overlapping) set of revs, return the greatest - * common ancestors: those with the longest path to the root. - */ -static PyObject *index_ancestors(indexObject *self, PyObject *args) -{ - PyObject *ret = NULL, *gca = NULL; - Py_ssize_t argcount, i, len; - bitmask repeat = 0; - int revcount = 0; - int *revs; - - argcount = PySequence_Length(args); - revs = malloc(argcount * sizeof(*revs)); - if (argcount > 0 && revs == NULL) - return PyErr_NoMemory(); - len = index_length(self) - 1; - - for (i = 0; i < argcount; i++) { - static const int capacity = 24; - PyObject *obj = PySequence_GetItem(args, i); - bitmask x; - long val; - - if (!PyInt_Check(obj)) { - PyErr_SetString(PyExc_TypeError, - "arguments must all be ints"); - Py_DECREF(obj); - goto bail; - } - val = PyInt_AsLong(obj); - Py_DECREF(obj); - if (val == -1) { - ret = PyList_New(0); - goto done; - } - if (val < 0 || val >= len) { - PyErr_SetString(PyExc_IndexError, - "index out of range"); - goto bail; - } - /* this cheesy bloom filter lets us avoid some more - * expensive duplicate checks in the common set-is-disjoint - * case */ - x = 1ull << (val & 0x3f); - if (repeat & x) { - int k; - for (k = 0; k < revcount; k++) { - if (val == revs[k]) - goto duplicate; - } - } - else repeat |= x; - if (revcount >= capacity) { - PyErr_Format(PyExc_OverflowError, - "bitset size (%d) > capacity (%d)", - revcount, capacity); - goto bail; - } - revs[revcount++] = (int)val; - duplicate:; - } - - if (revcount == 0) { - ret = PyList_New(0); - goto done; - } - if (revcount == 1) { - PyObject *obj; - ret = PyList_New(1); - if (ret == NULL) - goto bail; - obj = PyInt_FromLong(revs[0]); - if (obj == NULL) - goto bail; - PyList_SET_ITEM(ret, 0, obj); - goto done; - } - - gca = find_gca_candidates(self, revs, revcount); - if (gca == NULL) - goto bail; - - if (PyList_GET_SIZE(gca) <= 1) { - ret = gca; - Py_INCREF(gca); - } - else ret = find_deepest(self, gca); - -done: - free(revs); - Py_XDECREF(gca); - - return ret; - -bail: - free(revs); - Py_XDECREF(gca); - Py_XDECREF(ret); - return NULL; -} - -/* * Given a (possibly overlapping) set of revs, return all the * common ancestors heads: heads(::args[0] and ::a[1] and ...) */ @@ -1871,6 +2004,24 @@ } /* + * Given a (possibly overlapping) set of revs, return the greatest + * common ancestors: those with the longest path to the root. + */ +static PyObject *index_ancestors(indexObject *self, PyObject *args) +{ + PyObject *gca = index_commonancestorsheads(self, args); + if (gca == NULL) + return NULL; + + if (PyList_GET_SIZE(gca) <= 1) { + Py_INCREF(gca); + return gca; + } + + return find_deepest(self, gca); +} + +/* * Invalidate any trie entries introduced by added revs. */ static void nt_invalidate_added(indexObject *self, Py_ssize_t start) @@ -2127,6 +2278,8 @@ "clear the index caches"}, {"get", (PyCFunction)index_m_get, METH_VARARGS, "get an index entry"}, + {"computephases", (PyCFunction)compute_phases, METH_VARARGS, + "compute phases"}, {"headrevs", (PyCFunction)index_headrevs, METH_VARARGS, "get head revisions"}, /* Can do filtering since 3.2 */ {"headrevsfiltered", (PyCFunction)index_headrevs, METH_VARARGS, @@ -2230,6 +2383,157 @@ return NULL; } +#define BUMPED_FIX 1 +#define USING_SHA_256 2 + +static PyObject *readshas( + const char *source, unsigned char num, Py_ssize_t hashwidth) +{ + int i; + PyObject *list = PyTuple_New(num); + if (list == NULL) { + return NULL; + } + for (i = 0; i < num; i++) { + PyObject *hash = PyString_FromStringAndSize(source, hashwidth); + if (hash == NULL) { + Py_DECREF(list); + return NULL; + } + PyTuple_SetItem(list, i, hash); + source += hashwidth; + } + return list; +} + +static PyObject *fm1readmarker(const char *data, uint32_t *msize) +{ + const char *meta; + + double mtime; + int16_t tz; + uint16_t flags; + unsigned char nsuccs, nparents, nmetadata; + Py_ssize_t hashwidth = 20; + + PyObject *prec = NULL, *parents = NULL, *succs = NULL; + PyObject *metadata = NULL, *ret = NULL; + int i; + + *msize = getbe32(data); + data += 4; + mtime = getbefloat64(data); + data += 8; + tz = getbeint16(data); + data += 2; + flags = getbeuint16(data); + data += 2; + + if (flags & USING_SHA_256) { + hashwidth = 32; + } + + nsuccs = (unsigned char)(*data++); + nparents = (unsigned char)(*data++); + nmetadata = (unsigned char)(*data++); + + prec = PyString_FromStringAndSize(data, hashwidth); + data += hashwidth; + if (prec == NULL) { + goto bail; + } + + succs = readshas(data, nsuccs, hashwidth); + if (succs == NULL) { + goto bail; + } + data += nsuccs * hashwidth; + + if (nparents == 1 || nparents == 2) { + parents = readshas(data, nparents, hashwidth); + if (parents == NULL) { + goto bail; + } + data += nparents * hashwidth; + } else { + parents = Py_None; + } + + meta = data + (2 * nmetadata); + metadata = PyTuple_New(nmetadata); + if (metadata == NULL) { + goto bail; + } + for (i = 0; i < nmetadata; i++) { + PyObject *tmp, *left = NULL, *right = NULL; + Py_ssize_t metasize = (unsigned char)(*data++); + left = PyString_FromStringAndSize(meta, metasize); + meta += metasize; + metasize = (unsigned char)(*data++); + right = PyString_FromStringAndSize(meta, metasize); + meta += metasize; + if (!left || !right) { + Py_XDECREF(left); + Py_XDECREF(right); + goto bail; + } + tmp = PyTuple_Pack(2, left, right); + Py_DECREF(left); + Py_DECREF(right); + if (!tmp) { + goto bail; + } + PyTuple_SetItem(metadata, i, tmp); + } + ret = Py_BuildValue("(OOHO(di)O)", prec, succs, flags, + metadata, mtime, (int)tz * 60, parents); +bail: + Py_XDECREF(prec); + Py_XDECREF(succs); + Py_XDECREF(metadata); + if (parents != Py_None) + Py_XDECREF(parents); + return ret; +} + + +static PyObject *fm1readmarkers(PyObject *self, PyObject *args) { + const char *data; + Py_ssize_t datalen; + /* only unsigned long because python 2.4, should be Py_ssize_t */ + unsigned long offset, stop; + PyObject *markers = NULL; + + /* replace kk with nn when we drop Python 2.4 */ + if (!PyArg_ParseTuple(args, "s#kk", &data, &datalen, &offset, &stop)) { + return NULL; + } + data += offset; + markers = PyList_New(0); + if (!markers) { + return NULL; + } + while (offset < stop) { + uint32_t msize; + int error; + PyObject *record = fm1readmarker(data, &msize); + if (!record) { + goto bail; + } + error = PyList_Append(markers, record); + Py_DECREF(record); + if (error) { + goto bail; + } + data += msize; + offset += msize; + } + return markers; +bail: + Py_DECREF(markers); + return NULL; +} + static char parsers_doc[] = "Efficient content parsing."; PyObject *encodedir(PyObject *self, PyObject *args); @@ -2242,13 +2546,19 @@ {"parse_dirstate", parse_dirstate, METH_VARARGS, "parse a dirstate\n"}, {"parse_index2", parse_index2, METH_VARARGS, "parse a revlog index\n"}, {"asciilower", asciilower, METH_VARARGS, "lowercase an ASCII string\n"}, + {"asciiupper", asciiupper, METH_VARARGS, "uppercase an ASCII string\n"}, + {"make_file_foldmap", make_file_foldmap, METH_VARARGS, + "make file foldmap\n"}, {"encodedir", encodedir, METH_VARARGS, "encodedir a path\n"}, {"pathencode", pathencode, METH_VARARGS, "fncache-encode a path\n"}, {"lowerencode", lowerencode, METH_VARARGS, "lower-encode a path\n"}, + {"fm1readmarkers", fm1readmarkers, METH_VARARGS, + "parse v1 obsolete markers\n"}, {NULL, NULL} }; void dirs_module_init(PyObject *mod); +void manifest_module_init(PyObject *mod); static void module_init(PyObject *mod) { @@ -2263,6 +2573,7 @@ PyModule_AddStringConstant(mod, "versionerrortext", versionerrortext); dirs_module_init(mod); + manifest_module_init(mod); indexType.tp_new = PyType_GenericNew; if (PyType_Ready(&indexType) < 0 || diff -r 612ed41ae359 -r e89f909edffa mercurial/patch.py --- a/mercurial/patch.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/patch.py Thu Apr 16 20:57:51 2015 -0500 @@ -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. -import cStringIO, email, os, errno, re, posixpath +import cStringIO, email, os, errno, re, posixpath, copy import tempfile, zlib, shutil # On python2.4 you have to import these by name or they fail to # load. This was not a problem on Python 2.7. @@ -15,7 +15,9 @@ from i18n import _ from node import hex, short +import cStringIO import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error +import pathutil gitre = re.compile('diff --git a/(.*) b/(.*)') tabsplitter = re.compile(r'(\t+|[^\t]+)') @@ -259,8 +261,17 @@ if not diffs_seen: os.unlink(tmpname) return None, message, user, date, branch, None, None, None - p1 = parents and parents.pop(0) or None - p2 = parents and parents.pop(0) or None + + if parents: + p1 = parents.pop(0) + else: + p1 = None + + if parents: + p2 = parents.pop(0) + else: + p2 = None + return tmpname, message, user, date, branch, nodeid, p1, p2 class patchmeta(object): @@ -804,6 +815,277 @@ self.write_rej() return len(self.rej) +class header(object): + """patch header + """ + diffgit_re = re.compile('diff --git a/(.*) b/(.*)$') + diff_re = re.compile('diff -r .* (.*)$') + allhunks_re = re.compile('(?:index|deleted file) ') + pretty_re = re.compile('(?:new file|deleted file) ') + special_re = re.compile('(?:index|new|deleted|copy|rename) ') + + def __init__(self, header): + self.header = header + self.hunks = [] + + def binary(self): + return util.any(h.startswith('index ') for h in self.header) + + def pretty(self, fp): + for h in self.header: + if h.startswith('index '): + fp.write(_('this modifies a binary file (all or nothing)\n')) + break + if self.pretty_re.match(h): + fp.write(h) + if self.binary(): + fp.write(_('this is a binary file\n')) + break + if h.startswith('---'): + fp.write(_('%d hunks, %d lines changed\n') % + (len(self.hunks), + sum([max(h.added, h.removed) for h in self.hunks]))) + break + fp.write(h) + + def write(self, fp): + fp.write(''.join(self.header)) + + def allhunks(self): + return util.any(self.allhunks_re.match(h) for h in self.header) + + def files(self): + match = self.diffgit_re.match(self.header[0]) + if match: + fromfile, tofile = match.groups() + if fromfile == tofile: + return [fromfile] + return [fromfile, tofile] + else: + return self.diff_re.match(self.header[0]).groups() + + def filename(self): + return self.files()[-1] + + def __repr__(self): + return '
' % (' '.join(map(repr, self.files()))) + + def special(self): + return util.any(self.special_re.match(h) for h in self.header) + +class recordhunk(object): + """patch hunk + + XXX shouldn't we merge this with the other hunk class? + """ + maxcontext = 3 + + def __init__(self, header, fromline, toline, proc, before, hunk, after): + def trimcontext(number, lines): + delta = len(lines) - self.maxcontext + if False and delta > 0: + return number + delta, lines[:self.maxcontext] + return number, lines + + self.header = header + self.fromline, self.before = trimcontext(fromline, before) + self.toline, self.after = trimcontext(toline, after) + self.proc = proc + self.hunk = hunk + self.added, self.removed = self.countchanges(self.hunk) + + def __eq__(self, v): + if not isinstance(v, recordhunk): + return False + + return ((v.hunk == self.hunk) and + (v.proc == self.proc) and + (self.fromline == v.fromline) and + (self.header.files() == v.header.files())) + + def __hash__(self): + return hash((tuple(self.hunk), + tuple(self.header.files()), + self.fromline, + self.proc)) + + def countchanges(self, hunk): + """hunk -> (n+,n-)""" + add = len([h for h in hunk if h[0] == '+']) + rem = len([h for h in hunk if h[0] == '-']) + return add, rem + + def write(self, fp): + delta = len(self.before) + len(self.after) + if self.after and self.after[-1] == '\\ No newline at end of file\n': + delta -= 1 + fromlen = delta + self.removed + tolen = delta + self.added + fp.write('@@ -%d,%d +%d,%d @@%s\n' % + (self.fromline, fromlen, self.toline, tolen, + self.proc and (' ' + self.proc))) + fp.write(''.join(self.before + self.hunk + self.after)) + + pretty = write + + def filename(self): + return self.header.filename() + + def __repr__(self): + return '' % (self.filename(), self.fromline) + +def filterpatch(ui, headers): + """Interactively filter patch chunks into applied-only chunks""" + + def prompt(skipfile, skipall, query, chunk): + """prompt query, and process base inputs + + - y/n for the rest of file + - y/n for the rest + - ? (help) + - q (quit) + + Return True/False and possibly updated skipfile and skipall. + """ + newpatches = None + if skipall is not None: + return skipall, skipfile, skipall, newpatches + if skipfile is not None: + return skipfile, skipfile, skipall, newpatches + while True: + resps = _('[Ynesfdaq?]' + '$$ &Yes, record this change' + '$$ &No, skip this change' + '$$ &Edit this change manually' + '$$ &Skip remaining changes to this file' + '$$ Record remaining changes to this &file' + '$$ &Done, skip remaining changes and files' + '$$ Record &all changes to all remaining files' + '$$ &Quit, recording no changes' + '$$ &? (display help)') + r = ui.promptchoice("%s %s" % (query, resps)) + ui.write("\n") + if r == 8: # ? + for c, t in ui.extractchoices(resps)[1]: + ui.write('%s - %s\n' % (c, t.lower())) + continue + elif r == 0: # yes + ret = True + elif r == 1: # no + ret = False + elif r == 2: # Edit patch + if chunk is None: + ui.write(_('cannot edit patch for whole file')) + ui.write("\n") + continue + if chunk.header.binary(): + ui.write(_('cannot edit patch for binary file')) + ui.write("\n") + continue + # Patch comment based on the Git one (based on comment at end of + # http://mercurial.selenic.com/wiki/RecordExtension) + phelp = '---' + _(""" +To remove '-' lines, make them ' ' lines (context). +To remove '+' lines, delete them. +Lines starting with # will be removed from the patch. + +If the patch applies cleanly, the edited hunk will immediately be +added to the record list. If it does not apply cleanly, a rejects +file will be generated: you can use that when you try again. If +all lines of the hunk are removed, then the edit is aborted and +the hunk is left unchanged. +""") + (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-", + suffix=".diff", text=True) + ncpatchfp = None + try: + # Write the initial patch + f = os.fdopen(patchfd, "w") + chunk.header.write(f) + chunk.write(f) + f.write('\n'.join(['# ' + i for i in phelp.splitlines()])) + f.close() + # Start the editor and wait for it to complete + editor = ui.geteditor() + ui.system("%s \"%s\"" % (editor, patchfn), + environ={'HGUSER': ui.username()}, + onerr=util.Abort, errprefix=_("edit failed")) + # Remove comment lines + patchfp = open(patchfn) + ncpatchfp = cStringIO.StringIO() + for line in patchfp: + if not line.startswith('#'): + ncpatchfp.write(line) + patchfp.close() + ncpatchfp.seek(0) + newpatches = parsepatch(ncpatchfp) + finally: + os.unlink(patchfn) + del ncpatchfp + # Signal that the chunk shouldn't be applied as-is, but + # provide the new patch to be used instead. + ret = False + elif r == 3: # Skip + ret = skipfile = False + elif r == 4: # file (Record remaining) + ret = skipfile = True + elif r == 5: # done, skip remaining + ret = skipall = False + elif r == 6: # all + ret = skipall = True + elif r == 7: # quit + raise util.Abort(_('user quit')) + return ret, skipfile, skipall, newpatches + + seen = set() + applied = {} # 'filename' -> [] of chunks + skipfile, skipall = None, None + pos, total = 1, sum(len(h.hunks) for h in headers) + for h in headers: + pos += len(h.hunks) + skipfile = None + fixoffset = 0 + hdr = ''.join(h.header) + if hdr in seen: + continue + seen.add(hdr) + if skipall is None: + h.pretty(ui) + msg = (_('examine changes to %s?') % + _(' and ').join("'%s'" % f for f in h.files())) + r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None) + if not r: + continue + applied[h.filename()] = [h] + if h.allhunks(): + applied[h.filename()] += h.hunks + continue + for i, chunk in enumerate(h.hunks): + if skipfile is None and skipall is None: + chunk.pretty(ui) + if total == 1: + msg = _("record this change to '%s'?") % chunk.filename() + else: + idx = pos - len(h.hunks) + i + msg = _("record change %d/%d to '%s'?") % (idx, total, + chunk.filename()) + r, skipfile, skipall, newpatches = prompt(skipfile, + skipall, msg, chunk) + if r: + if fixoffset: + chunk = copy.copy(chunk) + chunk.toline += fixoffset + applied[chunk.filename()].append(chunk) + elif newpatches is not None: + for newpatch in newpatches: + for newhunk in newpatch.hunks: + if fixoffset: + newhunk.toline += fixoffset + applied[newhunk.filename()].append(newhunk) + else: + fixoffset += chunk.removed - chunk.added + return sum([h for h in applied.itervalues() + if h[0].special() or len(h) > 1], []) class hunk(object): def __init__(self, desc, num, lr, context): self.number = num @@ -1087,11 +1369,115 @@ return s return s[:i] -def pathstrip(path, strip): +def parsepatch(originalchunks): + """patch -> [] of headers -> [] of hunks """ + class parser(object): + """patch parsing state machine""" + def __init__(self): + self.fromline = 0 + self.toline = 0 + self.proc = '' + self.header = None + self.context = [] + self.before = [] + self.hunk = [] + self.headers = [] + + def addrange(self, limits): + fromstart, fromend, tostart, toend, proc = limits + self.fromline = int(fromstart) + self.toline = int(tostart) + self.proc = proc + + def addcontext(self, context): + if self.hunk: + h = recordhunk(self.header, self.fromline, self.toline, + self.proc, self.before, self.hunk, context) + self.header.hunks.append(h) + self.fromline += len(self.before) + h.removed + self.toline += len(self.before) + h.added + self.before = [] + self.hunk = [] + self.proc = '' + self.context = context + + def addhunk(self, hunk): + if self.context: + self.before = self.context + self.context = [] + self.hunk = hunk + + def newfile(self, hdr): + self.addcontext([]) + h = header(hdr) + self.headers.append(h) + self.header = h + + def addother(self, line): + pass # 'other' lines are ignored + + def finished(self): + self.addcontext([]) + return self.headers + + transitions = { + 'file': {'context': addcontext, + 'file': newfile, + 'hunk': addhunk, + 'range': addrange}, + 'context': {'file': newfile, + 'hunk': addhunk, + 'range': addrange, + 'other': addother}, + 'hunk': {'context': addcontext, + 'file': newfile, + 'range': addrange}, + 'range': {'context': addcontext, + 'hunk': addhunk}, + 'other': {'other': addother}, + } + + p = parser() + fp = cStringIO.StringIO() + fp.write(''.join(originalchunks)) + fp.seek(0) + + state = 'context' + for newstate, data in scanpatch(fp): + try: + p.transitions[state][newstate](p, data) + except KeyError: + raise PatchError('unhandled transition: %s -> %s' % + (state, newstate)) + state = newstate + del fp + return p.finished() + +def pathtransform(path, strip, prefix): + '''turn a path from a patch into a path suitable for the repository + + prefix, if not empty, is expected to be normalized with a / at the end. + + Returns (stripped components, path in repository). + + >>> pathtransform('a/b/c', 0, '') + ('', 'a/b/c') + >>> pathtransform(' a/b/c ', 0, '') + ('', ' a/b/c') + >>> pathtransform(' a/b/c ', 2, '') + ('a/b/', 'c') + >>> pathtransform('a/b/c', 0, 'd/e/') + ('', 'd/e/a/b/c') + >>> pathtransform(' a//b/c ', 2, 'd/e/') + ('a//b/', 'd/e/c') + >>> pathtransform('a/b/c', 3, '') + Traceback (most recent call last): + PatchError: unable to strip away 1 of 3 dirs from a/b/c + ''' pathlen = len(path) i = 0 if strip == 0: - return '', path.rstrip() + return '', prefix + path.rstrip() count = strip while count > 0: i = path.find('/', i) @@ -1103,16 +1489,16 @@ while i < pathlen - 1 and path[i] == '/': i += 1 count -= 1 - return path[:i].lstrip(), path[i:].rstrip() + return path[:i].lstrip(), prefix + path[i:].rstrip() -def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip): +def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix): nulla = afile_orig == "/dev/null" nullb = bfile_orig == "/dev/null" create = nulla and hunk.starta == 0 and hunk.lena == 0 remove = nullb and hunk.startb == 0 and hunk.lenb == 0 - abase, afile = pathstrip(afile_orig, strip) + abase, afile = pathtransform(afile_orig, strip, prefix) gooda = not nulla and backend.exists(afile) - bbase, bfile = pathstrip(bfile_orig, strip) + bbase, bfile = pathtransform(bfile_orig, strip, prefix) if afile == bfile: goodb = gooda else: @@ -1135,13 +1521,19 @@ fname = None if not missing: if gooda and goodb: - fname = isbackup and afile or bfile + if isbackup: + fname = afile + else: + fname = bfile elif gooda: fname = afile if not fname: if not nullb: - fname = isbackup and afile or bfile + if isbackup: + fname = afile + else: + fname = bfile elif not nulla: fname = afile else: @@ -1154,6 +1546,58 @@ gp.op = 'DELETE' return gp +def scanpatch(fp): + """like patch.iterhunks, but yield different events + + - ('file', [header_lines + fromfile + tofile]) + - ('context', [context_lines]) + - ('hunk', [hunk_lines]) + - ('range', (-start,len, +start,len, proc)) + """ + lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') + lr = linereader(fp) + + def scanwhile(first, p): + """scan lr while predicate holds""" + lines = [first] + while True: + line = lr.readline() + if not line: + break + if p(line): + lines.append(line) + else: + lr.push(line) + break + return lines + + while True: + line = lr.readline() + if not line: + break + if line.startswith('diff --git a/') or line.startswith('diff -r '): + def notheader(line): + s = line.split(None, 1) + return not s or s[0] not in ('---', 'diff') + header = scanwhile(line, notheader) + fromfile = lr.readline() + if fromfile.startswith('---'): + tofile = lr.readline() + header += [fromfile, tofile] + else: + lr.push(fromfile) + yield 'file', header + elif line[0] == ' ': + yield 'context', scanwhile(line, lambda l: l[0] in ' \\') + elif line[0] in '-+': + yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') + else: + m = lines_re.match(line) + if m: + yield 'range', m.groups() + else: + yield 'other', line + def scangitpatch(lr, firstline): """ Git patches can emit: @@ -1335,7 +1779,7 @@ raise PatchError(_('unexpected delta opcode 0')) return out -def applydiff(ui, fp, backend, store, strip=1, eolmode='strict'): +def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'): """Reads a patch from fp and tries to apply it. Returns 0 for a clean patch, -1 if any rejects were found and 1 if @@ -1346,13 +1790,18 @@ patching then normalized according to 'eolmode'. """ return _applydiff(ui, fp, patchfile, backend, store, strip=strip, - eolmode=eolmode) + prefix=prefix, eolmode=eolmode) -def _applydiff(ui, fp, patcher, backend, store, strip=1, +def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='', eolmode='strict'): + if prefix: + prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(), + prefix) + if prefix != '': + prefix += '/' def pstrip(p): - return pathstrip(p, strip - 1)[1] + return pathtransform(p, strip - 1, prefix)[1] rejects = 0 err = 0 @@ -1375,7 +1824,8 @@ if gp.oldpath: gp.oldpath = pstrip(gp.oldpath) else: - gp = makepatchmeta(backend, afile, bfile, first_hunk, strip) + gp = makepatchmeta(backend, afile, bfile, first_hunk, strip, + prefix) if gp.op == 'RENAME': backend.unlink(gp.oldpath) if not first_hunk: @@ -1472,7 +1922,8 @@ util.explainexit(code)[0]) return fuzz -def patchbackend(ui, backend, patchobj, strip, files=None, eolmode='strict'): +def patchbackend(ui, backend, patchobj, strip, prefix, files=None, + eolmode='strict'): if files is None: files = set() if eolmode is None: @@ -1487,7 +1938,7 @@ except TypeError: fp = patchobj try: - ret = applydiff(ui, fp, backend, store, strip=strip, + ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode) finally: if fp != patchobj: @@ -1498,19 +1949,19 @@ raise PatchError(_('patch failed to apply')) return ret > 0 -def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict', - similarity=0): +def internalpatch(ui, repo, patchobj, strip, prefix='', files=None, + eolmode='strict', similarity=0): """use builtin patch to apply to the working directory. returns whether patch was applied with fuzz factor.""" backend = workingbackend(ui, repo, similarity) - return patchbackend(ui, backend, patchobj, strip, files, eolmode) + return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode) -def patchrepo(ui, repo, ctx, store, patchobj, strip, files=None, +def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode='strict'): backend = repobackend(ui, repo, ctx, store) - return patchbackend(ui, backend, patchobj, strip, files, eolmode) + return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode) -def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict', +def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict', similarity=0): """Apply to the working directory. @@ -1529,7 +1980,7 @@ if patcher: return _externalpatch(ui, repo, patcher, patchname, strip, files, similarity) - return internalpatch(ui, repo, patchname, strip, files, eolmode, + return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode, similarity) def changedfiles(ui, repo, patchpath, strip=1): @@ -1541,11 +1992,12 @@ if state == 'file': afile, bfile, first_hunk, gp = values if gp: - gp.path = pathstrip(gp.path, strip - 1)[1] + gp.path = pathtransform(gp.path, strip - 1, '')[1] if gp.oldpath: - gp.oldpath = pathstrip(gp.oldpath, strip - 1)[1] + gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1] else: - gp = makepatchmeta(backend, afile, bfile, first_hunk, strip) + gp = makepatchmeta(backend, afile, bfile, first_hunk, strip, + '') changed.add(gp.path) if gp.op == 'RENAME': changed.add(gp.oldpath) @@ -1607,7 +2059,7 @@ return mdiff.diffopts(**buildopts) def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None, - losedatafn=None, prefix=''): + losedatafn=None, prefix='', relroot=''): '''yields diff of changes to files between two nodes, or node and working directory. @@ -1624,7 +2076,9 @@ prefix is a filename prefix that is prepended to all filenames on display (used for subrepos). - ''' + + relroot, if not empty, must be normalized with a trailing /. Any match + patterns that fall outside it will be ignored.''' if opts is None: opts = mdiff.defaultopts @@ -1651,6 +2105,13 @@ ctx1 = repo[node1] ctx2 = repo[node2] + relfiltered = False + if relroot != '' and match.always(): + # as a special case, create a new matcher with just the relroot + pats = [relroot] + match = scmutil.match(ctx2, pats, default='path') + relfiltered = True + if not changes: changes = repo.status(ctx1, ctx2, match=match) modified, added, removed = changes[:3] @@ -1658,16 +2119,35 @@ if not modified and not added and not removed: return [] - hexfunc = repo.ui.debugflag and hex or short + if repo.ui.debugflag: + hexfunc = hex + else: + hexfunc = short revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node] copy = {} if opts.git or opts.upgrade: - copy = copies.pathcopies(ctx1, ctx2) + copy = copies.pathcopies(ctx1, ctx2, match=match) + + if relroot is not None: + if not relfiltered: + # XXX this would ideally be done in the matcher, but that is + # generally meant to 'or' patterns, not 'and' them. In this case we + # need to 'and' all the patterns from the matcher with relroot. + def filterrel(l): + return [f for f in l if f.startswith(relroot)] + modified = filterrel(modified) + added = filterrel(added) + removed = filterrel(removed) + relfiltered = True + # filter out copies where either side isn't inside the relative root + copy = dict(((dst, src) for (dst, src) in copy.iteritems() + if dst.startswith(relroot) + and src.startswith(relroot))) def difffn(opts, losedata): return trydiff(repo, revs, ctx1, ctx2, modified, added, removed, - copy, getfilectx, opts, losedata, prefix) + copy, getfilectx, opts, losedata, prefix, relroot) if opts.upgrade and not opts.git: try: def losedata(fn): @@ -1736,19 +2216,55 @@ '''like diff(), but yields 2-tuples of (output, label) for ui.write()''' return difflabel(diff, *args, **kw) -def trydiff(repo, revs, ctx1, ctx2, modified, added, removed, - copy, getfilectx, opts, losedatafn, prefix): +def _filepairs(ctx1, modified, added, removed, copy, opts): + '''generates tuples (f1, f2, copyop), where f1 is the name of the file + before and f2 is the the name after. For added files, f1 will be None, + and for removed files, f2 will be None. copyop may be set to None, 'copy' + or 'rename' (the latter two only if opts.git is set).''' + gone = set() - def join(f): - return posixpath.join(prefix, f) + copyto = dict([(v, k) for k, v in copy.items()]) + + addedset, removedset = set(added), set(removed) + # Fix up added, since merged-in additions appear as + # modifications during merges + for f in modified: + if f not in ctx1: + addedset.add(f) - def addmodehdr(header, omode, nmode): - if omode != nmode: - header.append('old mode %s\n' % omode) - header.append('new mode %s\n' % nmode) + for f in sorted(modified + added + removed): + copyop = None + f1, f2 = f, f + if f in addedset: + f1 = None + if f in copy: + if opts.git: + f1 = copy[f] + if f1 in removedset and f1 not in gone: + copyop = 'rename' + gone.add(f1) + else: + copyop = 'copy' + elif f in removedset: + f2 = None + if opts.git: + # have we already reported a copy above? + if (f in copyto and copyto[f] in addedset + and copy[copyto[f]] == f): + continue + yield f1, f2, copyop - def addindexmeta(meta, oindex, nindex): - meta.append('index %s..%s\n' % (oindex, nindex)) +def trydiff(repo, revs, ctx1, ctx2, modified, added, removed, + copy, getfilectx, opts, losedatafn, prefix, relroot): + '''given input data, generate a diff and yield it in blocks + + If generating a diff would lose data like flags or binary data and + losedatafn is not None, it will be called. + + relroot is removed and prefix is added to every path in the diff output. + + If relroot is not empty, this function expects every path in modified, + added, removed and copy to start with it.''' def gitindex(text): if not text: @@ -1764,120 +2280,88 @@ aprefix = 'a/' bprefix = 'b/' - def diffline(a, b, revs): - if opts.git: - line = 'diff --git %s%s %s%s\n' % (aprefix, a, bprefix, b) - elif not repo.ui.quiet: - if revs: - revinfo = ' '.join(["-r %s" % rev for rev in revs]) - line = 'diff %s %s\n' % (revinfo, a) - else: - line = 'diff %s\n' % a - else: - line = '' - return line + def diffline(f, revs): + revinfo = ' '.join(["-r %s" % rev for rev in revs]) + return 'diff %s %s' % (revinfo, f) date1 = util.datestr(ctx1.date()) date2 = util.datestr(ctx2.date()) - gone = set() gitmode = {'l': '120000', 'x': '100755', '': '100644'} - copyto = dict([(v, k) for k, v in copy.items()]) - - if opts.git: - revs = None + if relroot != '' and (repo.ui.configbool('devel', 'all') + or repo.ui.configbool('devel', 'check-relroot')): + for f in modified + added + removed + copy.keys() + copy.values(): + if f is not None and not f.startswith(relroot): + raise AssertionError( + "file %s doesn't start with relroot %s" % (f, relroot)) - modifiedset, addedset, removedset = set(modified), set(added), set(removed) - # Fix up modified and added, since merged-in additions appear as - # modifications during merges - for f in modifiedset.copy(): - if f not in ctx1: - addedset.add(f) - modifiedset.remove(f) - for f in sorted(modified + added + removed): - to = None - tn = None - binarydiff = False - header = [] - if f not in addedset: - to = getfilectx(f, ctx1).data() - if f not in removedset: - tn = getfilectx(f, ctx2).data() - a, b = f, f + for f1, f2, copyop in _filepairs( + ctx1, modified, added, removed, copy, opts): + content1 = None + content2 = None + flag1 = None + flag2 = None + if f1: + content1 = getfilectx(f1, ctx1).data() + if opts.git or losedatafn: + flag1 = ctx1.flags(f1) + if f2: + content2 = getfilectx(f2, ctx2).data() + if opts.git or losedatafn: + flag2 = ctx2.flags(f2) + binary = False if opts.git or losedatafn: - if f in addedset: - mode = gitmode[ctx2.flags(f)] - if f in copy or f in copyto: - if opts.git: - if f in copy: - a = copy[f] - else: - a = copyto[f] - omode = gitmode[ctx1.flags(a)] - addmodehdr(header, omode, mode) - if a in removedset and a not in gone: - op = 'rename' - gone.add(a) - else: - op = 'copy' - header.append('%s from %s\n' % (op, join(a))) - header.append('%s to %s\n' % (op, join(f))) - to = getfilectx(a, ctx1).data() - else: - losedatafn(f) - else: - if opts.git: - header.append('new file mode %s\n' % mode) - elif ctx2.flags(f): - losedatafn(f) - if util.binary(to) or util.binary(tn): - if opts.git: - binarydiff = True - else: - losedatafn(f) - if not opts.git and not tn: - # regular diffs cannot represent new empty file - losedatafn(f) - elif f in removedset: - if opts.git: - # have we already reported a copy above? - if ((f in copy and copy[f] in addedset - and copyto[copy[f]] == f) or - (f in copyto and copyto[f] in addedset - and copy[copyto[f]] == f)): - continue - else: - header.append('deleted file mode %s\n' % - gitmode[ctx1.flags(f)]) - if util.binary(to): - binarydiff = True - elif not to or util.binary(to): - # regular diffs cannot represent empty file deletion - losedatafn(f) - else: - oflag = ctx1.flags(f) - nflag = ctx2.flags(f) - binary = util.binary(to) or util.binary(tn) - if opts.git: - addmodehdr(header, gitmode[oflag], gitmode[nflag]) - if binary: - binarydiff = True - elif binary or nflag != oflag: - losedatafn(f) + binary = util.binary(content1) or util.binary(content2) + + if losedatafn and not opts.git: + if (binary or + # copy/rename + f2 in copy or + # empty file creation + (not f1 and not content2) or + # empty file deletion + (not content1 and not f2) or + # create with flags + (not f1 and flag2) or + # change flags + (f1 and f2 and flag1 != flag2)): + losedatafn(f2 or f1) - if opts.git or revs: - header.insert(0, diffline(join(a), join(b), revs)) - if binarydiff and not opts.nobinary: - text = mdiff.b85diff(to, tn) - if text and opts.git: - addindexmeta(header, gitindex(to), gitindex(tn)) + path1 = f1 or f2 + path2 = f2 or f1 + path1 = posixpath.join(prefix, path1[len(relroot):]) + path2 = posixpath.join(prefix, path2[len(relroot):]) + header = [] + if opts.git: + header.append('diff --git %s%s %s%s' % + (aprefix, path1, bprefix, path2)) + if not f1: # added + header.append('new file mode %s' % gitmode[flag2]) + elif not f2: # removed + header.append('deleted file mode %s' % gitmode[flag1]) + else: # modified/copied/renamed + mode1, mode2 = gitmode[flag1], gitmode[flag2] + if mode1 != mode2: + header.append('old mode %s' % mode1) + header.append('new mode %s' % mode2) + if copyop is not None: + header.append('%s from %s' % (copyop, path1)) + header.append('%s to %s' % (copyop, path2)) + elif revs and not repo.ui.quiet: + header.append(diffline(path1, revs)) + + if binary and opts.git and not opts.nobinary: + text = mdiff.b85diff(content1, content2) + if text: + header.append('index %s..%s' % + (gitindex(content1), gitindex(content2))) else: - text = mdiff.unidiff(to, date1, - tn, date2, - join(a), join(b), opts=opts) + text = mdiff.unidiff(content1, date1, + content2, date2, + path1, path2, opts=opts) if header and (text or len(header) > 1): - yield ''.join(header) + yield '\n'.join(header) + '\n' if text: yield text diff -r 612ed41ae359 -r e89f909edffa mercurial/phases.py --- a/mercurial/phases.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/phases.py Thu Apr 16 20:57:51 2015 -0500 @@ -172,19 +172,36 @@ for a in 'phaseroots dirty opener _phaserevs'.split(): setattr(self, a, getattr(phcache, a)) + def _getphaserevsnative(self, repo): + repo = repo.unfiltered() + nativeroots = [] + for phase in trackedphases: + nativeroots.append(map(repo.changelog.rev, self.phaseroots[phase])) + return repo.changelog.computephases(nativeroots) + + def _computephaserevspure(self, repo): + repo = repo.unfiltered() + revs = [public] * len(repo.changelog) + self._phaserevs = revs + self._populatephaseroots(repo) + 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 + def getphaserevs(self, repo): if self._phaserevs is None: - repo = repo.unfiltered() - revs = [public] * len(repo.changelog) - self._phaserevs = revs - self._populatephaseroots(repo) - 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 + try: + if repo.ui.configbool('experimental', + 'nativephaseskillswitch'): + self._computephaserevspure(repo) + else: + self._phaserevs = self._getphaserevsnative(repo) + except AttributeError: + self._computephaserevspure(repo) return self._phaserevs def invalidate(self): diff -r 612ed41ae359 -r e89f909edffa mercurial/posix.py --- a/mercurial/posix.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/posix.py Thu Apr 16 20:57:51 2015 -0500 @@ -16,6 +16,7 @@ oslink = os.link unlink = os.unlink rename = os.rename +removedirs = os.removedirs expandglobs = False umask = os.umask(0) @@ -200,6 +201,11 @@ def normcase(path): return path.lower() +# what normcase does to ASCII strings +normcasespec = encoding.normcasespecs.lower +# fallback normcase function for non-ASCII strings +normcasefallback = normcase + if sys.platform == 'darwin': def normcase(path): @@ -223,7 +229,11 @@ try: return encoding.asciilower(path) # exception for non-ASCII except UnicodeDecodeError: - pass + return normcasefallback(path) + + normcasespec = encoding.normcasespecs.lower + + def normcasefallback(path): try: u = path.decode('utf-8') except UnicodeDecodeError: @@ -302,6 +312,9 @@ return encoding.upper(path) + normcasespec = encoding.normcasespecs.other + normcasefallback = normcase + # Cygwin translates native ACLs to POSIX permissions, # but these translations are not supported by native # tools, so the exec bit tends to be set erroneously. diff -r 612ed41ae359 -r e89f909edffa mercurial/pure/parsers.py --- a/mercurial/pure/parsers.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/pure/parsers.py Thu Apr 16 20:57:51 2015 -0500 @@ -5,15 +5,13 @@ # 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.node import bin, nullid -from mercurial import util +from mercurial.node import nullid import struct, zlib, cStringIO _pack = struct.pack _unpack = struct.unpack _compress = zlib.compress _decompress = zlib.decompress -_sha = util.sha1 # Some code below makes tuples directly because it's more convenient. However, # code outside this module should always use dirstatetuple. @@ -21,15 +19,6 @@ # x is a tuple return x -def parse_manifest(mfdict, fdict, lines): - for l in lines.splitlines(): - f, n = l.split('\0') - if len(n) > 40: - fdict[f] = n[40:] - mfdict[f] = bin(n[:40]) - else: - mfdict[f] = bin(n) - def parse_index2(data, inline): def gettype(q): return int(q & 0xFFFF) diff -r 612ed41ae359 -r e89f909edffa mercurial/pvec.py --- a/mercurial/pvec.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/pvec.py Thu Apr 16 20:57:51 2015 -0500 @@ -142,7 +142,7 @@ def ctxpvec(ctx): '''construct a pvec for ctx while filling in the cache''' - r = ctx._repo + r = ctx.repo() if not util.safehasattr(r, "_pveccache"): r._pveccache = {} pvc = r._pveccache diff -r 612ed41ae359 -r e89f909edffa mercurial/repair.py --- a/mercurial/repair.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/repair.py Thu Apr 16 20:57:51 2015 -0500 @@ -42,7 +42,7 @@ name = "%s/%s-%s-%s.hg" % (backupdir, short(node), totalhash[:8], suffix) if usebundle2: - bundletype = "HG2Y" + bundletype = "HG20" elif compress: bundletype = "HG10BZ" else: @@ -137,6 +137,7 @@ # create a changegroup for all the branches we need to keep backupfile = None vfs = repo.vfs + node = nodelist[-1] if backup: backupfile = _bundle(repo, stripbases, cl.heads(), node, topic) repo.ui.status(_("saved backup bundle to %s\n") % @@ -181,6 +182,8 @@ repo.ui.pushbuffer() if isinstance(gen, bundle2.unbundle20): tr = repo.transaction('strip') + tr.hookargs = {'source': 'strip', + 'url': 'bundle:' + vfs.join(chgrpfile)} try: bundle2.processbundle(repo, gen, lambda: tr) tr.close() diff -r 612ed41ae359 -r e89f909edffa mercurial/repoview.py --- a/mercurial/repoview.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/repoview.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,6 +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. +import heapq import copy import error import phases @@ -13,6 +14,7 @@ import obsolete import struct import tags as tagsmod +from node import nullrev def hideablerevs(repo): """Revisions candidates to be hidden @@ -20,23 +22,46 @@ This is a standalone function to help extensions to wrap it.""" return obsolete.getrevs(repo, 'obsolete') -def _getstaticblockers(repo): - """Cacheable revisions blocking hidden changesets from being filtered. +def _getstatichidden(repo): + """Revision to be hidden (disregarding dynamic blocker) - Additional non-cached hidden blockers are computed in _getdynamicblockers. - This is a standalone function to help extensions to wrap it.""" + To keep a consistent graph, we cannot hide any revisions with + non-hidden descendants. This function computes the set of + revisions that could be hidden while keeping the graph consistent. + + A second pass will be done to apply "dynamic blocker" like bookmarks or + working directory parents. + + """ assert not repo.changelog.filteredrevs - hideable = hideablerevs(repo) - blockers = set() - if hideable: - # We use cl to avoid recursive lookup from repo[xxx] - cl = repo.changelog - firsthideable = min(hideable) - revs = cl.revs(start=firsthideable) - tofilter = repo.revs( - '(%ld) and children(%ld)', list(revs), list(hideable)) - blockers.update([r for r in tofilter if r not in hideable]) - return blockers + hidden = set(hideablerevs(repo)) + if hidden: + getphase = repo._phasecache.phase + getparentrevs = repo.changelog.parentrevs + # Skip heads which are public (guaranteed to not be hidden) + heap = [-r for r in repo.changelog.headrevs() if getphase(repo, r)] + heapq.heapify(heap) + heappop = heapq.heappop + heappush = heapq.heappush + seen = set() # no need to init it with heads, they have no children + while heap: + rev = -heappop(heap) + # All children have been processed so at that point, if no children + # removed 'rev' from the 'hidden' set, 'rev' is going to be hidden. + blocker = rev not in hidden + for parent in getparentrevs(rev): + if parent == nullrev: + continue + if blocker: + # If visible, ensure parent will be visible too + hidden.discard(parent) + # - Avoid adding the same revision twice + # - Skip nodes which are public (guaranteed to not be hidden) + pre = len(seen) + seen.add(parent) + if pre < len(seen) and getphase(repo, rev): + heappush(heap, -parent) + return hidden def _getdynamicblockers(repo): """Non-cacheable revisions blocking hidden changesets from being filtered. @@ -137,8 +162,7 @@ cl = repo.changelog hidden = tryreadcache(repo, hideable) if hidden is None: - blocked = cl.ancestors(_getstaticblockers(repo), inclusive=True) - hidden = frozenset(r for r in hideable if r not in blocked) + hidden = frozenset(_getstatichidden(repo)) trywritehiddencache(repo, hideable, hidden) # check if we have wd parents, bookmarks or tags pointing to hidden diff -r 612ed41ae359 -r e89f909edffa mercurial/revlog.py --- a/mercurial/revlog.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/revlog.py Thu Apr 16 20:57:51 2015 -0500 @@ -277,6 +277,8 @@ def tip(self): return self.node(len(self.index) - 2) + def __contains__(self, rev): + return 0 <= rev < len(self) def __len__(self): return len(self.index) - 1 def __iter__(self): @@ -722,6 +724,9 @@ except AttributeError: return self._headrevs() + def computephases(self, roots): + return self.index.computephases(roots) + def _headrevs(self): count = len(self) if not count: @@ -1124,7 +1129,12 @@ % self.indexfile) trindex = trinfo[2] - dataoff = self.start(trindex) + if trindex is not None: + dataoff = self.start(trindex) + else: + # revlog was stripped at start of transaction, use all leftover data + trindex = len(self) - 1 + dataoff = self.end(-2) tr.add(self.datafile, dataoff) @@ -1231,8 +1241,18 @@ if dfh: dfh.flush() ifh.flush() - basetext = self.revision(self.node(cachedelta[0])) - btext[0] = mdiff.patch(basetext, cachedelta[1]) + baserev = cachedelta[0] + delta = cachedelta[1] + # special case deltas which replace entire base; no need to decode + # base revision. this neatly avoids censored bases, which throw when + # they're decoded. + hlen = struct.calcsize(">lll") + if delta[:hlen] == mdiff.replacediffheader(self.rawsize(baserev), + len(delta) - hlen): + btext[0] = delta[hlen:] + else: + basetext = self.revision(self.node(baserev)) + btext[0] = mdiff.patch(basetext, delta) try: self.checkhash(btext[0], p1, p2, node) if flags & REVIDX_ISCENSORED: @@ -1249,8 +1269,14 @@ delta = cachedelta[1] else: t = buildtext() - ptext = self.revision(self.node(rev)) - delta = mdiff.textdiff(ptext, t) + if self.iscensored(rev): + # deltas based on a censored revision must replace the + # full content in one patch, so delta works everywhere + header = mdiff.replacediffheader(self.rawsize(rev), len(t)) + delta = header + t + else: + ptext = self.revision(self.node(rev)) + delta = mdiff.textdiff(ptext, t) data = self.compress(delta) l = len(data[1]) + len(data[0]) if basecache[0] == rev: @@ -1368,7 +1394,10 @@ transaction.add(self.indexfile, isize, r) transaction.add(self.datafile, end) dfh = self.opener(self.datafile, "a") - + def flush(): + if dfh: + dfh.flush() + ifh.flush() try: # loop through our set of deltas chain = None @@ -1401,9 +1430,24 @@ _('unknown delta base')) baserev = self.rev(deltabase) + + if baserev != nullrev and self.iscensored(baserev): + # if base is censored, delta must be full replacement in a + # single patch operation + hlen = struct.calcsize(">lll") + oldlen = self.rawsize(baserev) + newlen = len(delta) - hlen + if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen): + raise error.CensoredBaseError(self.indexfile, + self.node(baserev)) + + flags = REVIDX_DEFAULT_FLAGS + if self._peek_iscensored(baserev, delta, flush): + flags |= REVIDX_ISCENSORED + chain = self._addrevision(node, None, transaction, link, - p1, p2, REVIDX_DEFAULT_FLAGS, - (baserev, delta), ifh, dfh) + p1, p2, flags, (baserev, delta), + ifh, dfh) if not dfh and not self._inline: # addrevision switched from inline to conventional # reopen the index @@ -1417,6 +1461,14 @@ return content + def iscensored(self, rev): + """Check if a file revision is censored.""" + return False + + def _peek_iscensored(self, baserev, delta, flush): + """Quickly check if a delta produces a censored revision.""" + return False + def getstrippoint(self, minlink): """find the minimum rev that must be stripped to strip the linkrev diff -r 612ed41ae359 -r e89f909edffa mercurial/revset.py --- a/mercurial/revset.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/revset.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. import re -import parser, util, error, discovery, hbisect, phases +import parser, util, error, hbisect, phases import node import heapq import match as matchmod @@ -18,7 +18,10 @@ def _revancestors(repo, revs, followfirst): """Like revlog.ancestors(), but supports followfirst.""" - cut = followfirst and 1 or None + if followfirst: + cut = 1 + else: + cut = None cl = repo.changelog def iterate(): @@ -49,7 +52,10 @@ def _revdescendants(repo, revs, followfirst): """Like revlog.descendants() but supports followfirst.""" - cut = followfirst and 1 or None + if followfirst: + cut = 1 + else: + cut = None def iterate(): cl = repo.changelog @@ -235,7 +241,8 @@ yield ('symbol', sym, s) pos -= 1 else: - raise error.ParseError(_("syntax error"), pos) + raise error.ParseError(_("syntax error in revset '%s'") % + program, pos) pos += 1 yield ('end', None, pos) @@ -323,8 +330,6 @@ def stringset(repo, subset, x): x = repo[x].rev() - if x == -1 and len(subset) == len(repo): - return baseset([-1]) if x in subset: return baseset([x]) return baseset() @@ -349,7 +354,7 @@ return r & subset def dagrange(repo, subset, x, y): - r = spanset(repo) + r = fullreposet(repo) xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y)) return xs & subset @@ -370,7 +375,7 @@ def func(repo, subset, a, b): if a[0] == 'symbol' and a[1] in symbols: return symbols[a[1]](repo, subset, b) - raise error.ParseError(_("not a function: %s") % a[1]) + raise error.UnknownIdentifier(a[1], symbols.keys()) # functions @@ -396,7 +401,7 @@ """ # i18n: "ancestor" is a keyword l = getlist(x) - rl = spanset(repo) + rl = fullreposet(repo) anc = None # (getset(repo, rl, i) for i in l) generates a list of lists @@ -412,7 +417,7 @@ return baseset() def _ancestors(repo, subset, x, followfirst=False): - heads = getset(repo, spanset(repo), x) + heads = getset(repo, fullreposet(repo), x) if not heads: return baseset() s = _revancestors(repo, heads, followfirst) @@ -524,10 +529,7 @@ a regular expression. To match a branch that actually starts with `re:`, use the prefix `literal:`. """ - import branchmap - urepo = repo.unfiltered() - ucl = urepo.changelog - getbi = branchmap.revbranchcache(urepo, readonly=True).branchinfo + getbi = repo.revbranchcache().branchinfo try: b = getstring(x, '') @@ -540,16 +542,16 @@ # note: falls through to the revspec case if no branch with # this name exists if pattern in repo.branchmap(): - return subset.filter(lambda r: matcher(getbi(ucl, r)[0])) + return subset.filter(lambda r: matcher(getbi(r)[0])) else: - return subset.filter(lambda r: matcher(getbi(ucl, r)[0])) - - s = getset(repo, spanset(repo), x) + return subset.filter(lambda r: matcher(getbi(r)[0])) + + s = getset(repo, fullreposet(repo), x) b = set() for r in s: - b.add(getbi(ucl, r)[0]) + b.add(getbi(r)[0]) c = s.__contains__ - return subset.filter(lambda r: c(r) or getbi(ucl, r)[0] in b) + return subset.filter(lambda r: c(r) or getbi(r)[0] in b) def bumped(repo, subset, x): """``bumped()`` @@ -708,7 +710,7 @@ return subset.filter(matches) def _descendants(repo, subset, x, followfirst=False): - roots = getset(repo, spanset(repo), x) + roots = getset(repo, fullreposet(repo), x) if not roots: return baseset() s = _revdescendants(repo, roots, followfirst) @@ -744,9 +746,9 @@ is the same as passing all(). """ if x is not None: - sources = getset(repo, spanset(repo), x) + sources = getset(repo, fullreposet(repo), x) else: - sources = getall(repo, spanset(repo), x) + sources = fullreposet(repo) dests = set() @@ -976,7 +978,7 @@ def follow(repo, subset, x): """``follow([file])`` - An alias for ``::.`` (ancestors of the working copy's first parent). + An alias for ``::.`` (ancestors of the working directory's first parent). If a filename is specified, the history of the given file is followed, including copies. """ @@ -994,7 +996,7 @@ """ # i18n: "all" is a keyword getargs(x, 0, 0, _("all takes no arguments")) - return subset + return subset & spanset(repo) # drop "null" if any def grep(repo, subset, x): """``grep(regex)`` @@ -1145,7 +1147,7 @@ # i18n: "limit" is a keyword raise error.ParseError(_("limit expects a number")) ss = subset - os = getset(repo, spanset(repo), l[0]) + os = getset(repo, fullreposet(repo), l[0]) result = [] it = iter(os) for x in xrange(lim): @@ -1172,7 +1174,7 @@ # i18n: "last" is a keyword raise error.ParseError(_("last expects a number")) ss = subset - os = getset(repo, spanset(repo), l[0]) + os = getset(repo, fullreposet(repo), l[0]) os.reverse() result = [] it = iter(os) @@ -1189,7 +1191,7 @@ """``max(set)`` Changeset with highest revision number in set. """ - os = getset(repo, spanset(repo), x) + os = getset(repo, fullreposet(repo), x) if os: m = os.max() if m in subset: @@ -1226,7 +1228,7 @@ """``min(set)`` Changeset with lowest revision number in set. """ - os = getset(repo, spanset(repo), x) + os = getset(repo, fullreposet(repo), x) if os: m = os.min() if m in subset: @@ -1322,7 +1324,7 @@ cl = repo.changelog # i18n: "only" is a keyword args = getargs(x, 1, 2, _('only takes one or two arguments')) - include = getset(repo, spanset(repo), args[0]) + include = getset(repo, fullreposet(repo), args[0]) if len(args) == 1: if not include: return baseset() @@ -1331,7 +1333,7 @@ exclude = [rev for rev in cl.headrevs() if not rev in descendants and not rev in include] else: - exclude = getset(repo, spanset(repo), args[1]) + exclude = getset(repo, fullreposet(repo), args[1]) results = set(cl.findmissingrevs(common=exclude, heads=include)) return subset & results @@ -1345,9 +1347,9 @@ for the first operation is selected. """ if x is not None: - dests = getset(repo, spanset(repo), x) + dests = getset(repo, fullreposet(repo), x) else: - dests = getall(repo, spanset(repo), x) + dests = fullreposet(repo) def _firstsrc(rev): src = _getrevsource(repo, rev) @@ -1370,7 +1372,9 @@ Changesets not found in the specified destination repository, or the default push location. """ - import hg # avoid start-up nasties + # Avoid cycles. + import discovery + import hg # i18n: "outgoing" is a keyword l = getargs(x, 0, 1, _("outgoing takes one or no arguments")) # i18n: "outgoing" is a keyword @@ -1400,7 +1404,7 @@ ps = set() cl = repo.changelog - for r in getset(repo, spanset(repo), x): + for r in getset(repo, fullreposet(repo), x): ps.add(cl.parentrevs(r)[0]) ps -= set([node.nullrev]) return subset & ps @@ -1421,7 +1425,7 @@ ps = set() cl = repo.changelog - for r in getset(repo, spanset(repo), x): + for r in getset(repo, fullreposet(repo), x): ps.add(cl.parentrevs(r)[1]) ps -= set([node.nullrev]) return subset & ps @@ -1435,7 +1439,7 @@ else: ps = set() cl = repo.changelog - for r in getset(repo, spanset(repo), x): + for r in getset(repo, fullreposet(repo), x): ps.update(cl.parentrevs(r)) ps -= set([node.nullrev]) return subset & ps @@ -1548,7 +1552,7 @@ except (TypeError, ValueError): # i18n: "rev" is a keyword raise error.ParseError(_("rev expects a number")) - if l not in fullreposet(repo) and l != node.nullrev: + if l not in repo.changelog and l != node.nullrev: return baseset() return subset & baseset([l]) @@ -1676,7 +1680,7 @@ """``roots(set)`` Changesets in set with no parent changeset in set. """ - s = getset(repo, spanset(repo), x) + s = getset(repo, fullreposet(repo), x) subset = baseset([r for r in s if r in subset]) cs = _children(repo, subset, s) return subset - cs @@ -1754,6 +1758,49 @@ l.sort() return baseset([e[-1] for e in l]) +def subrepo(repo, subset, x): + """``subrepo([pattern])`` + Changesets that add, modify or remove the given subrepo. If no subrepo + pattern is named, any subrepo changes are returned. + """ + # i18n: "subrepo" is a keyword + args = getargs(x, 0, 1, _('subrepo takes at most one argument')) + if len(args) != 0: + pat = getstring(args[0], _("subrepo requires a pattern")) + + m = matchmod.exact(repo.root, repo.root, ['.hgsubstate']) + + def submatches(names): + k, p, m = _stringmatcher(pat) + for name in names: + if m(name): + yield name + + def matches(x): + c = repo[x] + s = repo.status(c.p1().node(), c.node(), match=m) + + if len(args) == 0: + return s.added or s.modified or s.removed + + if s.added: + return util.any(submatches(c.substate.keys())) + + if s.modified: + subs = set(c.p1().substate.keys()) + subs.update(c.substate.keys()) + + for path in submatches(subs): + if c.p1().substate.get(path) != c.substate.get(path): + return True + + if s.removed: + return util.any(submatches(c.p1().substate.keys())) + + return False + + return subset.filter(matches) + def _stringmatcher(pattern): """ accepts a string, possibly starting with 're:' or 'literal:' prefix. @@ -1851,6 +1898,14 @@ """ return author(repo, subset, x) +# experimental +def wdir(repo, subset, x): + # i18n: "wdir" is a keyword + getargs(x, 0, 0, _("wdir takes no arguments")) + if None in subset: + return baseset([None]) + return baseset() + # for internal use def _list(repo, subset, x): s = getstring(x, "internal error") @@ -1941,11 +1996,13 @@ "roots": roots, "sort": sort, "secret": secret, + "subrepo": subrepo, "matching": matching, "tag": tag, "tagged": tagged, "user": user, "unstable": unstable, + "wdir": wdir, "_list": _list, "_intlist": _intlist, "_hexlist": _hexlist, @@ -2018,6 +2075,7 @@ "tagged", "user", "unstable", + "wdir", "_list", "_intlist", "_hexlist", @@ -2153,7 +2211,7 @@ if isinstance(tree, tuple): arg = _getaliasarg(tree) if arg is not None and (not known or arg not in known): - raise error.ParseError(_("not a function: %s") % '_aliasarg') + raise error.UnknownIdentifier('_aliasarg', []) for t in tree: _checkaliasarg(t, known) @@ -2243,6 +2301,71 @@ except error.ParseError, inst: return (decl, None, None, parseerrordetail(inst)) +def _parsealiasdefn(defn, args): + """Parse alias definition ``defn`` + + This function also replaces alias argument references in the + specified definition by ``_aliasarg(ARGNAME)``. + + ``args`` is a list of alias argument names, or None if the alias + is declared as a symbol. + + This returns "tree" as parsing result. + + >>> args = ['$1', '$2', 'foo'] + >>> print prettyformat(_parsealiasdefn('$1 or foo', args)) + (or + (func + ('symbol', '_aliasarg') + ('string', '$1')) + (func + ('symbol', '_aliasarg') + ('string', 'foo'))) + >>> try: + ... _parsealiasdefn('$1 or $bar', args) + ... except error.ParseError, inst: + ... print parseerrordetail(inst) + at 6: '$' not for alias arguments + >>> args = ['$1', '$10', 'foo'] + >>> print prettyformat(_parsealiasdefn('$10 or foobar', args)) + (or + (func + ('symbol', '_aliasarg') + ('string', '$10')) + ('symbol', 'foobar')) + >>> print prettyformat(_parsealiasdefn('"$1" or "foo"', args)) + (or + ('string', '$1') + ('string', 'foo')) + """ + def tokenizedefn(program, lookup=None): + if args: + argset = set(args) + else: + argset = set() + + for t, value, pos in _tokenizealias(program, lookup=lookup): + if t == 'symbol': + if value in argset: + # emulate tokenization of "_aliasarg('ARGNAME')": + # "_aliasarg()" is an unknown symbol only used separate + # alias argument placeholders from regular strings. + yield ('symbol', '_aliasarg', pos) + yield ('(', None, pos) + yield ('string', value, pos) + yield (')', None, pos) + continue + elif value.startswith('$'): + raise error.ParseError(_("'$' not for alias arguments"), + pos) + yield (t, value, pos) + + p = parser.parser(tokenizedefn, elements) + tree, pos = p.parse(defn) + if pos != len(defn): + raise error.ParseError(_('invalid token'), pos) + return tree + class revsetalias(object): # whether own `error` information is already shown or not. # this avoids showing same warning multiple times at each `findaliases`. @@ -2260,16 +2383,8 @@ ' "%s": %s') % (self.name, self.error) return - if self.args: - for arg in self.args: - # _aliasarg() is an unknown symbol only used separate - # alias argument placeholders from regular strings. - value = value.replace(arg, '_aliasarg(%r)' % (arg,)) - try: - self.replacement, pos = parse(value) - if pos != len(value): - raise error.ParseError(_('invalid token'), pos) + self.replacement = _parsealiasdefn(value, self.args) # Check for placeholder injection _checkaliasarg(self.replacement, self.args) except error.ParseError, inst: @@ -2379,6 +2494,10 @@ p = parser.parser(tokenize, elements) return p.parse(spec, lookup=lookup) +def posttreebuilthook(tree, repo): + # hook for extensions to execute code on the optimized tree + pass + def match(ui, spec, repo=None): if not spec: raise error.ParseError(_("empty query")) @@ -2392,7 +2511,10 @@ tree = findaliases(ui, tree, showwarning=ui.warn) tree = foldconcat(tree) weight, tree = optimize(tree, True) - def mfunc(repo, subset): + posttreebuilthook(tree, repo) + def mfunc(repo, subset=None): + if subset is None: + subset = fullreposet(repo) if util.safehasattr(subset, 'isascending'): result = getset(repo, subset, tree) else: @@ -2602,6 +2724,8 @@ """Returns a new object with the intersection of the two collections. This is part of the mandatory API for smartset.""" + if isinstance(other, fullreposet): + return self return self.filter(other.__contains__, cache=False) def __add__(self, other): @@ -2720,6 +2844,10 @@ return self._asclist[0] return None + def __repr__(self): + d = {None: '', False: '-', True: '+'}[self._ascending] + return '<%s%s %r>' % (type(self).__name__, d, self._list) + class filteredset(abstractsmartset): """Duck type for baseset class which iterates lazily over the revisions in the subset and contains a function which tests for membership in the @@ -2804,6 +2932,9 @@ return x return None + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self._subset) + class addset(abstractsmartset): """Represent the addition of two sets @@ -2977,6 +3108,10 @@ self.reverse() return val + def __repr__(self): + d = {None: '', False: '-', True: '+'}[self._ascending] + return '<%s%s %r, %r>' % (type(self).__name__, d, self._r1, self._r2) + class generatorset(abstractsmartset): """Wrap a generator for lazy iteration @@ -3146,18 +3281,11 @@ return it().next() return None -def spanset(repo, start=None, end=None): - """factory function to dispatch between fullreposet and actual spanset - - Feel free to update all spanset call sites and kill this function at some - point. - """ - if start is None and end is None: - return fullreposet(repo) - return _spanset(repo, start, end) - - -class _spanset(abstractsmartset): + def __repr__(self): + d = {False: '-', True: '+'}[self._ascending] + return '<%s%s>' % (type(self).__name__, d) + +class spanset(abstractsmartset): """Duck type for baseset class which represents a range of revisions and can work lazily and without having all the range in memory @@ -3261,15 +3389,26 @@ return x return None -class fullreposet(_spanset): + def __repr__(self): + d = {False: '-', True: '+'}[self._ascending] + return '<%s%s %d:%d>' % (type(self).__name__, d, + self._start, self._end - 1) + +class fullreposet(spanset): """a set containing all revisions in the repo - This class exists to host special optimization. + This class exists to host special optimization and magic to handle virtual + revisions such as "null". """ def __init__(self, repo): super(fullreposet, self).__init__(repo) + def __contains__(self, rev): + # assumes the given rev is valid + hidden = self._hiddenrevs + return not (hidden and rev in hidden) + def __and__(self, other): """As self contains the whole repo, all of the other set should also be in self. Therefore `self & other = other`. @@ -3288,5 +3427,19 @@ other.sort(reverse=self.isdescending()) return other +def prettyformatset(revs): + lines = [] + rs = repr(revs) + p = 0 + while p < len(rs): + q = rs.find('<', p + 1) + if q < 0: + q = len(rs) + l = rs.count('<', 0, p) - rs.count('>', 0, p) + assert l >= 0 + lines.append((l, rs[p:q].rstrip())) + p = q + return '\n'.join(' ' * l + s for l, s in lines) + # tell hggettext to extract docstrings from these functions: i18nfunctions = symbols.values() diff -r 612ed41ae359 -r e89f909edffa mercurial/scmutil.py --- a/mercurial/scmutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/scmutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,10 +7,10 @@ from i18n import _ from mercurial.node import nullrev -import util, error, osutil, revset, similar, encoding, phases, parsers +import util, error, osutil, revset, similar, encoding, phases import pathutil import match as matchmod -import os, errno, re, glob, tempfile +import os, errno, re, glob, tempfile, shutil, stat, inspect if os.name == 'nt': import scmwindows as scmplatform @@ -172,6 +172,40 @@ self._loweredfiles.add(fl) self._newfiles.add(f) +def develwarn(tui, msg): + """issue a developer warning message""" + msg = 'devel-warn: ' + msg + if tui.tracebackflag: + util.debugstacktrace(msg, 2) + else: + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe, 2) + tui.write_err('%s at: %s:%s (%s)\n' % ((msg,) + calframe[2][1:4])) + +def filteredhash(repo, maxrev): + """build hash of filtered revisions in the current repoview. + + Multiple caches perform up-to-date validation by checking that the + tiprev and tipnode stored in the cache file match the current repository. + However, this is not sufficient for validating repoviews because the set + of revisions in the view may change without the repository tiprev and + tipnode changing. + + This function hashes all the revs filtered from the view and returns + that SHA-1 digest. + """ + cl = repo.changelog + if not cl.filteredrevs: + return None + key = None + revs = sorted(r for r in cl.filteredrevs if r <= maxrev) + if revs: + s = util.sha1() + for rev in revs: + s.update('%s;' % rev) + key = s.digest() + return key + class abstractvfs(object): """Abstract base class; cannot be instantiated""" @@ -316,6 +350,31 @@ def readlink(self, path): return os.readlink(self.join(path)) + def removedirs(self, path=None): + """Remove a leaf directory and all empty intermediate ones + """ + return util.removedirs(self.join(path)) + + def rmtree(self, path=None, ignore_errors=False, forcibly=False): + """Remove a directory tree recursively + + If ``forcibly``, this tries to remove READ-ONLY files, too. + """ + if forcibly: + def onerror(function, path, excinfo): + if function is not os.remove: + raise + # read-only files cannot be unlinked under Windows + s = os.stat(path) + if (s.st_mode & stat.S_IWRITE) != 0: + raise + os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE) + os.remove(path) + else: + onerror = None + return shutil.rmtree(self.join(path), + ignore_errors=ignore_errors, onerror=onerror) + def setflags(self, path, l, x): return util.setflags(self.join(path), l, x) @@ -331,6 +390,22 @@ def utime(self, path=None, t=None): return os.utime(self.join(path), t) + def walk(self, path=None, onerror=None): + """Yield (dirpath, dirs, files) tuple for each directories under path + + ``dirpath`` is relative one from the root of this vfs. This + uses ``os.sep`` as path separator, even you specify POSIX + style ``path``. + + "The root of this vfs" is represented as empty ``dirpath``. + """ + root = os.path.normpath(self.join(None)) + # when dirpath == root, dirpath[prefixlen:] becomes empty + # because len(dirpath) < prefixlen. + prefixlen = len(pathutil.normasprefix(root)) + for dirpath, dirs, files in os.walk(self.join(path), onerror=onerror): + yield (dirpath[prefixlen:], dirs, files) + class vfs(abstractvfs): '''Operate files relative to a base directory @@ -445,9 +520,9 @@ else: self.write(dst, src) - def join(self, path): + def join(self, path, *insidef): if path: - return os.path.join(self.base, path) + return os.path.join(self.base, path, *insidef) else: return self.base @@ -475,9 +550,9 @@ def __call__(self, path, *args, **kwargs): return self.vfs(self._filter(path), *args, **kwargs) - def join(self, path): + def join(self, path, *insidef): if path: - return self.vfs.join(self._filter(path)) + return self.vfs.join(self._filter(self.vfs.reljoin(path, *insidef))) else: return self.vfs.join(path) @@ -582,6 +657,13 @@ _rcpath = osrcpath() return _rcpath +def intrev(repo, rev): + """Return integer for a given revision that can be used in comparison or + arithmetic operation""" + if rev is None: + return len(repo) + return rev + def revsingle(repo, revspec, default='.'): if not revspec and revspec != 0: return repo[default] @@ -628,12 +710,22 @@ return repo[val].rev() seen, l = set(), revset.baseset([]) + + revsetaliases = [alias for (alias, _) in + repo.ui.configitems("revsetalias")] + for spec in revs: if l and not seen: seen = set(l) # attempt to parse old-style ranges first to deal with # things like old-tag which contain query metacharacters try: + # ... except for revset aliases without arguments. These + # should be parsed as soon as possible, because they might + # clash with a hash prefix. + if spec in revsetaliases: + raise error.RepoLookupError + if isinstance(spec, int): seen.add(spec) l = l + revset.baseset([spec]) @@ -641,6 +733,9 @@ if _revrangesep in spec: start, end = spec.split(_revrangesep, 1) + if start in revsetaliases or end in revsetaliases: + raise error.RepoLookupError + start = revfix(repo, start, 0) end = revfix(repo, end, len(repo) - 1) if end == nullrev and start < 0: @@ -672,11 +767,11 @@ # fall through to new-style queries if old-style fails m = revset.match(repo.ui, spec, repo) if seen or l: - dl = [r for r in m(repo, revset.spanset(repo)) if r not in seen] + dl = [r for r in m(repo) if r not in seen] l = l + revset.baseset(dl) seen.update(dl) else: - l = m(repo, revset.spanset(repo)) + l = m(repo) return l @@ -710,8 +805,10 @@ m = ctx.match(pats, opts.get('include'), opts.get('exclude'), default) def badfn(f, msg): - ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg)) + ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg)) m.bad = badfn + if m.always(): + pats = [] return m, pats def match(ctx, pats=[], opts={}, globbed=False, default='relpath'): @@ -1061,48 +1158,3 @@ del obj.__dict__[self.name] except KeyError: raise AttributeError(self.name) - -class dirs(object): - '''a multiset of directory names from a dirstate or manifest''' - - def __init__(self, map, skip=None): - self._dirs = {} - addpath = self.addpath - if util.safehasattr(map, 'iteritems') and skip is not None: - for f, s in map.iteritems(): - if s[0] != skip: - addpath(f) - else: - for f in map: - addpath(f) - - def addpath(self, path): - dirs = self._dirs - for base in finddirs(path): - if base in dirs: - dirs[base] += 1 - return - dirs[base] = 1 - - def delpath(self, path): - dirs = self._dirs - for base in finddirs(path): - if dirs[base] > 1: - dirs[base] -= 1 - return - del dirs[base] - - def __iter__(self): - return self._dirs.iterkeys() - - def __contains__(self, d): - return d in self._dirs - -if util.safehasattr(parsers, 'dirs'): - dirs = parsers.dirs - -def finddirs(path): - pos = path.rfind('/') - while pos != -1: - yield path[:pos] - pos = path.rfind('/', 0, pos) diff -r 612ed41ae359 -r e89f909edffa mercurial/sslutil.py --- a/mercurial/sslutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/sslutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -10,12 +10,16 @@ from mercurial import util from mercurial.i18n import _ + +_canloaddefaultcerts = False try: # avoid using deprecated/broken FakeSocket in python 2.6 import ssl CERT_REQUIRED = ssl.CERT_REQUIRED try: ssl_context = ssl.SSLContext + _canloaddefaultcerts = util.safehasattr(ssl_context, + 'load_default_certs') def ssl_wrap_socket(sock, keyfile, certfile, cert_reqs=ssl.CERT_NONE, ca_certs=None, serverhostname=None): @@ -35,6 +39,8 @@ sslcontext.verify_mode = cert_reqs if ca_certs is not None: sslcontext.load_verify_locations(cafile=ca_certs) + elif _canloaddefaultcerts: + sslcontext.load_default_certs() sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname) @@ -123,29 +129,40 @@ for using system certificate store CAs in addition to the provided cacerts file """ - if sys.platform != 'darwin' or util.mainfrozen(): + if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable: return False - exe = (sys.executable or '').lower() + exe = os.path.realpath(sys.executable).lower() return (exe.startswith('/usr/bin/python') or exe.startswith('/system/library/frameworks/python.framework/')) +def _defaultcacerts(): + """return path to CA certificates; None for system's store; ! to disable""" + if _plainapplepython(): + dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem') + if os.path.exists(dummycert): + return dummycert + if _canloaddefaultcerts: + return None + return '!' + def sslkwargs(ui, host): kws = {} hostfingerprint = ui.config('hostfingerprints', host) if hostfingerprint: return kws cacerts = ui.config('web', 'cacerts') - if cacerts: + if cacerts == '!': + pass + elif cacerts: cacerts = util.expandpath(cacerts) if not os.path.exists(cacerts): raise util.Abort(_('could not find web.cacerts: %s') % cacerts) - elif cacerts is None and _plainapplepython(): - dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem') - if os.path.exists(dummycert): - ui.debug('using %s to enable OS X system CA\n' % dummycert) - ui.setconfig('web', 'cacerts', dummycert, 'dummy') - cacerts = dummycert - if cacerts: + else: + cacerts = _defaultcacerts() + if cacerts and cacerts != '!': + ui.debug('using %s to enable OS X system CA\n' % cacerts) + ui.setconfig('web', 'cacerts', cacerts, 'defaultcacerts') + if cacerts != '!': kws.update({'ca_certs': cacerts, 'cert_reqs': CERT_REQUIRED, }) @@ -194,7 +211,7 @@ hint=_('check hostfingerprint configuration')) self.ui.debug('%s certificate matched fingerprint %s\n' % (host, nicefingerprint)) - elif cacerts: + elif cacerts != '!': msg = _verifycert(peercert2, host) if msg: raise util.Abort(_('%s certificate error: %s') % (host, msg), diff -r 612ed41ae359 -r e89f909edffa mercurial/statichttprepo.py --- a/mercurial/statichttprepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/statichttprepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -141,8 +141,10 @@ self._tags = None self.nodetagscache = None self._branchcaches = {} + self._revbranchcache = None self.encodepats = None self.decodepats = None + self._transref = None def _restrictcapabilities(self, caps): caps = super(statichttprepository, self)._restrictcapabilities(caps) diff -r 612ed41ae359 -r e89f909edffa mercurial/subrepo.py --- a/mercurial/subrepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/subrepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. import copy -import errno, os, re, shutil, posixpath, sys +import errno, os, re, posixpath, sys import xml.dom.minidom import stat, subprocess, tarfile from i18n import _ @@ -70,11 +70,14 @@ if err.errno != errno.ENOENT: raise # handle missing subrepo spec files as removed - ui.warn(_("warning: subrepo spec file %s not found\n") % f) + ui.warn(_("warning: subrepo spec file \'%s\' not found\n") % + util.pathto(ctx.repo().root, ctx.repo().getcwd(), f)) return p.parse(f, data, sections, remap, read) else: - raise util.Abort(_("subrepo spec file %s not found") % f) + repo = ctx.repo() + raise util.Abort(_("subrepo spec file \'%s\' not found") % + util.pathto(repo.root, repo.getcwd(), f)) if '.hgsub' in ctx: read('.hgsub') @@ -92,9 +95,11 @@ try: revision, path = l.split(" ", 1) except ValueError: + repo = ctx.repo() raise util.Abort(_("invalid subrepository revision " - "specifier in .hgsubstate line %d") - % (i + 1)) + "specifier in \'%s\' line %d") + % (util.pathto(repo.root, repo.getcwd(), + '.hgsubstate'), (i + 1))) rev[path] = revision except IOError, err: if err.errno != errno.ENOENT: @@ -127,7 +132,7 @@ src = src.lstrip() # strip any extra whitespace after ']' if not util.url(src).isabs(): - parent = _abssource(ctx._repo, abort=False) + parent = _abssource(ctx.repo(), abort=False) if parent: parent = util.url(parent) parent.path = posixpath.join(parent.path or '', src) @@ -275,11 +280,7 @@ def subrelpath(sub): """return path to this subrepo as seen from outermost repo""" - if util.safehasattr(sub, '_relpath'): - return sub._relpath - if not util.safehasattr(sub, '_repo'): - return sub._path - return reporelpath(sub._repo) + return sub._relpath def _abssource(repo, push=False, abort=True): """return pull/push path of repo - either based on parent repo .hgsub info @@ -308,8 +309,8 @@ if abort: raise util.Abort(_("default path for subrepository not found")) -def _sanitize(ui, path, ignore): - for dirname, dirs, names in os.walk(path): +def _sanitize(ui, vfs, ignore): + for dirname, dirs, names in vfs.walk(): for i, d in enumerate(dirs): if d.lower() == ignore: del dirs[i] @@ -319,8 +320,8 @@ for f in names: if f.lower() == 'hgrc': ui.warn(_("warning: removing potentially hostile 'hgrc' " - "in '%s'\n") % dirname) - os.unlink(os.path.join(dirname, f)) + "in '%s'\n") % vfs.join(dirname)) + vfs.unlink(vfs.reljoin(dirname, f)) def subrepo(ctx, path): """return instance of the right subrepo class for subrepo in path""" @@ -332,7 +333,7 @@ import hg as h hg = h - pathutil.pathauditor(ctx._repo.root)(path) + pathutil.pathauditor(ctx.repo().root)(path) state = ctx.substate[path] if state[2] not in types: raise util.Abort(_('unknown subrepo type %s') % state[2]) @@ -373,8 +374,18 @@ class abstractsubrepo(object): - def __init__(self, ui): - self.ui = ui + def __init__(self, ctx, path): + """Initialize abstractsubrepo part + + ``ctx`` is the context referring this subrepository in the + parent repository. + + ``path`` is the path to this subrepositiry as seen from + innermost repository. + """ + self.ui = ctx.repo().ui + self._ctx = ctx + self._path = path def storeclean(self, path): """ @@ -390,6 +401,25 @@ """ raise NotImplementedError + def dirtyreason(self, ignoreupdate=False): + """return reason string if it is ``dirty()`` + + Returned string should have enough information for the message + of exception. + + This returns None, otherwise. + """ + if self.dirty(ignoreupdate=ignoreupdate): + return _("uncommitted changes in subrepository '%s'" + ) % subrelpath(self) + + def bailifchanged(self, ignoreupdate=False): + """raise Abort if subrepository is ``dirty()`` + """ + dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate) + if dirtyreason: + raise util.Abort(dirtyreason) + def basestate(self): """current working directory base state, disregarding .hgsubstate state and working directory modifications""" @@ -469,6 +499,10 @@ """return file flags""" return '' + def printfiles(self, ui, m, fm, fmt): + """handle the files command for this subrepo""" + return 1 + def archive(self, archiver, prefix, match=None): if match is not None: files = [f for f in self.files() if match(f)] @@ -482,7 +516,7 @@ flags = self.fileflags(name) mode = 'x' in flags and 0755 or 0644 symlink = 'l' in flags - archiver.addfile(os.path.join(prefix, self._path, name), + archiver.addfile(self.wvfs.reljoin(prefix, self._path, name), mode, symlink, self.filedata(name)) self.ui.progress(_('archiving (%s)') % relpath, i + 1, unit=_('files'), total=total) @@ -514,12 +548,23 @@ def shortid(self, revid): return revid + @propertycache + def wvfs(self): + """return vfs to access the working directory of this subrepository + """ + return scmutil.vfs(self._ctx.repo().wvfs.join(self._path)) + + @propertycache + def _relpath(self): + """return path to this subrepository as seen from outermost repository + """ + return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path) + class hgsubrepo(abstractsubrepo): def __init__(self, ctx, path, state): - super(hgsubrepo, self).__init__(ctx._repo.ui) - self._path = path + super(hgsubrepo, self).__init__(ctx, path) self._state = state - r = ctx._repo + r = ctx.repo() root = r.wjoin(path) create = not r.wvfs.exists('%s/.hg' % path) self._repo = hg.repository(r.baseui, root, create=create) @@ -623,9 +668,10 @@ @annotatesubrepoerror def add(self, ui, match, prefix, explicitonly, **opts): return cmdutil.add(ui, self._repo, match, - os.path.join(prefix, self._path), explicitonly, - **opts) + self.wvfs.reljoin(prefix, self._path), + explicitonly, **opts) + @annotatesubrepoerror def addremove(self, m, prefix, opts, dry_run, similarity): # In the same way as sub directories are processed, once in a subrepo, # always entry any of its subrepos. Don't corrupt the options that will @@ -633,7 +679,7 @@ opts = copy.copy(opts) opts['subrepos'] = True return scmutil.addremove(self._repo, m, - os.path.join(prefix, self._path), opts, + self.wvfs.reljoin(prefix, self._path), opts, dry_run, similarity) @annotatesubrepoerror @@ -680,7 +726,7 @@ s = subrepo(ctx, subpath) submatch = matchmod.narrowmatcher(subpath, match) total += s.archive( - archiver, os.path.join(prefix, self._path), submatch) + archiver, self.wvfs.reljoin(prefix, self._path), submatch) return total @annotatesubrepoerror @@ -734,7 +780,8 @@ self.ui.status(_('cloning subrepo %s from %s\n') % (subrelpath(self), srcurl)) parentrepo = self._repo._subparent - shutil.rmtree(self._repo.path) + # use self._repo.vfs instead of self.wvfs to remove .hg only + self._repo.vfs.rmtree() other, cloned = hg.clone(self._repo._subparent.baseui, {}, other, self._repo.root, update=False) @@ -835,7 +882,7 @@ def files(self): rev = self._state[1] ctx = self._repo[rev] - return ctx.manifest() + return ctx.manifest().keys() def filedata(self, name): rev = self._state[1] @@ -846,6 +893,17 @@ ctx = self._repo[rev] return ctx.flags(name) + @annotatesubrepoerror + def printfiles(self, ui, m, fm, fmt): + # If the parent context is a workingctx, use the workingctx here for + # consistency. + if self._ctx.rev() is None: + ctx = self._repo[None] + else: + rev = self._state[1] + ctx = self._repo[rev] + return cmdutil.files(ui, ctx, m, fm, fmt, True) + def walk(self, match): ctx = self._repo[None] return ctx.walk(match) @@ -853,13 +911,13 @@ @annotatesubrepoerror def forget(self, match, prefix): return cmdutil.forget(self.ui, self._repo, match, - os.path.join(prefix, self._path), True) + self.wvfs.reljoin(prefix, self._path), True) @annotatesubrepoerror def removefiles(self, matcher, prefix, after, force, subrepos): return cmdutil.remove(self.ui, self._repo, matcher, - os.path.join(prefix, self._path), after, force, - subrepos) + self.wvfs.reljoin(prefix, self._path), + after, force, subrepos) @annotatesubrepoerror def revert(self, substate, *pats, **opts): @@ -877,13 +935,11 @@ opts['date'] = None opts['rev'] = substate[1] - pats = [] - if not opts.get('all'): - pats = ['set:modified()'] self.filerevert(*pats, **opts) # Update the repo to the revision specified in the given substate - self.get(substate, overwrite=True) + if not opts.get('dry_run'): + self.get(substate, overwrite=True) def filerevert(self, *pats, **opts): ctx = self._repo[opts['rev']] @@ -897,12 +953,23 @@ def shortid(self, revid): return revid[:12] + @propertycache + def wvfs(self): + """return own wvfs for efficiency and consitency + """ + return self._repo.wvfs + + @propertycache + def _relpath(self): + """return path to this subrepository as seen from outermost repository + """ + # Keep consistent dir separators by avoiding vfs.join(self._path) + return reporelpath(self._repo) + class svnsubrepo(abstractsubrepo): def __init__(self, ctx, path, state): - super(svnsubrepo, self).__init__(ctx._repo.ui) - self._path = path + super(svnsubrepo, self).__init__(ctx, path) self._state = state - self._ctx = ctx self._exe = util.findexe('svn') if not self._exe: raise util.Abort(_("'svn' executable not found for subrepo '%s'") @@ -923,7 +990,8 @@ cmd.append('--non-interactive') cmd.extend(commands) if filename is not None: - path = os.path.join(self._ctx._repo.origroot, self._path, filename) + path = self.wvfs.reljoin(self._ctx.repo().origroot, + self._path, filename) cmd.append(path) env = dict(os.environ) # Avoid localized output, preserve current locale for everything else. @@ -1055,20 +1123,9 @@ return self.ui.note(_('removing subrepo %s\n') % self._path) - def onerror(function, path, excinfo): - if function is not os.remove: - raise - # read-only files cannot be unlinked under Windows - s = os.stat(path) - if (s.st_mode & stat.S_IWRITE) != 0: - raise - os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE) - os.remove(path) - - path = self._ctx._repo.wjoin(self._path) - shutil.rmtree(path, onerror=onerror) + self.wvfs.rmtree(forcibly=True) try: - os.removedirs(os.path.dirname(path)) + self._ctx.repo().wvfs.removedirs(os.path.dirname(self._path)) except OSError: pass @@ -1083,7 +1140,7 @@ # update to a directory which has since been deleted and recreated. args.append('%s@%s' % (state[0], state[1])) status, err = self._svncommand(args, failok=True) - _sanitize(self.ui, self._ctx._repo.wjoin(self._path), '.svn') + _sanitize(self.ui, self.wvfs, '.svn') if not re.search('Checked out revision [0-9]+.', status): if ('is already a working copy for a different URL' in err and (self._wcchanged()[:2] == (False, False))): @@ -1129,13 +1186,10 @@ class gitsubrepo(abstractsubrepo): def __init__(self, ctx, path, state): - super(gitsubrepo, self).__init__(ctx._repo.ui) + super(gitsubrepo, self).__init__(ctx, path) self._state = state - self._ctx = ctx - self._path = path - self._relpath = os.path.join(reporelpath(ctx._repo), path) - self._abspath = ctx._repo.wjoin(path) - self._subparent = ctx._repo + self._abspath = ctx.repo().wjoin(path) + self._subparent = ctx.repo() self._ensuregit() def _ensuregit(self): @@ -1244,7 +1298,7 @@ return retdata, p.returncode def _gitmissing(self): - return not os.path.exists(os.path.join(self._abspath, '.git')) + return not self.wvfs.exists('.git') def _gitstate(self): return self._gitcommand(['rev-parse', 'HEAD']) @@ -1387,7 +1441,7 @@ self._gitcommand(['reset', 'HEAD']) cmd.append('-f') self._gitcommand(cmd + args) - _sanitize(self.ui, self._abspath, '.git') + _sanitize(self.ui, self.wvfs, '.git') def rawcheckout(): # no branch to checkout, check it out with no branch @@ -1436,7 +1490,7 @@ if tracking[remote] != self._gitcurrentbranch(): checkout([tracking[remote]]) self._gitcommand(['merge', '--ff', remote]) - _sanitize(self.ui, self._abspath, '.git') + _sanitize(self.ui, self.wvfs, '.git') else: # a real merge would be required, just checkout the revision rawcheckout() @@ -1472,7 +1526,7 @@ self.get(state) # fast forward merge elif base != self._state[1]: self._gitcommand(['merge', '--no-commit', revision]) - _sanitize(self.ui, self._abspath, '.git') + _sanitize(self.ui, self.wvfs, '.git') if self.dirty(): if self._gitstate() != revision: @@ -1524,6 +1578,47 @@ return False @annotatesubrepoerror + def add(self, ui, match, prefix, explicitonly, **opts): + if self._gitmissing(): + return [] + + (modified, added, removed, + deleted, unknown, ignored, clean) = self.status(None, unknown=True, + clean=True) + + tracked = set() + # dirstates 'amn' warn, 'r' is added again + for l in (modified, added, deleted, clean): + tracked.update(l) + + # Unknown files not of interest will be rejected by the matcher + files = unknown + files.extend(match.files()) + + rejected = [] + + files = [f for f in sorted(set(files)) if match(f)] + for f in files: + exact = match.exact(f) + command = ["add"] + if exact: + command.append("-f") #should be added, even if ignored + if ui.verbose or not exact: + ui.status(_('adding %s\n') % match.rel(f)) + + if f in tracked: # hg prints 'adding' even if already tracked + if exact: + rejected.append(f) + continue + if not opts.get('dry_run'): + self._gitcommand(command + [f]) + + for f in rejected: + ui.warn(_("%s already tracked!\n") % match.abs(f)) + + return rejected + + @annotatesubrepoerror def remove(self): if self._gitmissing(): return @@ -1535,14 +1630,13 @@ # local-only history self.ui.note(_('removing subrepo %s\n') % self._relpath) self._gitcommand(['config', 'core.bare', 'true']) - for f in os.listdir(self._abspath): + for f, kind in self.wvfs.readdir(): if f == '.git': continue - path = os.path.join(self._abspath, f) - if os.path.isdir(path) and not os.path.islink(path): - shutil.rmtree(path) + if kind == stat.S_IFDIR: + self.wvfs.rmtree(f) else: - os.remove(path) + self.wvfs.unlink(f) def archive(self, archiver, prefix, match=None): total = 0 @@ -1567,7 +1661,7 @@ data = info.linkname else: data = tar.extractfile(info).read() - archiver.addfile(os.path.join(prefix, self._path, info.name), + archiver.addfile(self.wvfs.reljoin(prefix, self._path, info.name), info.mode, info.issym(), data) total += 1 self.ui.progress(_('archiving (%s)') % relpath, i + 1, @@ -1577,11 +1671,30 @@ @annotatesubrepoerror + def cat(self, match, prefix, **opts): + rev = self._state[1] + if match.anypats(): + return 1 #No support for include/exclude yet + + if not match.files(): + return 1 + + for f in match.files(): + output = self._gitcommand(["show", "%s:%s" % (rev, f)]) + fp = cmdutil.makefileobj(self._subparent, opts.get('output'), + self._ctx.node(), + pathname=self.wvfs.reljoin(prefix, f)) + fp.write(output) + fp.close() + return 0 + + + @annotatesubrepoerror def status(self, rev2, **opts): rev1 = self._state[1] if self._gitmissing() or not rev1: # if the repo is missing, return no results - return [], [], [], [], [], [], [] + return scmutil.status([], [], [], [], [], [], []) modified, added, removed = [], [], [] self._gitupdatestat() if rev2: @@ -1603,13 +1716,42 @@ deleted, unknown, ignored, clean = [], [], [], [] - if not rev2: - command = ['ls-files', '--others', '--exclude-standard'] - out = self._gitcommand(command) - for line in out.split('\n'): - if len(line) == 0: - continue - unknown.append(line) + command = ['status', '--porcelain', '-z'] + if opts.get('unknown'): + command += ['--untracked-files=all'] + if opts.get('ignored'): + command += ['--ignored'] + out = self._gitcommand(command) + + changedfiles = set() + changedfiles.update(modified) + changedfiles.update(added) + changedfiles.update(removed) + for line in out.split('\0'): + if not line: + continue + st = line[0:2] + #moves and copies show 2 files on one line + if line.find('\0') >= 0: + filename1, filename2 = line[3:].split('\0') + else: + filename1 = line[3:] + filename2 = None + + changedfiles.add(filename1) + if filename2: + changedfiles.add(filename2) + + if st == '??': + unknown.append(filename1) + elif st == '!!': + ignored.append(filename1) + + if opts.get('clean'): + out = self._gitcommand(['ls-files']) + for f in out.split('\n'): + if not f in changedfiles: + clean.append(f) return scmutil.status(modified, added, removed, deleted, unknown, ignored, clean) @@ -1624,7 +1766,7 @@ # for Git, this also implies '-p' cmd.append('-U%d' % diffopts.context) - gitprefix = os.path.join(prefix, self._path) + gitprefix = self.wvfs.reljoin(prefix, self._path) if diffopts.noprefix: cmd.extend(['--src-prefix=%s/' % gitprefix, @@ -1645,17 +1787,15 @@ if node2: cmd.append(node2) - if match.anypats(): - return #No support for include/exclude yet - output = "" if match.always(): output += self._gitcommand(cmd) + '\n' - elif match.files(): - for f in match.files(): - output += self._gitcommand(cmd + [f]) + '\n' - elif match(gitprefix): #Subrepo is matched - output += self._gitcommand(cmd) + '\n' + else: + st = self.status(node2)[:3] + files = [f for sublist in st for f in sublist] + for f in files: + if match(f): + output += self._gitcommand(cmd + ['--', f]) + '\n' if output.strip(): ui.write(output) @@ -1670,10 +1810,10 @@ bakname = "%s.orig" % name self.ui.note(_('saving current version of %s as %s\n') % (name, bakname)) - util.rename(os.path.join(self._abspath, name), - os.path.join(self._abspath, bakname)) + self.wvfs.rename(name, bakname) - self.get(substate, overwrite=True) + if not opts.get('dry_run'): + self.get(substate, overwrite=True) return [] def shortid(self, revid): diff -r 612ed41ae359 -r e89f909edffa mercurial/tags.py --- a/mercurial/tags.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/tags.py Thu Apr 16 20:57:51 2015 -0500 @@ -15,21 +15,78 @@ import util import encoding import error +from array import array import errno import time +# Tags computation can be expensive and caches exist to make it fast in +# the common case. +# +# The "hgtagsfnodes1" cache file caches the .hgtags filenode values for +# each revision in the repository. The file is effectively an array of +# fixed length records. Read the docs for "hgtagsfnodescache" for technical +# details. +# +# The .hgtags filenode cache grows in proportion to the length of the +# changelog. The file is truncated when the # changelog is stripped. +# +# The purpose of the filenode cache is to avoid the most expensive part +# of finding global tags, which is looking up the .hgtags filenode in the +# manifest for each head. This can take dozens or over 100ms for +# repositories with very large manifests. Multiplied by dozens or even +# hundreds of heads and there is a significant performance concern. +# +# There also exist a separate cache file for each repository filter. +# These "tags-*" files store information about the history of tags. +# +# The tags cache files consists of a cache validation line followed by +# a history of tags. +# +# The cache validation line has the format: +# +# [] +# +# is an integer revision and is a 40 character hex +# node for that changeset. These redundantly identify the repository +# tip from the time the cache was written. In addition, , +# if present, is a 40 character hex hash of the contents of the filtered +# revisions for this filter. If the set of filtered revs changes, the +# hash will change and invalidate the cache. +# +# The history part of the tags cache consists of lines of the form: +# +# +# +# (This format is identical to that of .hgtags files.) +# +# is the tag name and is the 40 character hex changeset +# the tag is associated with. +# +# Tags are written sorted by tag name. +# +# Tags associated with multiple changesets have an entry for each changeset. +# The most recent changeset (in terms of revlog ordering for the head +# setting it) for each tag is last. + def findglobaltags(ui, repo, alltags, tagtypes): - '''Find global tags in repo by reading .hgtags from every head that - has a distinct version of it, using a cache to avoid excess work. - Updates the dicts alltags, tagtypes in place: alltags maps tag name - to (node, hist) pair (see _readtags() below), and tagtypes maps tag - name to tag type ("global" in this case).''' + '''Find global tags in a repo. + + "alltags" maps tag name to (node, hist) 2-tuples. + + "tagtypes" maps tag name to tag type. Global tags always have the + "global" tag type. + + The "alltags" and "tagtypes" dicts are updated in place. Empty dicts + should be passed in. + + The tags cache is read and updated as a side-effect of calling. + ''' # This is so we can be lazy and assume alltags contains only global # tags when we pass it to _writetagcache(). assert len(alltags) == len(tagtypes) == 0, \ "findglobaltags() should be called first" - (heads, tagfnode, cachetags, shouldwrite) = _readtagcache(ui, repo) + (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo) if cachetags is not None: assert not shouldwrite # XXX is this really 100% correct? are there oddball special @@ -38,9 +95,9 @@ _updatetags(cachetags, 'global', alltags, tagtypes) return - seen = set() # set of fnode + seen = set() # set of fnode fctx = None - for head in reversed(heads): # oldest to newest + for head in reversed(heads): # oldest to newest assert head in repo.changelog.nodemap, \ "tag cache returned bogus head %s" % short(head) @@ -57,10 +114,10 @@ # and update the cache (if necessary) if shouldwrite: - _writetagcache(ui, repo, heads, tagfnode, alltags) + _writetagcache(ui, repo, valid, alltags) def readlocaltags(ui, repo, alltags, tagtypes): - '''Read local tags in repo. Update alltags and tagtypes.''' + '''Read local tags in repo. Update alltags and tagtypes.''' try: data = repo.vfs.read("localtags") except IOError, inst: @@ -86,14 +143,18 @@ def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False): '''Read tag definitions from a file (or any source of lines). + This function returns two sortdicts with similar information: + - the first dict, bintaghist, contains the tag information as expected by the _readtags function, i.e. a mapping from tag name to (node, hist): - node is the node id from the last line read for that name, - hist is the list of node ids previously associated with it (in file - order). All node ids are binary, not hex. + order). All node ids are binary, not hex. + - the second dict, hextaglines, is a mapping from tag name to a list of [hexnode, line number] pairs, ordered from the oldest to the newest node. + When calcnodelines is False the hextaglines dict is not calculated (an empty dict is returned). This is done to improve this function's performance in cases where the line numbers are not needed. @@ -139,10 +200,13 @@ def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False): '''Read tag definitions from a file (or any source of lines). - Return a mapping from tag name to (node, hist): node is the node id - from the last line read for that name, and hist is the list of node - ids previously associated with it (in file order). All node ids are - binary, not hex.''' + + Returns a mapping from tag name to (node, hist). + + "node" is the node id from the last line read for that name. "hist" + is the list of node ids previously associated with it (in file order). + All node ids are binary, not hex. + ''' filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode, calcnodelines=calcnodelines) for tag, taghist in filetags.items(): @@ -174,64 +238,54 @@ ahist.extend([n for n in bhist if n not in ahist]) alltags[name] = anode, ahist - -# The tag cache only stores info about heads, not the tag contents -# from each head. I.e. it doesn't try to squeeze out the maximum -# performance, but is simpler has a better chance of actually -# working correctly. And this gives the biggest performance win: it -# avoids looking up .hgtags in the manifest for every head, and it -# can avoid calling heads() at all if there have been no changes to -# the repo. +def _filename(repo): + """name of a tagcache file for a given repo or repoview""" + filename = 'cache/tags2' + if repo.filtername: + filename = '%s-%s' % (filename, repo.filtername) + return filename def _readtagcache(ui, repo): - '''Read the tag cache and return a tuple (heads, fnodes, cachetags, - shouldwrite). If the cache is completely up-to-date, cachetags is a - dict of the form returned by _readtags(); otherwise, it is None and - heads and fnodes are set. In that case, heads is the list of all - heads currently in the repository (ordered from tip to oldest) and - fnodes is a mapping from head to .hgtags filenode. If those two are - set, caller is responsible for reading tag info from each head.''' + '''Read the tag cache. + + Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite). + + If the cache is completely up-to-date, "cachetags" is a dict of the + form returned by _readtags() and "heads", "fnodes", and "validinfo" are + None and "shouldwrite" is False. + + If the cache is not up to date, "cachetags" is None. "heads" is a list + of all heads currently in the repository, ordered from tip to oldest. + "validinfo" is a tuple describing cache validation info. This is used + when writing the tags cache. "fnodes" is a mapping from head to .hgtags + filenode. "shouldwrite" is True. + + If the cache is not up to date, the caller is responsible for reading tag + info from each returned head. (See findglobaltags().) + ''' + import scmutil # avoid cycle try: - cachefile = repo.vfs('cache/tags', 'r') + cachefile = repo.vfs(_filename(repo), 'r') # force reading the file for static-http cachelines = iter(cachefile) except IOError: cachefile = None - # The cache file consists of lines like - # [] - # where and redundantly identify a repository - # head from the time the cache was written, and is the - # filenode of .hgtags on that head. Heads with no .hgtags file will - # have no . The cache is ordered from tip to oldest (which - # is part of why is there: a quick visual check is all - # that's required to ensure correct order). - # - # This information is enough to let us avoid the most expensive part - # of finding global tags, which is looking up in the - # manifest for each head. - cacherevs = [] # list of headrev - cacheheads = [] # list of headnode - cachefnode = {} # map headnode to filenode + cacherev = None + cachenode = None + cachehash = None if cachefile: try: - for line in cachelines: - if line == "\n": - break - line = line.split() - cacherevs.append(int(line[0])) - headnode = bin(line[1]) - cacheheads.append(headnode) - if len(line) == 3: - fnode = bin(line[2]) - cachefnode[headnode] = fnode + validline = cachelines.next() + validline = validline.split() + cacherev = int(validline[0]) + cachenode = bin(validline[1]) + if len(validline) > 2: + cachehash = bin(validline[2]) except Exception: - # corruption of the tags cache, just recompute it - ui.warn(_('.hg/cache/tags is corrupt, rebuilding it\n')) - cacheheads = [] - cacherevs = [] - cachefnode = {} + # corruption of the cache, just recompute it. + pass tipnode = repo.changelog.tip() tiprev = len(repo.changelog) - 1 @@ -240,18 +294,22 @@ # (Unchanged tip trivially means no changesets have been added. # But, thanks to localrepository.destroyed(), it also means none # have been destroyed by strip or rollback.) - if cacheheads and cacheheads[0] == tipnode and cacherevs[0] == tiprev: + if (cacherev == tiprev + and cachenode == tipnode + and cachehash == scmutil.filteredhash(repo, tiprev)): tags = _readtags(ui, repo, cachelines, cachefile.name) cachefile.close() - return (None, None, tags, False) + return (None, None, None, tags, False) if cachefile: cachefile.close() # ignore rest of file + valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev)) + repoheads = repo.heads() # Case 2 (uncommon): empty repo; get out quickly and don't bother # writing an empty cache. if repoheads == [nullid]: - return ([], {}, {}, False) + return ([], {}, valid, {}, False) # Case 3 (uncommon): cache file missing or empty. @@ -269,75 +327,53 @@ if not len(repo.file('.hgtags')): # No tags have ever been committed, so we can avoid a # potentially expensive search. - return (repoheads, cachefnode, None, True) + return ([], {}, valid, None, True) starttime = time.time() - newheads = [head - for head in repoheads - if head not in set(cacheheads)] - # Now we have to lookup the .hgtags filenode for every new head. # This is the most expensive part of finding tags, so performance # depends primarily on the size of newheads. Worst case: no cache # file, so newheads == repoheads. - for head in reversed(newheads): - cctx = repo[head] - try: - fnode = cctx.filenode('.hgtags') + fnodescache = hgtagsfnodescache(repo.unfiltered()) + cachefnode = {} + for head in reversed(repoheads): + fnode = fnodescache.getfnode(head) + if fnode != nullid: cachefnode[head] = fnode - except error.LookupError: - # no .hgtags file on this head - pass + + fnodescache.write() duration = time.time() - starttime ui.log('tagscache', - 'resolved %d tags cache entries from %d manifests in %0.4f ' + '%d/%d cache hits/lookups in %0.4f ' 'seconds\n', - len(cachefnode), len(newheads), duration) + fnodescache.hitcount, fnodescache.lookupcount, duration) # Caller has to iterate over all heads, but can use the filenodes in # cachefnode to get to each .hgtags revision quickly. - return (repoheads, cachefnode, None, True) + return (repoheads, cachefnode, valid, None, True) -def _writetagcache(ui, repo, heads, tagfnode, cachetags): - +def _writetagcache(ui, repo, valid, cachetags): + filename = _filename(repo) try: - cachefile = repo.vfs('cache/tags', 'w', atomictemp=True) + cachefile = repo.vfs(filename, 'w', atomictemp=True) except (OSError, IOError): return - ui.log('tagscache', 'writing tags cache file with %d heads and %d tags\n', - len(heads), len(cachetags)) + ui.log('tagscache', 'writing .hg/%s with %d tags\n', + filename, len(cachetags)) - realheads = repo.heads() # for sanity checks below - for head in heads: - # temporary sanity checks; these can probably be removed - # once this code has been in crew for a few weeks - assert head in repo.changelog.nodemap, \ - 'trying to write non-existent node %s to tag cache' % short(head) - assert head in realheads, \ - 'trying to write non-head %s to tag cache' % short(head) - assert head != nullid, \ - 'trying to write nullid to tag cache' - - # This can't fail because of the first assert above. When/if we - # remove that assert, we might want to catch LookupError here - # and downgrade it to a warning. - rev = repo.changelog.rev(head) - - fnode = tagfnode.get(head) - if fnode: - cachefile.write('%d %s %s\n' % (rev, hex(head), hex(fnode))) - else: - cachefile.write('%d %s\n' % (rev, hex(head))) + if valid[2]: + cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2]))) + else: + cachefile.write('%d %s\n' % (valid[0], hex(valid[1]))) # Tag names in the cache are in UTF-8 -- which is the whole reason # we keep them in UTF-8 throughout this module. If we converted # them local encoding on input, we would lose info writing them to # the cache. - cachefile.write('\n') - for (name, (node, hist)) in cachetags.iteritems(): + for (name, (node, hist)) in sorted(cachetags.iteritems()): for n in hist: cachefile.write("%s %s\n" % (hex(n), name)) cachefile.write("%s %s\n" % (hex(node), name)) @@ -346,3 +382,153 @@ cachefile.close() except (OSError, IOError): pass + +_fnodescachefile = 'cache/hgtagsfnodes1' +_fnodesrecsize = 4 + 20 # changeset fragment + filenode +_fnodesmissingrec = '\xff' * 24 + +class hgtagsfnodescache(object): + """Persistent cache mapping revisions to .hgtags filenodes. + + The cache is an array of records. Each item in the array corresponds to + a changelog revision. Values in the array contain the first 4 bytes of + the node hash and the 20 bytes .hgtags filenode for that revision. + + The first 4 bytes are present as a form of verification. Repository + stripping and rewriting may change the node at a numeric revision in the + changelog. The changeset fragment serves as a verifier to detect + rewriting. This logic is shared with the rev branch cache (see + branchmap.py). + + The instance holds in memory the full cache content but entries are + only parsed on read. + + Instances behave like lists. ``c[i]`` works where i is a rev or + changeset node. Missing indexes are populated automatically on access. + """ + def __init__(self, repo): + assert repo.filtername is None + + self._repo = repo + + # Only for reporting purposes. + self.lookupcount = 0 + self.hitcount = 0 + + self._raw = array('c') + + data = repo.vfs.tryread(_fnodescachefile) + self._raw.fromstring(data) + + # The end state of self._raw is an array that is of the exact length + # required to hold a record for every revision in the repository. + # We truncate or extend the array as necessary. self._dirtyoffset is + # defined to be the start offset at which we need to write the output + # file. This offset is also adjusted when new entries are calculated + # for array members. + cllen = len(repo.changelog) + wantedlen = cllen * _fnodesrecsize + rawlen = len(self._raw) + + self._dirtyoffset = None + + if rawlen < wantedlen: + self._dirtyoffset = rawlen + self._raw.extend('\xff' * (wantedlen - rawlen)) + elif rawlen > wantedlen: + # There's no easy way to truncate array instances. This seems + # slightly less evil than copying a potentially large array slice. + for i in range(rawlen - wantedlen): + self._raw.pop() + self._dirtyoffset = len(self._raw) + + def getfnode(self, node): + """Obtain the filenode of the .hgtags file at a specified revision. + + If the value is in the cache, the entry will be validated and returned. + Otherwise, the filenode will be computed and returned. + + If an .hgtags does not exist at the specified revision, nullid is + returned. + """ + ctx = self._repo[node] + rev = ctx.rev() + + self.lookupcount += 1 + + offset = rev * _fnodesrecsize + record = self._raw[offset:offset + _fnodesrecsize].tostring() + properprefix = node[0:4] + + # Validate and return existing entry. + if record != _fnodesmissingrec: + fileprefix = record[0:4] + + if fileprefix == properprefix: + self.hitcount += 1 + return record[4:] + + # Fall through. + + # If we get here, the entry is either missing or invalid. Populate it. + try: + fnode = ctx.filenode('.hgtags') + except error.LookupError: + # No .hgtags file on this revision. + fnode = nullid + + # Slices on array instances only accept other array. + entry = array('c', properprefix + fnode) + self._raw[offset:offset + _fnodesrecsize] = entry + # self._dirtyoffset could be None. + self._dirtyoffset = min(self._dirtyoffset, offset) or 0 + + return fnode + + def write(self): + """Perform all necessary writes to cache file. + + This may no-op if no writes are needed or if a write lock could + not be obtained. + """ + if self._dirtyoffset is None: + return + + data = self._raw[self._dirtyoffset:] + if not data: + return + + repo = self._repo + + try: + lock = repo.wlock(wait=False) + except error.LockHeld: + repo.ui.log('tagscache', + 'not writing .hg/%s because lock held\n' % + (_fnodescachefile)) + return + + try: + try: + f = repo.vfs.open(_fnodescachefile, 'ab') + try: + # if the file has been truncated + actualoffset = f.tell() + if actualoffset < self._dirtyoffset: + self._dirtyoffset = actualoffset + data = self._raw[self._dirtyoffset:] + f.seek(self._dirtyoffset) + f.truncate() + repo.ui.log('tagscache', + 'writing %d bytes to %s\n' % ( + len(data), _fnodescachefile)) + f.write(data) + self._dirtyoffset = None + finally: + f.close() + except (IOError, OSError), inst: + repo.ui.log('tagscache', + "couldn't write %s: %s\n" % ( + _fnodescachefile, inst)) + finally: + lock.release() diff -r 612ed41ae359 -r e89f909edffa mercurial/templatefilters.py --- a/mercurial/templatefilters.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templatefilters.py Thu Apr 16 20:57:51 2015 -0500 @@ -234,6 +234,10 @@ """:localdate: Date. Converts a date to local date.""" return (util.parsedate(text)[0], util.makedate()[1]) +def lower(text): + """:lower: Any text. Converts the text to lowercase.""" + return encoding.lower(text) + def nonempty(str): """:nonempty: Any text. Returns '(none)' if the string is empty.""" return str or "(none)" @@ -344,6 +348,10 @@ """ return indent(text, '\t') +def upper(text): + """:upper: Any text. Converts the text to uppercase.""" + return encoding.upper(text) + def urlescape(text): """:urlescape: Any text. Escapes all "special" characters. For example, "foo bar" becomes "foo%20bar". @@ -387,6 +395,7 @@ "json": json, "jsonescape": jsonescape, "localdate": localdate, + "lower": lower, "nonempty": nonempty, "obfuscate": obfuscate, "permissions": permissions, @@ -402,6 +411,7 @@ "strip": strip, "stripdir": stripdir, "tabindent": tabindent, + "upper": upper, "urlescape": urlescape, "user": userfilter, "emailuser": emailuser, diff -r 612ed41ae359 -r e89f909edffa mercurial/templatekw.py --- a/mercurial/templatekw.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templatekw.py Thu Apr 16 20:57:51 2015 -0500 @@ -12,11 +12,15 @@ # This helper class allows us to handle both: # "{files}" (legacy command-line-specific list hack) and # "{files % '{file}\n'}" (hgweb-style with inlining and function support) +# and to access raw values: +# "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}" +# "{get(extras, key)}" class _hybrid(object): - def __init__(self, gen, values, joinfmt=None): + def __init__(self, gen, values, makemap, joinfmt=None): self.gen = gen self.values = values + self._makemap = makemap if joinfmt: self.joinfmt = joinfmt else: @@ -24,16 +28,23 @@ def __iter__(self): return self.gen def __call__(self): + makemap = self._makemap for x in self.values: - yield x + yield makemap(x) + def __contains__(self, x): + return x in self.values def __len__(self): return len(self.values) + def __getattr__(self, name): + if name != 'get': + raise AttributeError(name) + return getattr(self.values, name) def showlist(name, values, plural=None, element=None, **args): if not element: element = name f = _showlist(name, values, plural, **args) - return _hybrid(f, [{element: x} for x in values]) + return _hybrid(f, values, lambda x: {element: x}) def _showlist(name, values, plural=None, **args): '''expand set of values. @@ -200,9 +211,9 @@ repo = args['ctx']._repo bookmarks = args['ctx'].bookmarks() current = repo._bookmarkcurrent - c = [{'bookmark': x, 'current': current} for x in bookmarks] + makemap = lambda v: {'bookmark': v, 'current': current} f = _showlist('bookmark', bookmarks, **args) - return _hybrid(f, c, lambda x: x['bookmark']) + return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark']) def showchildren(**args): """:children: List of strings. The children of the changeset.""" @@ -241,9 +252,12 @@ """:extras: List of dicts with key, value entries of the 'extras' field of this changeset.""" extras = args['ctx'].extra() - c = [{'key': x[0], 'value': x[1]} for x in sorted(extras.items())] + extras = util.sortdict((k, extras[k]) for k in sorted(extras)) + makemap = lambda k: {'key': k, 'value': extras[k]} + c = [makemap(k) for k in extras] f = _showlist('extra', c, plural='extras', **args) - return _hybrid(f, c, lambda x: '%s=%s' % (x['key'], x['value'])) + return _hybrid(f, extras, makemap, + lambda x: '%s=%s' % (x['key'], x['value'])) def showfileadds(**args): """:file_adds: List of strings. Files added by this changeset.""" @@ -267,9 +281,12 @@ if rename: copies.append((fn, rename[0])) - c = [{'name': x[0], 'source': x[1]} for x in copies] + copies = util.sortdict(copies) + makemap = lambda k: {'name': k, 'source': copies[k]} + c = [makemap(k) for k in copies] f = _showlist('file_copy', c, plural='file_copies', **args) - return _hybrid(f, c, lambda x: '%s (%s)' % (x['name'], x['source'])) + return _hybrid(f, copies, makemap, + lambda x: '%s (%s)' % (x['name'], x['source'])) # showfilecopiesswitch() displays file copies only if copy records are # provided before calling the templater, usually with a --copies @@ -279,9 +296,12 @@ only if the --copied switch is set. """ copies = args['revcache'].get('copies') or [] - c = [{'name': x[0], 'source': x[1]} for x in copies] + copies = util.sortdict(copies) + makemap = lambda k: {'name': k, 'source': copies[k]} + c = [makemap(k) for k in copies] f = _showlist('file_copy', c, plural='file_copies', **args) - return _hybrid(f, c, lambda x: '%s (%s)' % (x['name'], x['source'])) + return _hybrid(f, copies, makemap, + lambda x: '%s (%s)' % (x['name'], x['source'])) def showfiledels(**args): """:file_dels: List of strings. Files removed by this changeset.""" @@ -313,9 +333,9 @@ def showmanifest(**args): repo, ctx, templ = args['repo'], args['ctx'], args['templ'] + mnode = ctx.manifestnode() args = args.copy() - args.update({'rev': repo.manifest.rev(ctx.changeset()[0]), - 'node': hex(ctx.changeset()[0])}) + args.update({'rev': repo.manifest.rev(mnode), 'node': hex(mnode)}) return templ('manifest', **args) def shownode(repo, ctx, templ, **args): @@ -377,7 +397,7 @@ def shownames(namespace, **args): """helper method to generate a template keyword for a namespace""" ctx = args['ctx'] - repo = ctx._repo + repo = ctx.repo() ns = repo.names[namespace] names = ns.names(repo, ctx.node()) return showlist(ns.templatename, names, plural=namespace, **args) diff -r 612ed41ae359 -r e89f909edffa mercurial/templater.py --- a/mercurial/templater.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templater.py Thu Apr 16 20:57:51 2015 -0500 @@ -162,8 +162,13 @@ def runfilter(context, mapping, data): func, data, filt = data + # func() may return string, generator of strings or arbitrary object such + # as date tuple, but filter does not want generator. + thing = func(context, mapping, data) + if isinstance(thing, types.GeneratorType): + thing = stringify(thing) try: - return filt(func(context, mapping, data)) + return filt(thing) except (ValueError, AttributeError, TypeError): if isinstance(data, tuple): dt = data[1] @@ -214,6 +219,8 @@ raise error.ParseError(_("unknown function '%s'") % n) def date(context, mapping, args): + """:date(date[, fmt]): Format a date. See :hg:`help dates` for formatting + strings.""" if not (1 <= len(args) <= 2): # i18n: "date" is a keyword raise error.ParseError(_("date expects one or two arguments")) @@ -225,6 +232,8 @@ return util.datestr(date) def diff(context, mapping, args): + """:diff([includepattern [, excludepattern]]): Show a diff, optionally + specifying files to include or exclude.""" if len(args) > 2: # i18n: "diff" is a keyword raise error.ParseError(_("diff expects one, two or no arguments")) @@ -242,6 +251,8 @@ return ''.join(chunks) def fill(context, mapping, args): + """:fill(text[, width[, initialident[, hangindent]]]): Fill many + paragraphs with optional indentation. See the "fill" filter.""" if not (1 <= len(args) <= 4): # i18n: "fill" is a keyword raise error.ParseError(_("fill expects one to four arguments")) @@ -265,8 +276,8 @@ return templatefilters.fill(text, width, initindent, hangindent) def pad(context, mapping, args): - """usage: pad(text, width, fillchar=' ', right=False) - """ + """:pad(text, width[, fillchar=' '[, right=False]]): Pad text with a + fill character.""" if not (2 <= len(args) <= 4): # i18n: "pad" is a keyword raise error.ParseError(_("pad() expects two to four arguments")) @@ -291,6 +302,9 @@ return text.ljust(width, fillchar) def get(context, mapping, args): + """:get(dict, key): Get an attribute/key from an object. Some keywords + are complex types. This function allows you to obtain the value of an + attribute on these type.""" if len(args) != 2: # i18n: "get" is a keyword raise error.ParseError(_("get() expects two arguments")) @@ -312,6 +326,8 @@ yield t def if_(context, mapping, args): + """:if(expr, then[, else]): Conditionally execute based on the result of + an expression.""" if not (2 <= len(args) <= 3): # i18n: "if" is a keyword raise error.ParseError(_("if expects two or three arguments")) @@ -323,6 +339,8 @@ yield _evalifliteral(args[2], context, mapping) def ifcontains(context, mapping, args): + """:ifcontains(search, thing, then[, else]): Conditionally execute based + on whether the item "search" is in "thing".""" if not (3 <= len(args) <= 4): # i18n: "ifcontains" is a keyword raise error.ParseError(_("ifcontains expects three or four arguments")) @@ -330,15 +348,14 @@ item = stringify(args[0][0](context, mapping, args[0][1])) items = args[1][0](context, mapping, args[1][1]) - # Iterating over items gives a formatted string, so we iterate - # directly over the raw values. - if ((callable(items) and item in [i.values()[0] for i in items()]) or - (isinstance(items, str) and item in items)): + if item in items: yield _evalifliteral(args[2], context, mapping) elif len(args) == 4: yield _evalifliteral(args[3], context, mapping) def ifeq(context, mapping, args): + """:ifeq(expr1, expr2, then[, else]): Conditionally execute based on + whether 2 items are equivalent.""" if not (3 <= len(args) <= 4): # i18n: "ifeq" is a keyword raise error.ParseError(_("ifeq expects three or four arguments")) @@ -351,6 +368,7 @@ yield _evalifliteral(args[3], context, mapping) def join(context, mapping, args): + """:join(list, sep): Join items in a list with a delimiter.""" if not (1 <= len(args) <= 2): # i18n: "join" is a keyword raise error.ParseError(_("join expects one or two arguments")) @@ -373,6 +391,9 @@ yield x def label(context, mapping, args): + """:label(label, expr): Apply a label to generated content. Content with + a label applied can result in additional post-processing, such as + automatic colorization.""" if len(args) != 2: # i18n: "label" is a keyword raise error.ParseError(_("label expects two arguments")) @@ -381,19 +402,19 @@ yield _evalifliteral(args[1], context, mapping) def revset(context, mapping, args): - """usage: revset(query[, formatargs...]) - """ + """:revset(query[, formatargs...]): Execute a revision set query. See + :hg:`help revset`.""" if not len(args) > 0: # i18n: "revset" is a keyword raise error.ParseError(_("revset expects one or more arguments")) raw = args[0][1] ctx = mapping['ctx'] - repo = ctx._repo + repo = ctx.repo() def query(expr): m = revsetmod.match(repo.ui, expr) - return m(repo, revsetmod.spanset(repo)) + return m(repo) if len(args) > 1: formatargs = list([a[0](context, mapping, a[1]) for a in args[1:]]) @@ -411,6 +432,7 @@ return templatekw.showlist("revision", revs, **mapping) def rstdoc(context, mapping, args): + """:rstdoc(text, style): Format ReStructuredText.""" if len(args) != 2: # i18n: "rstdoc" is a keyword raise error.ParseError(_("rstdoc expects two arguments")) @@ -421,8 +443,8 @@ return minirst.format(text, style=style, keep=['verbose']) def shortest(context, mapping, args): - """usage: shortest(node, minlength=4) - """ + """:shortest(node, minlength=4): Obtain the shortest representation of + a node.""" if not (1 <= len(args) <= 2): # i18n: "shortest" is a keyword raise error.ParseError(_("shortest() expects one or two arguments")) @@ -473,6 +495,7 @@ return shortest def strip(context, mapping, args): + """:strip(text[, chars]): Strip characters from a string.""" if not (1 <= len(args) <= 2): # i18n: "strip" is a keyword raise error.ParseError(_("strip expects one or two arguments")) @@ -484,6 +507,8 @@ return text.strip() def sub(context, mapping, args): + """:sub(pattern, replacement, expression): Perform text substitution + using regular expressions.""" if len(args) != 3: # i18n: "sub" is a keyword raise error.ParseError(_("sub expects three arguments")) @@ -494,6 +519,8 @@ yield re.sub(pat, rpl, src) def startswith(context, mapping, args): + """:startswith(pattern, text): Returns the value from the "text" argument + if it begins with the content from the "pattern" argument.""" if len(args) != 2: # i18n: "startswith" is a keyword raise error.ParseError(_("startswith expects two arguments")) @@ -506,7 +533,7 @@ def word(context, mapping, args): - """return nth word from a string""" + """:word(number, text[, separator]): Return the nth word from a string.""" if not (2 <= len(args) <= 3): # i18n: "word" is a keyword raise error.ParseError(_("word expects two or three arguments, got %d") @@ -654,7 +681,10 @@ self.mapfile = mapfile or 'template' self.cache = cache.copy() self.map = {} - self.base = (mapfile and os.path.dirname(mapfile)) or '' + if mapfile: + self.base = os.path.dirname(mapfile) + else: + self.base = '' self.filters = templatefilters.filters.copy() self.filters.update(filters) self.defaults = defaults @@ -763,3 +793,6 @@ return style, mapfile raise RuntimeError("No hgweb templates found in %r" % paths) + +# tell hggettext to extract docstrings from these functions: +i18nfunctions = funcs.values() diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/gitweb/map --- a/mercurial/templates/gitweb/map Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/gitweb/map Thu Apr 16 20:57:51 2015 -0500 @@ -140,7 +140,7 @@ parent {rev} - {changesetlink} {ifeq(node, basenode, '(current diff)', \'({difffrom})\')} + {changesetlink} {ifeq(node, basenode, '(current diff)', '({difffrom})')} ' difffrom = 'diff' diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/json/changelist.tmpl --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/templates/json/changelist.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,5 @@ +\{ + "node": {node|json}, + "changeset_count": {changesets|json}, + "changesets": [{join(entries%changelistentry, ", ")}] +} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/json/map --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/templates/json/map Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,174 @@ +mimetype = 'application/json' +filerevision = '"not yet implemented"' +search = '"not yet implemented"' +# changelog and shortlog are the same web API but with different +# number of entries. +changelog = changelist.tmpl +shortlog = changelist.tmpl +changelistentry = '\{ + "node": {node|json}, + "date": {date|json}, + "desc": {desc|json}, + "bookmarks": [{join(bookmarks%changelistentryname, ", ")}], + "tags": [{join(tags%changelistentryname, ", ")}], + "user": {author|json} + }' +changelistentryname = '{name|json}' +changeset = '\{ + "node": {node|json}, + "date": {date|json}, + "desc": {desc|json}, + "branch": {if(branch, branch%changesetbranch, "default"|json)}, + "bookmarks": [{join(changesetbookmark, ", ")}], + "tags": [{join(changesettag, ", ")}], + "user": {author|json}, + "parents": [{join(parent%changesetparent, ", ")}], + "phase": {phase|json} + }' +changesetbranch = '{name|json}' +changesetbookmark = '{bookmark|json}' +changesettag = '{tag|json}' +changesetparent = '{node|json}' +manifest = '\{ + "node": {node|json}, + "abspath": {path|json}, + "directories": [{join(dentries%direntry, ", ")}], + "files": [{join(fentries%fileentry, ", ")}], + "bookmarks": [{join(bookmarks%name, ", ")}], + "tags": [{join(tags%name, ", ")}] + }' +name = '{name|json}' +direntry = '\{ + "abspath": {path|json}, + "basename": {basename|json}, + "emptydirs": {emptydirs|json} + }' +fileentry = '\{ + "abspath": {file|json}, + "basename": {basename|json}, + "date": {date|json}, + "size": {size|json}, + "flags": {permissions|json} + }' +tags = '\{ + "node": {node|json}, + "tags": [{join(entriesnotip%tagentry, ", ")}] + }' +tagentry = '\{ + "tag": {tag|json}, + "node": {node|json}, + "date": {date|json} + }' +bookmarks = '\{ + "node": {node|json}, + "bookmarks": [{join(entries%bookmarkentry, ", ")}] + }' +bookmarkentry = '\{ + "bookmark": {bookmark|json}, + "node": {node|json}, + "date": {date|json} + }' +branches = '\{ + "branches": [{join(entries%branchentry, ", ")}] + }' +branchentry = '\{ + "branch": {branch|json}, + "node": {node|json}, + "date": {date|json}, + "status": {status|json} + }' +summary = '"not yet implemented"' +filediff = '\{ + "path": {file|json}, + "node": {node|json}, + "date": {date|json}, + "desc": {desc|json}, + "author": {author|json}, + "parents": [{join(parent%changesetparent, ", ")}], + "children": [{join(child%changesetparent, ", ")}], + "diff": [{join(diff%diffblock, ", ")}] + }' +diffblock = '\{ + "blockno": {blockno|json}, + "lines": [{join(lines, ", ")}] + }' +difflineplus = '\{ + "t": "+", + "n": {lineno|json}, + "l": {line|json} + }' +difflineminus = '\{ + "t": "-", + "n": {lineno|json}, + "l": {line|json} + }' +difflineat = '\{ + "t": "@", + "n": {lineno|json}, + "l": {line|json} + }' +diffline = '\{ + "t": "", + "n": {lineno|json}, + "l": {line|json} + }' +filecomparison = '\{ + "path": {file|json}, + "node": {node|json}, + "date": {date|json}, + "desc": {desc|json}, + "author": {author|json}, + "parents": [{join(parent%changesetparent, ", ")}], + "children": [{join(child%changesetparent, ", ")}], + "leftnode": {leftnode|json}, + "rightnode": {rightnode|json}, + "comparison": [{join(comparison, ", ")}] + }' +comparisonblock = '\{ + "lines": [{join(lines, ", ")}] + }' +comparisonline = '\{ + "t": {type|json}, + "ln": {leftlineno|json}, + "ll": {leftline|json}, + "rn": {rightlineno|json}, + "rl": {rightline|json} + }' +fileannotate = '\{ + "abspath": {file|json}, + "node": {node|json}, + "author": {author|json}, + "date": {date|json}, + "desc": {desc|json}, + "parents": [{join(parent%changesetparent, ", ")}], + "children": [{join(child%changesetparent, ", ")}], + "permissions": {permissions|json}, + "annotate": [{join(annotate%fileannotation, ", ")}] + }' +fileannotation = '\{ + "node": {node|json}, + "author": {author|json}, + "desc": {desc|json}, + "abspath": {file|json}, + "targetline": {targetline|json}, + "line": {line|json}, + "lineno": {lineno|json}, + "revdate": {revdate|json} + }' +filelog = '"not yet implemented"' +graph = '"not yet implemented"' +helptopics = '\{ + "topics": [{join(topics%helptopicentry, ", ")}], + "earlycommands": [{join(earlycommands%helptopicentry, ", ")}], + "othercommands": [{join(othercommands%helptopicentry, ", ")}] + }' +helptopicentry = '\{ + "topic": {topic|json}, + "summary": {summary|json} + }' +help = '\{ + "topic": {topic|json}, + "rawdoc": {doc|json} + }' +filenodelink = '' +filenolink = '' diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/map-cmdline.default --- a/mercurial/templates/map-cmdline.default Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/map-cmdline.default Thu Apr 16 20:57:51 2015 -0500 @@ -58,8 +58,8 @@ user = '{label("log.user", "user: {author}")}\n' -summary = '{label("log.summary", - "summary: {desc|firstline}")}\n' +summary = '{if(desc|strip, "{label('log.summary', + 'summary: {desc|firstline}')}\n")}' ldate = '{label("log.date", "date: {date|date}")}\n' @@ -67,7 +67,7 @@ extra = '{label("ui.debug log.extra", "extra: {key}={value|stringescape}")}\n' -description = '{label("ui.note log.description", - "description:")} - {label("ui.note log.description", - "{desc|strip}")}\n\n' +description = '{if(desc|strip, "{label('ui.note log.description', + 'description:')} + {label('ui.note log.description', + '{desc|strip}')}\n\n")}' diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/bookmarks.tmpl --- a/mercurial/templates/monoblue/bookmarks.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/bookmarks.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/branches.tmpl --- a/mercurial/templates/monoblue/branches.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/branches.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/changelog.tmpl --- a/mercurial/templates/monoblue/changelog.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/changelog.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -27,7 +27,7 @@
  • branches
  • files
  • {archives%archiveentry} -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/graph.tmpl --- a/mercurial/templates/monoblue/graph.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/graph.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -27,7 +27,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/help.tmpl --- a/mercurial/templates/monoblue/help.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/help.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/helptopics.tmpl --- a/mercurial/templates/monoblue/helptopics.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/helptopics.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/manifest.tmpl --- a/mercurial/templates/monoblue/manifest.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/manifest.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/map --- a/mercurial/templates/monoblue/map Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/map Thu Apr 16 20:57:51 2015 -0500 @@ -93,7 +93,7 @@ {author|user}@{rev} + title="{node|short}: {desc|escape|firstline}">{author|user}@{rev} {linenumber} @@ -129,7 +129,7 @@
    {changesetlink}
    ' changesetparentdiff = '
    parent {rev}
    -
    {changesetlink} {ifeq(node, basenode, '(current diff)', \'({difffrom})\')}
    ' +
    {changesetlink} {ifeq(node, basenode, '(current diff)', '({difffrom})')}
    ' difffrom = 'diff' filerevbranch = '
    branch
    {name|escape}
    ' filerevparent = ' diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/shortlog.tmpl --- a/mercurial/templates/monoblue/shortlog.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/shortlog.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,8 +26,8 @@
  • bookmarks
  • branches
  • files
  • - {archives%archiveentry} -
  • help
  • + {archives%archiveentry} +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/summary.tmpl --- a/mercurial/templates/monoblue/summary.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/summary.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/monoblue/tags.tmpl --- a/mercurial/templates/monoblue/tags.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/monoblue/tags.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -26,7 +26,7 @@
  • bookmarks
  • branches
  • files
  • -
  • help
  • +
  • help
  • diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/bookmarks.tmpl --- a/mercurial/templates/paper/bookmarks.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/bookmarks.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -23,7 +23,6 @@ -

    diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/fileannotate.tmpl --- a/mercurial/templates/paper/fileannotate.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/fileannotate.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -68,10 +68,12 @@
    + + {annotate%annotateline} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/filelog.tmpl --- a/mercurial/templates/paper/filelog.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/filelog.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -35,7 +35,6 @@ -

    rev   line source
    + + {entries%filelogentry} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/graph.tmpl --- a/mercurial/templates/paper/graph.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/graph.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -28,7 +28,6 @@ -

    age author description
    + @@ -20,6 +21,7 @@ + {entries%indexentry} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/manifest.tmpl --- a/mercurial/templates/paper/manifest.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/manifest.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -39,11 +39,13 @@
    Name Description   
    + + diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/search.tmpl --- a/mercurial/templates/paper/search.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/search.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -43,11 +43,13 @@
    name size permissions
    [up]
    + + {entries} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/shortlog.tmpl --- a/mercurial/templates/paper/shortlog.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/shortlog.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -30,7 +30,6 @@ -

    age author description
    + + {entries%shortlogentry} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/paper/tags.tmpl --- a/mercurial/templates/paper/tags.tmpl Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/paper/tags.tmpl Thu Apr 16 20:57:51 2015 -0500 @@ -23,7 +23,6 @@ -

    age author description
    + + {entries%tagentry} diff -r 612ed41ae359 -r e89f909edffa mercurial/templates/static/style-paper.css --- a/mercurial/templates/static/style-paper.css Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/templates/static/style-paper.css Thu Apr 16 20:57:51 2015 -0500 @@ -60,6 +60,10 @@ border: 0; } +div.atom-logo { + margin-top: 10px; +} + .atom-logo img{ width: 14px; height: 14px; @@ -104,6 +108,9 @@ .minusline { color: #dc143c; } /* crimson */ .atline { color: purple; } +.diffstat-table { + margin-top: 1em; +} .diffstat-file { white-space: nowrap; font-size: 90%; @@ -232,8 +239,9 @@ .sourcelines > span { display: inline-block; + box-sizing: border-box; width: 100%; - padding: 1px 0px; + padding: 1px 0px 1px 5em; counter-increment: lineno; } @@ -244,8 +252,8 @@ -ms-user-select: none; user-select: none; display: inline-block; + margin-left: -5em; width: 4em; - margin-right: 1em; font-size: smaller; color: #999; text-align: right; diff -r 612ed41ae359 -r e89f909edffa mercurial/transaction.py --- a/mercurial/transaction.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/transaction.py Thu Apr 16 20:57:51 2015 -0500 @@ -83,7 +83,7 @@ class transaction(object): def __init__(self, report, opener, vfsmap, journalname, undoname=None, - after=None, createmode=None): + after=None, createmode=None, validator=None): """Begin a new transaction Begins a new transaction that allows rolling back writes in the event of @@ -107,6 +107,12 @@ self.journal = journalname self.undoname = undoname self._queue = [] + # A callback to validate transaction content before closing it. + # should raise exception is anything is wrong. + # target user is repository hooks. + if validator is None: + validator = lambda tr: None + self.validator = validator # a dict of arguments to be passed to hooks self.hookargs = {} self.file = opener.open(self.journal, "w") @@ -378,6 +384,7 @@ def close(self): '''commit the transaction''' if self.count == 1: + self.validator(self) # will raise exception if needed self._generatefiles() categories = sorted(self._finalizecallback) for cat in categories: @@ -535,6 +542,6 @@ backupentries.append((l, f, b, bool(c))) else: report(_("journal was created by a different version of " - "Mercurial")) + "Mercurial\n")) _playback(file, report, opener, vfsmap, entries, backupentries) diff -r 612ed41ae359 -r e89f909edffa mercurial/ui.py --- a/mercurial/ui.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/ui.py Thu Apr 16 20:57:51 2015 -0500 @@ -158,7 +158,7 @@ if self.plain(): for k in ('debug', 'fallbackencoding', 'quiet', 'slash', - 'logtemplate', 'style', + 'logtemplate', 'statuscopies', 'style', 'traceback', 'verbose'): if k in cfg['ui']: del cfg['ui'][k] @@ -531,10 +531,14 @@ if util.hasscheme(loc) or os.path.isdir(os.path.join(loc, '.hg')): return loc - path = self.config('paths', loc) - if not path and default is not None: - path = self.config('paths', default) - return path or loc + p = self.paths.getpath(loc, default=default) + if p: + return p.loc + return loc + + @util.propertycache + def paths(self): + return paths(self) def pushbuffer(self, error=False): """install a buffer to capture standard output of the ui object @@ -805,7 +809,7 @@ environ = {'HGUSER': user} if 'transplant_source' in extra: environ.update({'HGREVISION': hex(extra['transplant_source'])}) - for label in ('source', 'rebase_source'): + for label in ('intermediate-source', 'source', 'rebase_source'): if label in extra: environ.update({'HGREVISION': extra[label]}) break @@ -923,3 +927,48 @@ ui.write(ui.label(s, 'label')). ''' return msg + +class paths(dict): + """Represents a collection of paths and their configs. + + Data is initially derived from ui instances and the config files they have + loaded. + """ + def __init__(self, ui): + dict.__init__(self) + + for name, loc in ui.configitems('paths'): + # No location is the same as not existing. + if not loc: + continue + self[name] = path(name, rawloc=loc) + + def getpath(self, name, default=None): + """Return a ``path`` for the specified name, falling back to a default. + + Returns the first of ``name`` or ``default`` that is present, or None + if neither is present. + """ + try: + return self[name] + except KeyError: + if default is not None: + try: + return self[default] + except KeyError: + pass + + return None + +class path(object): + """Represents an individual path and its configuration.""" + + def __init__(self, name, rawloc=None): + """Construct a path from its config options. + + ``name`` is the symbolic name of the path. + ``rawloc`` is the raw location, as defined in the config. + """ + self.name = name + # We'll do more intelligent things with rawloc in the future. + self.loc = rawloc diff -r 612ed41ae359 -r e89f909edffa mercurial/unionrepo.py --- a/mercurial/unionrepo.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/unionrepo.py Thu Apr 16 20:57:51 2015 -0500 @@ -160,8 +160,11 @@ def baserevdiff(self, rev1, rev2): return filelog.filelog.revdiff(self, rev1, rev2) - def _file(self, f): - self._repo.file(f) + def iscensored(self, rev): + """Check if a revision is censored.""" + if rev <= self.repotiprev: + return filelog.filelog.iscensored(self, rev) + return self.revlog2.iscensored(rev) class unionpeer(localrepo.localpeer): def canpush(self): diff -r 612ed41ae359 -r e89f909edffa mercurial/util.h --- a/mercurial/util.h Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/util.h Thu Apr 16 20:57:51 2015 -0500 @@ -172,6 +172,22 @@ (d[3])); } +static inline int16_t getbeint16(const char *c) +{ + const unsigned char *d = (const unsigned char *)c; + + return ((d[0] << 8) | + (d[1])); +} + +static inline uint16_t getbeuint16(const char *c) +{ + const unsigned char *d = (const unsigned char *)c; + + return ((d[0] << 8) | + (d[1])); +} + static inline void putbe32(uint32_t x, char *c) { c[0] = (x >> 24) & 0xff; @@ -180,4 +196,34 @@ c[3] = (x) & 0xff; } +static inline double getbefloat64(const char *c) +{ + const unsigned char *d = (const unsigned char *)c; + double ret; + int i; + uint64_t t = 0; + for (i = 0; i < 8; i++) { + t = (t<<8) + d[i]; + } + memcpy(&ret, &t, sizeof(t)); + return ret; +} + +/* This should be kept in sync with normcasespecs in encoding.py. */ +enum normcase_spec { + NORMCASE_LOWER = -1, + NORMCASE_UPPER = 1, + NORMCASE_OTHER = 0 +}; + +#define MIN(a, b) (((a)<(b))?(a):(b)) +/* VC9 doesn't include bool and lacks stdbool.h based on my searching */ +#ifdef _MSC_VER +#define true 1 +#define false 0 +typedef unsigned char bool; +#else +#include +#endif + #endif /* _HG_UTIL_H_ */ diff -r 612ed41ae359 -r e89f909edffa mercurial/util.py --- a/mercurial/util.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/util.py Thu Apr 16 20:57:51 2015 -0500 @@ -15,7 +15,7 @@ import i18n _ = i18n._ -import error, osutil, encoding +import error, osutil, encoding, parsers import errno, shutil, sys, tempfile, traceback import re as remod import os, time, datetime, calendar, textwrap, signal, collections @@ -48,6 +48,8 @@ nlinks = platform.nlinks normpath = platform.normpath normcase = platform.normcase +normcasespec = platform.normcasespec +normcasefallback = platform.normcasefallback openhardlinks = platform.openhardlinks oslink = platform.oslink parsepatchoutput = platform.parsepatchoutput @@ -57,6 +59,7 @@ quotecommand = platform.quotecommand readpipe = platform.readpipe rename = platform.rename +removedirs = platform.removedirs samedevice = platform.samedevice samefile = platform.samefile samestat = platform.samestat @@ -359,8 +362,10 @@ def __iter__(self): return self._list.__iter__() def update(self, src): - for k in src: - self[k] = src[k] + if isinstance(src, dict): + src = src.iteritems() + for k, v in src: + self[k] = v def clear(self): dict.clear(self) self._list = [] @@ -737,20 +742,27 @@ except shutil.Error, inst: raise Abort(str(inst)) -def copyfiles(src, dst, hardlink=None): - """Copy a directory tree using hardlinks if possible""" +def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None): + """Copy a directory tree using hardlinks if possible.""" + num = 0 if hardlink is None: hardlink = (os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev) + if hardlink: + topic = _('linking') + else: + topic = _('copying') - num = 0 if os.path.isdir(src): os.mkdir(dst) for name, kind in osutil.listdir(src): srcname = os.path.join(src, name) dstname = os.path.join(dst, name) - hardlink, n = copyfiles(srcname, dstname, hardlink) + def nprog(t, pos): + if pos is not None: + return progress(t, pos + num) + hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog) num += n else: if hardlink: @@ -762,6 +774,8 @@ else: shutil.copy(src, dst) num += 1 + progress(topic, num) + progress(topic, None) return hardlink, num @@ -1352,11 +1366,11 @@ formats = defaultdateformats date = date.strip() - if date == _('now'): + if date == 'now' or date == _('now'): return makedate() - if date == _('today'): + if date == 'today' or date == _('today'): date = datetime.date.today().strftime('%b %d') - elif date == _('yesterday'): + elif date == 'yesterday' or date == _('yesterday'): date = (datetime.date.today() - datetime.timedelta(days=1)).strftime('%b %d') @@ -2227,5 +2241,50 @@ f.write(' %-*s in %s\n' % (fnmax, fnln, func)) f.flush() +class dirs(object): + '''a multiset of directory names from a dirstate or manifest''' + + def __init__(self, map, skip=None): + self._dirs = {} + addpath = self.addpath + if safehasattr(map, 'iteritems') and skip is not None: + for f, s in map.iteritems(): + if s[0] != skip: + addpath(f) + else: + for f in map: + addpath(f) + + def addpath(self, path): + dirs = self._dirs + for base in finddirs(path): + if base in dirs: + dirs[base] += 1 + return + dirs[base] = 1 + + def delpath(self, path): + dirs = self._dirs + for base in finddirs(path): + if dirs[base] > 1: + dirs[base] -= 1 + return + del dirs[base] + + def __iter__(self): + return self._dirs.iterkeys() + + def __contains__(self, d): + return d in self._dirs + +if safehasattr(parsers, 'dirs'): + dirs = parsers.dirs + +def finddirs(path): + pos = path.rfind('/') + while pos != -1: + yield path[:pos] + pos = path.rfind('/', 0, pos) + # convenient shortcut dst = debugstacktrace diff -r 612ed41ae359 -r e89f909edffa mercurial/verify.py --- a/mercurial/verify.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/verify.py Thu Apr 16 20:57:51 2015 -0500 @@ -169,7 +169,7 @@ for f, fn in mf.readdelta(n).iteritems(): if not f: err(lr, _("file without name in manifest")) - elif f != "/dev/null": + elif f != "/dev/null": # ignore this in very old repos filenodes.setdefault(_normpath(f), {}).setdefault(fn, lr) except Exception, inst: exc(lr, _("reading manifest delta %s") % short(n), inst) diff -r 612ed41ae359 -r e89f909edffa mercurial/win32.py --- a/mercurial/win32.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/win32.py Thu Apr 16 20:57:51 2015 -0500 @@ -5,7 +5,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import ctypes, errno, os, subprocess, random +import ctypes, errno, msvcrt, os, subprocess, random _kernel32 = ctypes.windll.kernel32 _advapi32 = ctypes.windll.advapi32 @@ -26,6 +26,7 @@ _ERROR_SUCCESS = 0 _ERROR_NO_MORE_FILES = 18 _ERROR_INVALID_PARAMETER = 87 +_ERROR_BROKEN_PIPE = 109 _ERROR_INSUFFICIENT_BUFFER = 122 # WPARAM is defined as UINT_PTR (unsigned type) @@ -211,6 +212,10 @@ _kernel32.CreateToolhelp32Snapshot.argtypes = [_DWORD, _DWORD] _kernel32.CreateToolhelp32Snapshot.restype = _BOOL +_kernel32.PeekNamedPipe.argtypes = [_HANDLE, ctypes.c_void_p, _DWORD, + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p] +_kernel32.PeekNamedPipe.restype = _BOOL + _kernel32.Process32First.argtypes = [_HANDLE, ctypes.c_void_p] _kernel32.Process32First.restype = _BOOL @@ -260,6 +265,19 @@ res2 = _getfileinfo(path2) return res1.dwVolumeSerialNumber == res2.dwVolumeSerialNumber +def peekpipe(pipe): + handle = msvcrt.get_osfhandle(pipe.fileno()) + avail = _DWORD() + + if not _kernel32.PeekNamedPipe(handle, None, 0, None, ctypes.byref(avail), + None): + err = _kernel32.GetLastError() + if err == _ERROR_BROKEN_PIPE: + return 0 + raise ctypes.WinError(err) + + return avail.value + def testpid(pid): '''return True if pid is still running or unable to determine, False otherwise''' @@ -279,7 +297,7 @@ buf = ctypes.create_string_buffer(size + 1) len = _kernel32.GetModuleFileNameA(None, ctypes.byref(buf), size) if len == 0: - raise ctypes.WinError() + raise ctypes.WinError() # Note: WinError is a function elif len == size: raise ctypes.WinError(_ERROR_INSUFFICIENT_BUFFER) return buf.value diff -r 612ed41ae359 -r e89f909edffa mercurial/windows.py --- a/mercurial/windows.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/windows.py Thu Apr 16 20:57:51 2015 -0500 @@ -26,14 +26,22 @@ unlink = win32.unlink umask = 0022 +_SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5 -# wrap osutil.posixfile to provide friendlier exceptions def posixfile(name, mode='r', buffering=-1): + '''Open a file with even more POSIX-like semantics''' try: - return osutil.posixfile(name, mode, buffering) + fp = osutil.posixfile(name, mode, buffering) # may raise WindowsError + + # The position when opening in append mode is implementation defined, so + # make it consistent with other platforms, which position at EOF. + if 'a' in mode: + fp.seek(0, _SEEK_END) + + return fp except WindowsError, err: + # convert to a friendlier exception raise IOError(err.errno, '%s: %s' % (name, err.strerror)) -posixfile.__doc__ = osutil.posixfile.__doc__ class winstdout(object): '''stdout on windows misbehaves if sent through a pipe''' @@ -133,6 +141,10 @@ def normcase(path): return encoding.upper(path) +# see posix.py for definitions +normcasespec = encoding.normcasespecs.upper +normcasefallback = encoding.upperfallback + def samestat(s1, s2): return False @@ -258,7 +270,7 @@ If gid is None, return the name of the current group.""" return None -def _removedirs(name): +def removedirs(name): """special version of os.removedirs that does not remove symlinked directories or junction points if they actually contain files""" if osutil.listdir(name): @@ -285,7 +297,7 @@ raise # try removing directories that might now be empty try: - _removedirs(os.path.dirname(f)) + removedirs(os.path.dirname(f)) except OSError: pass @@ -351,7 +363,7 @@ """Read all available data from a pipe.""" chunks = [] while True: - size = os.fstat(pipe.fileno()).st_size + size = win32.peekpipe(pipe) if not size: break diff -r 612ed41ae359 -r e89f909edffa mercurial/wireproto.py --- a/mercurial/wireproto.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/wireproto.py Thu Apr 16 20:57:51 2015 -0500 @@ -363,8 +363,10 @@ opts[key] = value f = self._callcompressable("getbundle", **opts) bundlecaps = kwargs.get('bundlecaps') - if bundlecaps is not None and 'HG2Y' in bundlecaps: - return bundle2.unbundle20(self.ui, f) + if bundlecaps is None: + bundlecaps = () # kwargs could have it to None + if util.any((cap.startswith('HG2') for cap in bundlecaps)): + return bundle2.getunbundler(self.ui, f) else: return changegroupmod.cg1unpacker(f, 'UN') @@ -401,7 +403,7 @@ else: # bundle2 push. Send a stream, fetch a stream. stream = self._calltwowaystream('unbundle', cg, heads=heads) - ret = bundle2.unbundle20(self.ui, stream) + ret = bundle2.getunbundler(self.ui, stream) return ret def debugwireargs(self, one, two, three=None, four=None, five=None): @@ -613,9 +615,9 @@ # otherwise, add 'streamreqs' detailing our local revlog format else: caps.append('streamreqs=%s' % ','.join(requiredformats)) - if repo.ui.configbool('experimental', 'bundle2-exp', False): + if repo.ui.configbool('experimental', 'bundle2-advertise', True): capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo)) - caps.append('bundle2-exp=' + urllib.quote(capsblob)) + caps.append('bundle2=' + urllib.quote(capsblob)) caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority)) caps.append('httpheader=1024') return caps @@ -839,35 +841,40 @@ finally: fp.close() os.unlink(tempname) - except error.BundleValueError, exc: - bundler = bundle2.bundle20(repo.ui) - errpart = bundler.newpart('b2x:error:unsupportedcontent') + + except (error.BundleValueError, util.Abort, error.PushRaced), exc: + # handle non-bundle2 case first + if not getattr(exc, 'duringunbundle2', False): + try: + raise + except util.Abort: + # The old code we moved used sys.stderr directly. + # We did not change it to minimise code change. + # This need to be moved to something proper. + # Feel free to do it. + sys.stderr.write("abort: %s\n" % exc) + return pushres(0) + except error.PushRaced: + return pusherr(str(exc)) + + bundler = bundle2.bundle20(repo.ui) + for out in getattr(exc, '_bundle2salvagedoutput', ()): + bundler.addpart(out) + try: + raise + except error.BundleValueError, exc: + errpart = bundler.newpart('error:unsupportedcontent') if exc.parttype is not None: errpart.addparam('parttype', exc.parttype) if exc.params: errpart.addparam('params', '\0'.join(exc.params)) - return streamres(bundler.getchunks()) - except util.Abort, inst: - # The old code we moved used sys.stderr directly. - # We did not change it to minimise code change. - # This need to be moved to something proper. - # Feel free to do it. - if getattr(inst, 'duringunbundle2', False): - bundler = bundle2.bundle20(repo.ui) - manargs = [('message', str(inst))] + except util.Abort, exc: + manargs = [('message', str(exc))] advargs = [] - if inst.hint is not None: - advargs.append(('hint', inst.hint)) - bundler.addpart(bundle2.bundlepart('b2x:error:abort', + if exc.hint is not None: + advargs.append(('hint', exc.hint)) + bundler.addpart(bundle2.bundlepart('error:abort', manargs, advargs)) - return streamres(bundler.getchunks()) - else: - sys.stderr.write("abort: %s\n" % inst) - return pushres(0) - except error.PushRaced, exc: - if getattr(exc, 'duringunbundle2', False): - bundler = bundle2.bundle20(repo.ui) - bundler.newpart('b2x:error:pushraced', [('message', str(exc))]) - return streamres(bundler.getchunks()) - else: - return pusherr(str(exc)) + except error.PushRaced, exc: + bundler.newpart('error:pushraced', [('message', str(exc))]) + return streamres(bundler.getchunks()) diff -r 612ed41ae359 -r e89f909edffa setup.py --- a/setup.py Thu Apr 16 22:33:53 2015 +0900 +++ b/setup.py Thu Apr 16 20:57:51 2015 -0500 @@ -63,6 +63,8 @@ raise SystemExit( "Couldn't import standard bz2 (incomplete Python install).") +ispypy = "PyPy" in sys.version + import os, stat, subprocess, time import re import shutil @@ -276,7 +278,7 @@ class hgdist(Distribution): - pure = 0 + pure = ispypy global_options = Distribution.global_options + \ [('pure', None, "use pure (slow) Python " @@ -491,6 +493,7 @@ Extension('mercurial.mpatch', ['mercurial/mpatch.c'], depends=common_depends), Extension('mercurial.parsers', ['mercurial/dirs.c', + 'mercurial/manifest.c', 'mercurial/parsers.c', 'mercurial/pathencode.c'], depends=common_depends), @@ -555,7 +558,7 @@ if py2exeloaded: extra['console'] = [ {'script':'hg', - 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others', + 'copyright':'Copyright (C) 2005-2015 Matt Mackall and others', 'product_version':version}] # sub command of 'build' because 'py2exe' does not handle sub_commands build.sub_commands.insert(0, ('build_hgextindex', None)) diff -r 612ed41ae359 -r e89f909edffa tests/get-with-headers.py --- a/tests/get-with-headers.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/get-with-headers.py Thu Apr 16 20:57:51 2015 -0500 @@ -6,6 +6,14 @@ import httplib, sys try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + json = None + +try: import msvcrt, os msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY) @@ -20,6 +28,10 @@ if '--headeronly' in sys.argv: sys.argv.remove('--headeronly') headeronly = True +formatjson = False +if '--json' in sys.argv: + sys.argv.remove('--json') + formatjson = True reasons = {'Not modified': 'Not Modified'} # python 2.4 @@ -44,7 +56,23 @@ if not headeronly: print data = response.read() - sys.stdout.write(data) + + # Pretty print JSON. This also has the beneficial side-effect + # of verifying emitted JSON is well-formed. + if formatjson: + if not json: + print 'no json module not available' + print 'did you forget a #require json?' + sys.exit(1) + + # json.dumps() will print trailing newlines. Eliminate them + # to make tests easier to write. + data = json.loads(data) + lines = json.dumps(data, sort_keys=True, indent=2).splitlines() + for line in lines: + print line.rstrip() + else: + sys.stdout.write(data) if twice and response.getheader('ETag', None): tag = response.getheader('ETag') diff -r 612ed41ae359 -r e89f909edffa tests/hghave.py --- a/tests/hghave.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/hghave.py Thu Apr 16 20:57:51 2015 -0500 @@ -320,6 +320,11 @@ except ImportError: return False +@check("defaultcacerts", "can verify SSL certs by system's CA certs store") +def has_defaultcacerts(): + from mercurial import sslutil + return sslutil._defaultcacerts() != '!' + @check("windows", "Windows") def has_windows(): return os.name == 'nt' diff -r 612ed41ae359 -r e89f909edffa tests/mockblackbox.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/mockblackbox.py Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,11 @@ +from mercurial import util + +def makedate(): + return 0, 0 +def getuser(): + return 'bob' + +# mock the date and user apis so the output is always the same +def uisetup(ui): + util.makedate = makedate + util.getuser = getuser diff -r 612ed41ae359 -r e89f909edffa tests/run-tests.py --- a/tests/run-tests.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/run-tests.py Thu Apr 16 20:57:51 2015 -0500 @@ -76,6 +76,8 @@ if sys.version_info < (2, 5): subprocess._cleanup = lambda: None +wifexited = getattr(os, "WIFEXITED", lambda x: False) + closefds = os.name == 'posix' def Popen4(cmd, wd, timeout, env=None): processlock.acquire() @@ -170,6 +172,8 @@ help="shortcut for --with-hg=/../hg") parser.add_option("--loop", action="store_true", help="loop tests repeatedly") + parser.add_option("--runs-per-test", type="int", dest="runs_per_test", + help="run each test N times (default=1)", default=1) parser.add_option("-n", "--nodiff", action="store_true", help="skip showing test changes") parser.add_option("-p", "--port", type="int", @@ -258,6 +262,10 @@ parser.error("sorry, coverage options do not work when --local " "is specified") + if options.anycoverage and options.with_hg: + parser.error("sorry, coverage options do not work when --with-hg " + "is specified") + global verbose if options.verbose: verbose = '' @@ -459,7 +467,14 @@ # Remove any previous output files. if os.path.exists(self.errpath): - os.remove(self.errpath) + try: + os.remove(self.errpath) + except OSError, e: + # We might have raced another test to clean up a .err + # file, so ignore ENOENT when removing a previous .err + # file. + if e.errno != errno.ENOENT: + raise def run(self, result): """Run this test and report results against a TestResult instance.""" @@ -528,14 +543,13 @@ This will return a tuple describing the result of the test. """ - replacements = self._getreplacements() env = self._getenv() self._daemonpids.append(env['DAEMON_PIDS']) self._createhgrc(env['HGRCPATH']) vlog('# Test', self.name) - ret, out = self._run(replacements, env) + ret, out = self._run(env) self._finished = True self._ret = ret self._out = out @@ -608,7 +622,7 @@ vlog("# Ret was:", self._ret) - def _run(self, replacements, env): + def _run(self, env): # This should be implemented in child classes to run tests. raise SkipTest('unknown test type') @@ -691,6 +705,8 @@ hgrc.write('commit = -d "0 0"\n') hgrc.write('shelve = --date "0 0"\n') hgrc.write('tag = -d "0 0"\n') + hgrc.write('[devel]\n') + hgrc.write('all = true\n') hgrc.write('[largefiles]\n') hgrc.write('usercache = %s\n' % (os.path.join(self._testtmp, '.cache/largefiles'))) @@ -707,6 +723,55 @@ # Failed is denoted by AssertionError (by default at least). raise AssertionError(msg) + def _runcommand(self, cmd, env, normalizenewlines=False): + """Run command in a sub-process, capturing the output (stdout and + stderr). + + Return a tuple (exitcode, output). output is None in debug mode. + """ + if self._debug: + proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp, + env=env) + ret = proc.wait() + return (ret, None) + + proc = Popen4(cmd, self._testtmp, self._timeout, env) + def cleanup(): + terminate(proc) + ret = proc.wait() + if ret == 0: + ret = signal.SIGTERM << 8 + killdaemons(env['DAEMON_PIDS']) + return ret + + output = '' + proc.tochild.close() + + try: + output = proc.fromchild.read() + except KeyboardInterrupt: + vlog('# Handling keyboard interrupt') + cleanup() + raise + + ret = proc.wait() + if wifexited(ret): + ret = os.WEXITSTATUS(ret) + + if proc.timeout: + ret = 'timeout' + + if ret: + killdaemons(env['DAEMON_PIDS']) + + for s, r in self._getreplacements(): + output = re.sub(s, r, output) + + if normalizenewlines: + output = output.replace('\r\n', '\n') + + return ret, output.splitlines(True) + class PythonTest(Test): """A Python-based test.""" @@ -714,14 +779,13 @@ def refpath(self): return os.path.join(self._testdir, '%s.out' % self.name) - def _run(self, replacements, env): + def _run(self, env): py3kswitch = self._py3kwarnings and ' -3' or '' cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path) vlog("# Running", cmd) - if os.name == 'nt': - replacements.append((r'\r\n', '\n')) - result = run(cmd, self._testtmp, replacements, env, - debug=self._debug, timeout=self._timeout) + normalizenewlines = os.name == 'nt' + result = self._runcommand(cmd, env, + normalizenewlines=normalizenewlines) if self._aborted: raise KeyboardInterrupt() @@ -751,7 +815,7 @@ def refpath(self): return os.path.join(self._testdir, self.name) - def _run(self, replacements, env): + def _run(self, env): f = open(self.path, 'rb') lines = f.readlines() f.close() @@ -768,8 +832,7 @@ cmd = '%s "%s"' % (self._shell, fname) vlog("# Running", cmd) - exitcode, output = run(cmd, self._testtmp, replacements, env, - debug=self._debug, timeout=self._timeout) + exitcode, output = self._runcommand(cmd, env) if self._aborted: raise KeyboardInterrupt() @@ -1062,49 +1125,6 @@ def _stringescape(s): return TTest.ESCAPESUB(TTest._escapef, s) - -wifexited = getattr(os, "WIFEXITED", lambda x: False) -def run(cmd, wd, replacements, env, debug=False, timeout=None): - """Run command in a sub-process, capturing the output (stdout and stderr). - Return a tuple (exitcode, output). output is None in debug mode.""" - if debug: - proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env) - ret = proc.wait() - return (ret, None) - - proc = Popen4(cmd, wd, timeout, env) - def cleanup(): - terminate(proc) - ret = proc.wait() - if ret == 0: - ret = signal.SIGTERM << 8 - killdaemons(env['DAEMON_PIDS']) - return ret - - output = '' - proc.tochild.close() - - try: - output = proc.fromchild.read() - except KeyboardInterrupt: - vlog('# Handling keyboard interrupt') - cleanup() - raise - - ret = proc.wait() - if wifexited(ret): - ret = os.WEXITSTATUS(ret) - - if proc.timeout: - ret = 'timeout' - - if ret: - killdaemons(env['DAEMON_PIDS']) - - for s, r in replacements: - output = re.sub(s, r, output) - return ret, output.splitlines(True) - iolock = threading.RLock() class SkipTest(Exception): @@ -1140,8 +1160,6 @@ self.warned = [] self.times = [] - self._started = {} - self._stopped = {} # Data stored for the benefit of generating xunit reports. self.successes = [] self.faildata = {} @@ -1263,21 +1281,18 @@ # child's processes along with real elapsed time taken by a process. # This module has one limitation. It can only work for Linux user # and not for Windows. - self._started[test.name] = os.times() + test.started = os.times() def stopTest(self, test, interrupted=False): super(TestResult, self).stopTest(test) - self._stopped[test.name] = os.times() + test.stopped = os.times() - starttime = self._started[test.name] - endtime = self._stopped[test.name] + starttime = test.started + endtime = test.stopped self.times.append((test.name, endtime[2] - starttime[2], endtime[3] - starttime[3], endtime[4] - starttime[4])) - del self._started[test.name] - del self._stopped[test.name] - if interrupted: iolock.acquire() self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % ( @@ -1288,7 +1303,8 @@ """Custom unittest TestSuite that knows how to execute Mercurial tests.""" def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None, - retest=False, keywords=None, loop=False, + retest=False, keywords=None, loop=False, runs_per_test=1, + loadtest=None, *args, **kwargs): """Create a new instance that can run tests with a configuration. @@ -1323,13 +1339,21 @@ self._retest = retest self._keywords = keywords self._loop = loop + self._runs_per_test = runs_per_test + self._loadtest = loadtest def run(self, result): # We have a number of filters that need to be applied. We do this # here instead of inside Test because it makes the running logic for # Test simpler. tests = [] + num_tests = [0] for test in self._tests: + def get(): + num_tests[0] += 1 + if getattr(test, 'should_reload', False): + return self._loadtest(test.name, num_tests[0]) + return test if not os.path.exists(test.path): result.addSkip(test, "Doesn't exist") continue @@ -1356,8 +1380,8 @@ if ignored: continue - - tests.append(test) + for _ in xrange(self._runs_per_test): + tests.append(get()) runtests = list(tests) done = queue.Queue() @@ -1373,24 +1397,44 @@ done.put(('!', test, 'run-test raised an error, see traceback')) raise + stoppedearly = False + try: while tests or running: if not done.empty() or running == self._jobs or not tests: try: done.get(True, 1) + running -= 1 if result and result.shouldStop: + stoppedearly = True break except queue.Empty: continue - running -= 1 if tests and not running == self._jobs: test = tests.pop(0) if self._loop: - tests.append(test) + if getattr(test, 'should_reload', False): + num_tests[0] += 1 + tests.append( + self._loadtest(test.name, num_tests[0])) + else: + tests.append(test) t = threading.Thread(target=job, name=test.name, args=(test, result)) t.start() running += 1 + + # If we stop early we still need to wait on started tests to + # finish. Otherwise, there is a race between the test completing + # and the test's cleanup code running. This could result in the + # test reporting incorrect. + if stoppedearly: + while running: + try: + done.get(True, 1) + running -= 1 + except queue.Empty: + continue except KeyboardInterrupt: for test in runtests: test.abort() @@ -1451,7 +1495,11 @@ t = doc.createElement('testcase') t.setAttribute('name', tc) t.setAttribute('time', '%.3f' % timesd[tc]) - cd = doc.createCDATASection(cdatasafe(err)) + # createCDATASection expects a unicode or it will convert + # using default conversion rules, which will fail if + # string isn't ASCII. + err = cdatasafe(err).decode('utf-8', 'replace') + cd = doc.createCDATASection(err) t.appendChild(cd) s.appendChild(t) xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8')) @@ -1545,6 +1593,7 @@ def __init__(self): self.options = None + self._hgroot = None self._testdir = None self._hgtmp = None self._installdir = None @@ -1646,6 +1695,11 @@ runtestdir = os.path.abspath(os.path.dirname(__file__)) path = [self._bindir, runtestdir] + os.environ["PATH"].split(os.pathsep) + if os.path.islink(__file__): + # test helper will likely be at the end of the symlink + realfile = os.path.realpath(__file__) + realdir = os.path.abspath(os.path.dirname(realfile)) + path.insert(2, realdir) if self._tmpbindir != self._bindir: path = [self._tmpbindir] + path os.environ["PATH"] = os.pathsep.join(path) @@ -1729,7 +1783,8 @@ retest=self.options.retest, keywords=self.options.keywords, loop=self.options.loop, - tests=tests) + runs_per_test=self.options.runs_per_test, + tests=tests, loadtest=self._gettest) verbosity = 1 if self.options.verbose: verbosity = 2 @@ -1769,14 +1824,16 @@ refpath = os.path.join(self._testdir, test) tmpdir = os.path.join(self._hgtmp, 'child%d' % count) - return testcls(refpath, tmpdir, - keeptmpdir=self.options.keep_tmpdir, - debug=self.options.debug, - timeout=self.options.timeout, - startport=self.options.port + count * 3, - extraconfigopts=self.options.extra_config_opt, - py3kwarnings=self.options.py3k_warnings, - shell=self.options.shell) + t = testcls(refpath, tmpdir, + keeptmpdir=self.options.keep_tmpdir, + debug=self.options.debug, + timeout=self.options.timeout, + startport=self.options.port + count * 3, + extraconfigopts=self.options.extra_config_opt, + py3kwarnings=self.options.py3k_warnings, + shell=self.options.shell) + t.should_reload = True + return t def _cleanup(self): """Clean up state from this test invocation.""" @@ -1836,7 +1893,10 @@ compiler = '' if self.options.compiler: compiler = '--compiler ' + self.options.compiler - pure = self.options.pure and "--pure" or "" + if self.options.pure: + pure = "--pure" + else: + pure = "" py3 = '' if sys.version_info[0] == 3: py3 = '--c2to3' @@ -1844,6 +1904,7 @@ # Run installer in hg root script = os.path.realpath(sys.argv[0]) hgroot = os.path.dirname(os.path.dirname(script)) + self._hgroot = hgroot os.chdir(hgroot) nohome = '--home=""' if os.name == 'nt': @@ -1863,6 +1924,17 @@ 'prefix': self._installdir, 'libdir': self._pythondir, 'bindir': self._bindir, 'nohome': nohome, 'logfile': installerrs}) + + # setuptools requires install directories to exist. + def makedirs(p): + try: + os.makedirs(p) + except OSError, e: + if e.errno != errno.EEXIST: + raise + makedirs(self._pythondir) + makedirs(self._bindir) + vlog("# Running", cmd) if os.system(cmd) == 0: if not self.options.verbose: @@ -1870,7 +1942,7 @@ else: f = open(installerrs, 'rb') for line in f: - print line + sys.stdout.write(line) f.close() sys.exit(1) os.chdir(self._testdir) @@ -1912,8 +1984,14 @@ rc = os.path.join(self._testdir, '.coveragerc') vlog('# Installing coverage rc to %s' % rc) os.environ['COVERAGE_PROCESS_START'] = rc - fn = os.path.join(self._installdir, '..', '.coverage') - os.environ['COVERAGE_FILE'] = fn + covdir = os.path.join(self._installdir, '..', 'coverage') + try: + os.mkdir(covdir) + except OSError, e: + if e.errno != errno.EEXIST: + raise + + os.environ['COVERAGE_DIR'] = covdir def _checkhglib(self, verb): """Ensure that the 'mercurial' package imported by python is @@ -1946,27 +2024,31 @@ def _outputcoverage(self): """Produce code coverage output.""" + from coverage import coverage + vlog('# Producing coverage report') - os.chdir(self._pythondir) + # chdir is the easiest way to get short, relative paths in the + # output. + os.chdir(self._hgroot) + covdir = os.path.join(self._installdir, '..', 'coverage') + cov = coverage(data_file=os.path.join(covdir, 'cov')) - def covrun(*args): - cmd = 'coverage %s' % ' '.join(args) - vlog('# Running: %s' % cmd) - os.system(cmd) + # Map install directory paths back to source directory. + cov.config.paths['srcdir'] = ['.', self._pythondir] - covrun('-c') - omit = ','.join(os.path.join(x, '*') for x in - [self._bindir, self._testdir]) - covrun('-i', '-r', '"--omit=%s"' % omit) # report + cov.combine() + + omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]] + cov.report(ignore_errors=True, omit=omit) + if self.options.htmlcov: htmldir = os.path.join(self._testdir, 'htmlcov') - covrun('-i', '-b', '"--directory=%s"' % htmldir, - '"--omit=%s"' % omit) + cov.html_report(directory=htmldir, omit=omit) if self.options.annotate: adir = os.path.join(self._testdir, 'annotated') if not os.path.isdir(adir): os.mkdir(adir) - covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) + cov.annotate(directory=adir, omit=omit) def _findprogram(self, program): """Search PATH for a executable program""" diff -r 612ed41ae359 -r e89f909edffa tests/seq.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/seq.py Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# +# A portable replacement for 'seq' +# +# Usage: +# seq STOP [1, STOP] stepping by 1 +# seq START STOP [START, STOP] stepping by 1 +# seq START STEP STOP [START, STOP] stepping by STEP + +import sys + +start = 1 +if len(sys.argv) > 2: + start = int(sys.argv[1]) + +step = 1 +if len(sys.argv) > 3: + step = int(sys.argv[2]) + +stop = int(sys.argv[-1]) + 1 + +for i in xrange(start, stop, step): + print i diff -r 612ed41ae359 -r e89f909edffa tests/sitecustomize.py --- a/tests/sitecustomize.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/sitecustomize.py Thu Apr 16 20:57:51 2015 -0500 @@ -1,5 +1,16 @@ -try: - import coverage - getattr(coverage, 'process_startup', lambda: None)() -except ImportError: - pass +import os + +if os.environ.get('COVERAGE_PROCESS_START'): + try: + import coverage + import random + + # uuid is better, but not available in Python 2.4. + covpath = os.path.join(os.environ['COVERAGE_DIR'], + 'cov.%s' % random.randrange(0, 1000000000000)) + cov = coverage.coverage(data_file=covpath, auto_data=True) + cov._warn_no_data = False + cov._warn_unimported_source = False + cov.start() + except ImportError: + pass diff -r 612ed41ae359 -r e89f909edffa tests/test-add.t --- a/tests/test-add.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-add.t Thu Apr 16 20:57:51 2015 -0500 @@ -176,12 +176,48 @@ $ mkdir CapsDir1/CapsDir/SubDir $ echo def > CapsDir1/CapsDir/SubDir/Def.txt - $ hg add -v capsdir1/capsdir + $ hg add capsdir1/capsdir adding CapsDir1/CapsDir/AbC.txt (glob) adding CapsDir1/CapsDir/SubDir/Def.txt (glob) $ hg forget capsdir1/capsdir/abc.txt removing CapsDir1/CapsDir/AbC.txt (glob) + + $ hg forget capsdir1/capsdir + removing CapsDir1/CapsDir/SubDir/Def.txt (glob) + + $ hg add capsdir1 + adding CapsDir1/CapsDir/AbC.txt (glob) + adding CapsDir1/CapsDir/SubDir/Def.txt (glob) + + $ hg ci -m "AbCDef" capsdir1/capsdir + + $ hg status -A capsdir1/capsdir + C CapsDir1/CapsDir/AbC.txt + C CapsDir1/CapsDir/SubDir/Def.txt + + $ hg files capsdir1/capsdir + CapsDir1/CapsDir/AbC.txt (glob) + CapsDir1/CapsDir/SubDir/Def.txt (glob) + + $ echo xyz > CapsDir1/CapsDir/SubDir/Def.txt + $ hg ci -m xyz capsdir1/capsdir/subdir/def.txt + + $ hg revert -r '.^' capsdir1/capsdir + reverting CapsDir1/CapsDir/SubDir/Def.txt (glob) + + $ hg diff capsdir1/capsdir + diff -r 5112e00e781d CapsDir1/CapsDir/SubDir/Def.txt + --- a/CapsDir1/CapsDir/SubDir/Def.txt Thu Jan 01 00:00:00 1970 +0000 + +++ b/CapsDir1/CapsDir/SubDir/Def.txt * +0000 (glob) + @@ -1,1 +1,1 @@ + -xyz + +def + + $ hg remove -f 'glob:**.txt' -X capsdir1/capsdir + $ hg remove -f 'glob:**.txt' -I capsdir1/capsdir + removing CapsDir1/CapsDir/AbC.txt (glob) + removing CapsDir1/CapsDir/SubDir/Def.txt (glob) #endif $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-addremove.t --- a/tests/test-addremove.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-addremove.t Thu Apr 16 20:57:51 2015 -0500 @@ -30,12 +30,12 @@ adding foo $ hg forget foo #if windows - $ hg -v addremove nonexistant - nonexistant: The system cannot find the file specified + $ hg -v addremove nonexistent + nonexistent: The system cannot find the file specified [1] #else - $ hg -v addremove nonexistant - nonexistant: No such file or directory + $ hg -v addremove nonexistent + nonexistent: No such file or directory [1] #endif $ cd .. @@ -88,13 +88,13 @@ $ rm c #if windows - $ hg ci -A -m "c" nonexistant - nonexistant: The system cannot find the file specified + $ hg ci -A -m "c" nonexistent + nonexistent: The system cannot find the file specified abort: failed to mark all new/missing files as added/removed [255] #else - $ hg ci -A -m "c" nonexistant - nonexistant: No such file or directory + $ hg ci -A -m "c" nonexistent + nonexistent: No such file or directory abort: failed to mark all new/missing files as added/removed [255] #endif diff -r 612ed41ae359 -r e89f909edffa tests/test-alias.t --- a/tests/test-alias.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-alias.t Thu Apr 16 20:57:51 2015 -0500 @@ -360,9 +360,11 @@ sub $ hg --cwd .. subalias > /dev/null hg: unknown command 'subalias' + (did you mean one of idalias?) [255] $ hg -R .. subalias > /dev/null hg: unknown command 'subalias' + (did you mean one of idalias?) [255] @@ -370,12 +372,18 @@ $ hg mainalias > /dev/null hg: unknown command 'mainalias' + (did you mean one of idalias?) [255] $ hg -R .. mainalias main $ hg --cwd .. mainalias main +typos get useful suggestions + $ hg --cwd .. manalias + hg: unknown command 'manalias' + (did you mean one of idalias, mainalias, manifest?) + [255] shell aliases with escaped $ chars diff -r 612ed41ae359 -r e89f909edffa tests/test-annotate.t --- a/tests/test-annotate.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-annotate.t Thu Apr 16 20:57:51 2015 -0500 @@ -398,6 +398,88 @@ 20: 4 baz:4 16: 5 +annotate clean file + + $ hg annotate -ncr "wdir()" foo + 11 472b18db256d : foo + +annotate modified file + + $ echo foofoo >> foo + $ hg annotate -r "wdir()" foo + 11 : foo + 20+: foofoo + + $ hg annotate -cr "wdir()" foo + 472b18db256d : foo + b6bedd5477e7+: foofoo + + $ hg annotate -ncr "wdir()" foo + 11 472b18db256d : foo + 20 b6bedd5477e7+: foofoo + + $ hg annotate --debug -ncr "wdir()" foo + 11 472b18db256d1e8282064eab4bfdaf48cbfe83cd : foo + 20 b6bedd5477e797f25e568a6402d4697f3f895a72+: foofoo + + $ hg annotate -udr "wdir()" foo + test Thu Jan 01 00:00:00 1970 +0000: foo + test [A-Za-z0-9:+ ]+: foofoo (re) + + $ hg annotate -ncr "wdir()" -Tjson foo + [ + { + "line": "foo\n", + "node": "472b18db256d1e8282064eab4bfdaf48cbfe83cd", + "rev": 11 + }, + { + "line": "foofoo\n", + "node": null, + "rev": null + } + ] + +annotate added file + + $ echo bar > bar + $ hg add bar + $ hg annotate -ncr "wdir()" bar + 20 b6bedd5477e7+: bar + +annotate renamed file + + $ hg rename foo renamefoo2 + $ hg annotate -ncr "wdir()" renamefoo2 + 11 472b18db256d : foo + 20 b6bedd5477e7+: foofoo + +annotate missing file + + $ rm baz +#if windows + $ hg annotate -ncr "wdir()" baz + abort: $TESTTMP\repo\baz: The system cannot find the file specified + [255] +#else + $ hg annotate -ncr "wdir()" baz + abort: No such file or directory: $TESTTMP/repo/baz + [255] +#endif + +annotate removed file + + $ hg rm baz +#if windows + $ hg annotate -ncr "wdir()" baz + abort: $TESTTMP\repo\baz: The system cannot find the file specified + [255] +#else + $ hg annotate -ncr "wdir()" baz + abort: No such file or directory: $TESTTMP/repo/baz + [255] +#endif + Test annotate with whitespace options $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-basic.t --- a/tests/test-basic.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-basic.t Thu Apr 16 20:57:51 2015 -0500 @@ -5,6 +5,7 @@ defaults.commit=-d "0 0" defaults.shelve=--date "0 0" defaults.tag=-d "0 0" + devel.all=true largefiles.usercache=$TESTTMP/.cache/largefiles (glob) ui.slash=True ui.interactive=False diff -r 612ed41ae359 -r e89f909edffa tests/test-blackbox.t --- a/tests/test-blackbox.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-blackbox.t Thu Apr 16 20:57:51 2015 -0500 @@ -1,20 +1,8 @@ setup - $ cat > mock.py < from mercurial import util - > - > def makedate(): - > return 0, 0 - > def getuser(): - > return 'bob' - > # mock the date and user apis so the output is always the same - > def uisetup(ui): - > util.makedate = makedate - > util.getuser = getuser - > EOF $ cat >> $HGRCPATH < [extensions] > blackbox= - > mock=`pwd`/mock.py + > mock=$TESTDIR/mockblackbox.py > mq= > EOF $ hg init blackboxtest @@ -124,18 +112,6 @@ 1970/01/01 00:00:00 bob> wrote base branch cache with 1 labels and 2 nodes 1970/01/01 00:00:00 bob> strip tip exited 0 after * seconds (glob) -tags cache gets logged - $ hg up tip - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg tag -m 'create test tag' test-tag - $ hg tags - tip 3:5b5562c08298 - test-tag 2:d02f48003e62 - $ hg blackbox -l 3 - 1970/01/01 00:00:00 bob> resolved 1 tags cache entries from 1 manifests in ?.???? seconds (glob) - 1970/01/01 00:00:00 bob> writing tags cache file with 2 heads and 1 tags - 1970/01/01 00:00:00 bob> tags exited 0 after ?.?? seconds (glob) - extension and python hooks - use the eol extension for a pythonhook $ echo '[extensions]' >> .hg/hgrc @@ -144,9 +120,10 @@ $ echo 'update = echo hooked' >> .hg/hgrc $ hg update hooked - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ hg blackbox -l 4 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg blackbox -l 5 1970/01/01 00:00:00 bob> update + 1970/01/01 00:00:00 bob> writing .hg/cache/tags2-visible with 0 tags 1970/01/01 00:00:00 bob> pythonhook-preupdate: hgext.eol.preupdate finished in * seconds (glob) 1970/01/01 00:00:00 bob> exthook-update: echo hooked finished in * seconds (glob) 1970/01/01 00:00:00 bob> update exited 0 after * seconds (glob) @@ -160,7 +137,7 @@ $ hg status $ hg status $ hg tip -q - 3:5b5562c08298 + 2:d02f48003e62 $ ls .hg/blackbox.log* .hg/blackbox.log .hg/blackbox.log.1 diff -r 612ed41ae359 -r e89f909edffa tests/test-bookmarks-pushpull.t --- a/tests/test-bookmarks-pushpull.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bookmarks-pushpull.t Thu Apr 16 20:57:51 2015 -0500 @@ -164,6 +164,40 @@ Z 2:0d2164f0ce0d foo -1:000000000000 * foobar 1:9b140be10808 + +(test that too many divergence of bookmark) + + $ python $TESTDIR/seq.py 1 100 | while read i; do hg bookmarks -r 000000000000 "X@${i}"; done + $ hg pull ../a + pulling from ../a + searching for changes + no changes found + warning: failed to assign numbered name to divergent bookmark X + divergent bookmark @ stored as @1 + $ hg bookmarks | grep '^ X' | grep -v ':000000000000' + X 1:9b140be10808 + X@foo 2:0d2164f0ce0d + +(test that remotely diverged bookmarks are reused if they aren't changed) + + $ hg bookmarks | grep '^ @' + @ 1:9b140be10808 + @1 2:0d2164f0ce0d + @foo 2:0d2164f0ce0d + $ hg pull ../a + pulling from ../a + searching for changes + no changes found + warning: failed to assign numbered name to divergent bookmark X + divergent bookmark @ stored as @1 + $ hg bookmarks | grep '^ @' + @ 1:9b140be10808 + @1 2:0d2164f0ce0d + @foo 2:0d2164f0ce0d + + $ python $TESTDIR/seq.py 1 100 | while read i; do hg bookmarks -d "X@${i}"; done + $ hg bookmarks -d "@1" + $ hg push -f ../a pushing to ../a searching for changes @@ -368,8 +402,11 @@ $ hg out -B http://localhost:$HGPORT/ comparing with http://localhost:$HGPORT/ searching for changed bookmarks - no changed bookmarks found - [1] + @ 0d2164f0ce0d + X 0d2164f0ce0d + Z 0d2164f0ce0d + foo + foobar $ hg push -B Z http://localhost:$HGPORT/ pushing to http://localhost:$HGPORT/ searching for changes @@ -380,6 +417,8 @@ $ hg in -B http://localhost:$HGPORT/ comparing with http://localhost:$HGPORT/ searching for changed bookmarks + @ 9b140be10808 + X 9b140be10808 Z 0d2164f0ce0d foo 000000000000 foobar 9b140be10808 @@ -409,6 +448,121 @@ $ cd .. +Test to show result of bookmarks comparision + + $ mkdir bmcomparison + $ cd bmcomparison + + $ hg init source + $ hg -R source debugbuilddag '+2*2*3*4' + $ hg -R source log -G --template '{rev}:{node|short}' + o 4:e7bd5218ca15 + | + | o 3:6100d3090acf + |/ + | o 2:fa942426a6fd + |/ + | o 1:66f7d451a68b + |/ + o 0:1ea73414a91b + + $ hg -R source bookmarks -r 0 SAME + $ hg -R source bookmarks -r 0 ADV_ON_REPO1 + $ hg -R source bookmarks -r 0 ADV_ON_REPO2 + $ hg -R source bookmarks -r 0 DIFF_ADV_ON_REPO1 + $ hg -R source bookmarks -r 0 DIFF_ADV_ON_REPO2 + $ hg -R source bookmarks -r 1 DIVERGED + + $ hg clone -U source repo1 + +(test that incoming/outgoing exit with 1, if there is no bookmark to +be excahnged) + + $ hg -R repo1 incoming -B + comparing with $TESTTMP/bmcomparison/source + searching for changed bookmarks + no changed bookmarks found + [1] + $ hg -R repo1 outgoing -B + comparing with $TESTTMP/bmcomparison/source + searching for changed bookmarks + no changed bookmarks found + [1] + + $ hg -R repo1 bookmarks -f -r 1 ADD_ON_REPO1 + $ hg -R repo1 bookmarks -f -r 2 ADV_ON_REPO1 + $ hg -R repo1 bookmarks -f -r 3 DIFF_ADV_ON_REPO1 + $ hg -R repo1 bookmarks -f -r 3 DIFF_DIVERGED + $ hg -R repo1 -q --config extensions.mq= strip 4 + $ hg -R repo1 log -G --template '{node|short} ({bookmarks})' + o 6100d3090acf (DIFF_ADV_ON_REPO1 DIFF_DIVERGED) + | + | o fa942426a6fd (ADV_ON_REPO1) + |/ + | o 66f7d451a68b (ADD_ON_REPO1 DIVERGED) + |/ + o 1ea73414a91b (ADV_ON_REPO2 DIFF_ADV_ON_REPO2 SAME) + + + $ hg clone -U source repo2 + $ hg -R repo2 bookmarks -f -r 1 ADD_ON_REPO2 + $ hg -R repo2 bookmarks -f -r 1 ADV_ON_REPO2 + $ hg -R repo2 bookmarks -f -r 2 DIVERGED + $ hg -R repo2 bookmarks -f -r 4 DIFF_ADV_ON_REPO2 + $ hg -R repo2 bookmarks -f -r 4 DIFF_DIVERGED + $ hg -R repo2 -q --config extensions.mq= strip 3 + $ hg -R repo2 log -G --template '{node|short} ({bookmarks})' + o e7bd5218ca15 (DIFF_ADV_ON_REPO2 DIFF_DIVERGED) + | + | o fa942426a6fd (DIVERGED) + |/ + | o 66f7d451a68b (ADD_ON_REPO2 ADV_ON_REPO2) + |/ + o 1ea73414a91b (ADV_ON_REPO1 DIFF_ADV_ON_REPO1 SAME) + + +(test that difference of bookmarks between repositories are fully shown) + + $ hg -R repo1 incoming -B repo2 -v + comparing with repo2 + searching for changed bookmarks + ADD_ON_REPO2 66f7d451a68b added + ADV_ON_REPO2 66f7d451a68b advanced + DIFF_ADV_ON_REPO2 e7bd5218ca15 changed + DIFF_DIVERGED e7bd5218ca15 changed + DIVERGED fa942426a6fd diverged + $ hg -R repo1 outgoing -B repo2 -v + comparing with repo2 + searching for changed bookmarks + ADD_ON_REPO1 66f7d451a68b added + ADD_ON_REPO2 deleted + ADV_ON_REPO1 fa942426a6fd advanced + DIFF_ADV_ON_REPO1 6100d3090acf advanced + DIFF_ADV_ON_REPO2 1ea73414a91b changed + DIFF_DIVERGED 6100d3090acf changed + DIVERGED 66f7d451a68b diverged + + $ hg -R repo2 incoming -B repo1 -v + comparing with repo1 + searching for changed bookmarks + ADD_ON_REPO1 66f7d451a68b added + ADV_ON_REPO1 fa942426a6fd advanced + DIFF_ADV_ON_REPO1 6100d3090acf changed + DIFF_DIVERGED 6100d3090acf changed + DIVERGED 66f7d451a68b diverged + $ hg -R repo2 outgoing -B repo1 -v + comparing with repo1 + searching for changed bookmarks + ADD_ON_REPO1 deleted + ADD_ON_REPO2 66f7d451a68b added + ADV_ON_REPO2 66f7d451a68b advanced + DIFF_ADV_ON_REPO1 1ea73414a91b changed + DIFF_ADV_ON_REPO2 e7bd5218ca15 advanced + DIFF_DIVERGED e7bd5218ca15 changed + DIVERGED fa942426a6fd diverged + + $ cd .. + Pushing a bookmark should only push the changes required by that bookmark, not all outgoing changes: $ hg clone http://localhost:$HGPORT/ addmarks @@ -460,6 +614,13 @@ $ hg -R ../b id -r W cc978a373a53 tip W +Check summary output for incoming/outgoing bookmarks + + $ hg bookmarks -d X + $ hg bookmarks -d Y + $ hg summary --remote | grep '^remote:' + remote: *, 2 incoming bookmarks, 1 outgoing bookmarks (glob) + $ cd .. pushing an unchanged bookmark should result in no changes diff -r 612ed41ae359 -r e89f909edffa tests/test-bookmarks-rebase.t --- a/tests/test-bookmarks-rebase.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bookmarks-rebase.t Thu Apr 16 20:57:51 2015 -0500 @@ -66,3 +66,27 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: 0 +aborted rebase should restore active bookmark. + + $ hg up 1 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + (leaving bookmark two) + $ echo 'e' > d + $ hg ci -A -m "4" + adding d + created new head + $ hg bookmark three + $ hg rebase -s three -d two + rebasing 4:dd7c838e8362 "4" (tip three) + merging d + warning: conflicts during merge. + merging d incomplete! (edit conflicts, then use 'hg resolve --mark') + unresolved conflicts (see hg resolve, then hg rebase --continue) + [1] + $ hg rebase --abort + rebase aborted + $ hg bookmark + one 1:925d80f479bb + * three 4:dd7c838e8362 + two 3:42e5ed2cdcf4 + diff -r 612ed41ae359 -r e89f909edffa tests/test-branches.t --- a/tests/test-branches.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-branches.t Thu Apr 16 20:57:51 2015 -0500 @@ -547,11 +547,22 @@ 0050: bf be 84 1b 00 00 00 02 d3 f1 63 45 80 00 00 02 |..........cE....| 0060: e3 d4 9c 05 80 00 00 02 e2 3b 55 05 00 00 00 02 |.........;U.....| 0070: f8 94 c2 56 80 00 00 03 |...V....| + +#if unix-permissions no-root +no errors when revbranchcache is not writable + + $ echo >> .hg/cache/rbc-revs-v1 + $ chmod a-w .hg/cache/rbc-revs-v1 + $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' + 5 + $ chmod a+w .hg/cache/rbc-revs-v1 +#endif + recovery from invalid cache revs file with trailing data $ echo >> .hg/cache/rbc-revs-v1 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug + 5 truncating cache/rbc-revs-v1 to 120 - 5 $ f --size .hg/cache/rbc-revs* .hg/cache/rbc-revs-v1: size=120 recovery from invalid cache file with partial last record @@ -560,8 +571,8 @@ $ f --size .hg/cache/rbc-revs* .hg/cache/rbc-revs-v1: size=119 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug + 5 truncating cache/rbc-revs-v1 to 112 - 5 $ f --size .hg/cache/rbc-revs* .hg/cache/rbc-revs-v1: size=120 recovery from invalid cache file with missing record - no truncation @@ -579,11 +590,11 @@ $ f -qDB 112 rbc-revs-v1 >> .hg/cache/rbc-revs-v1 $ f --size .hg/cache/rbc-revs* .hg/cache/rbc-revs-v1: size=120 - $ hg log -r 'branch(.)' -T '{rev} ' - 3 4 8 9 10 11 12 13 (no-eol) + $ hg log -r 'branch(.)' -T '{rev} ' --debug + 3 4 8 9 10 11 12 13 truncating cache/rbc-revs-v1 to 8 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug - truncating cache/rbc-revs-v1 to 8 5 + truncating cache/rbc-revs-v1 to 104 $ f --size --hexdump --bytes=16 .hg/cache/rbc-revs* .hg/cache/rbc-revs-v1: size=120 0000: 19 70 9c 5a 00 00 00 00 dd 6b 44 0d 00 00 00 01 |.p.Z.....kD.....| diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle-type.t --- a/tests/test-bundle-type.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle-type.t Thu Apr 16 20:57:51 2015 -0500 @@ -87,6 +87,7 @@ $ hg init tgarbage $ cd tgarbage $ hg pull ../bgarbage + pulling from ../bgarbage abort: ../bgarbage: not a Mercurial bundle [255] $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle.t --- a/tests/test-bundle.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle.t Thu Apr 16 20:57:51 2015 -0500 @@ -224,7 +224,7 @@ adding manifests adding file changes added 9 changesets with 7 changes to 4 files (+1 heads) - changegroup hook: HG_NODE=f9ee2f85a263049e9ae6d37a0e67e96194ffb735 HG_SOURCE=pull HG_URL=bundle:../full.hg + changegroup hook: HG_NODE=f9ee2f85a263049e9ae6d37a0e67e96194ffb735 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=bundle:../full.hg (glob) (run 'hg heads' to see heads, 'hg merge' to merge) Rollback empty @@ -247,7 +247,7 @@ adding manifests adding file changes added 9 changesets with 7 changes to 4 files (+1 heads) - changegroup hook: HG_NODE=f9ee2f85a263049e9ae6d37a0e67e96194ffb735 HG_SOURCE=pull HG_URL=bundle:empty+full.hg + changegroup hook: HG_NODE=f9ee2f85a263049e9ae6d37a0e67e96194ffb735 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=bundle:empty+full.hg (glob) (run 'hg heads' to see heads, 'hg merge' to merge) Create partial clones diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle2-exchange.t --- a/tests/test-bundle2-exchange.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle2-exchange.t Thu Apr 16 20:57:51 2015 -0500 @@ -25,10 +25,9 @@ > [phases] > publish=False > [hooks] - > changegroup = sh -c "HG_LOCAL= python \"$TESTDIR/printenv.py\" changegroup" - > b2x-pretransactionclose.tip = hg log -r tip -T "pre-close-tip:{node|short} {phase} {bookmarks}\n" - > b2x-transactionclose.tip = hg log -r tip -T "postclose-tip:{node|short} {phase} {bookmarks}\n" - > b2x-transactionclose.env = sh -c "HG_LOCAL= python \"$TESTDIR/printenv.py\" b2x-transactionclose" + > pretxnclose.tip = hg log -r tip -T "pre-close-tip:{node|short} {phase} {bookmarks}\n" + > txnclose.tip = hg log -r tip -T "postclose-tip:{node|short} {phase} {bookmarks}\n" + > txnclose.env = sh -c "HG_LOCAL= python \"$TESTDIR/printenv.py\" txnclose" > pushkey= sh "$TESTTMP/bundle2-pushkey-hook.sh" > EOF @@ -39,13 +38,19 @@ $ touch a $ hg add a $ hg commit -m 'a' + pre-close-tip:3903775176ed draft + postclose-tip:3903775176ed draft + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=commit (glob) $ hg unbundle $TESTDIR/bundles/rebase.hg adding changesets adding manifests adding file changes added 8 changesets with 7 changes to 7 files (+3 heads) - changegroup hook: HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_SOURCE=unbundle HG_URL=bundle:*/rebase.hg (glob) + pre-close-tip:02de42196ebe draft + postclose-tip:02de42196ebe draft + txnclose hook: HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_PHASES_MOVED=1 HG_SOURCE=unbundle HG_TXNID=TXN:* HG_TXNNAME=unbundle (glob) + bundle:*/tests/bundles/rebase.hg HG_URL=bundle:*/tests/bundles/rebase.hg (glob) (run 'hg heads' to see heads, 'hg merge' to merge) $ cd .. @@ -56,11 +61,20 @@ Add more obsolescence information $ hg -R main debugobsolete -d '0 0' 1111111111111111111111111111111111111111 `getmainid 9520eea781bc` + pre-close-tip:02de42196ebe draft + postclose-tip:02de42196ebe draft + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R main debugobsolete -d '0 0' 2222222222222222222222222222222222222222 `getmainid 24b6387c8c8c` + pre-close-tip:02de42196ebe draft + postclose-tip:02de42196ebe draft + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) clone --pull $ hg -R main phase --public cd010b8cd998 + pre-close-tip:000000000000 public + postclose-tip:02de42196ebe draft + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=phase (glob) $ hg clone main other --pull --rev 9520eea781bc adding changesets adding manifests @@ -69,8 +83,8 @@ 1 new obsolescence markers pre-close-tip:9520eea781bc draft postclose-tip:9520eea781bc draft - b2x-transactionclose hook: HG_NEW_OBSMARKERS=1 HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/main - changegroup hook: HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_SOURCE=pull HG_URL=file:$TESTTMP/main + txnclose hook: HG_NEW_OBSMARKERS=1 HG_NODE=cd010b8cd998f3981a5a8115f94f8da4ab506089 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob) updating to branch default 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg -R other log -G @@ -84,6 +98,9 @@ pull $ hg -R main phase --public 9520eea781bc + pre-close-tip:000000000000 public + postclose-tip:02de42196ebe draft + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=phase (glob) $ hg -R other pull -r 24b6387c8c8c pulling from $TESTTMP/main (glob) searching for changes @@ -94,8 +111,8 @@ 1 new obsolescence markers pre-close-tip:24b6387c8c8c draft postclose-tip:24b6387c8c8c draft - b2x-transactionclose hook: HG_NEW_OBSMARKERS=1 HG_NODE=24b6387c8c8cae37178880f3fa95ded3cb1cf785 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/main - changegroup hook: HG_NODE=24b6387c8c8cae37178880f3fa95ded3cb1cf785 HG_SOURCE=pull HG_URL=file:$TESTTMP/main + txnclose hook: HG_NEW_OBSMARKERS=1 HG_NODE=24b6387c8c8cae37178880f3fa95ded3cb1cf785 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob) (run 'hg heads' to see heads, 'hg merge' to merge) $ hg -R other log -G o 2:24b6387c8c8c draft Nicolas Dumazet F @@ -111,12 +128,16 @@ pull empty (with phase movement) $ hg -R main phase --public 24b6387c8c8c + pre-close-tip:000000000000 public + postclose-tip:02de42196ebe draft + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=phase (glob) $ hg -R other pull -r 24b6387c8c8c pulling from $TESTTMP/main (glob) no changes found pre-close-tip:000000000000 public postclose-tip:24b6387c8c8c public - b2x-transactionclose hook: HG_NEW_OBSMARKERS=0 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/main + txnclose hook: HG_NEW_OBSMARKERS=0 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob) $ hg -R other log -G o 2:24b6387c8c8c public Nicolas Dumazet F | @@ -135,7 +156,8 @@ no changes found pre-close-tip:24b6387c8c8c public postclose-tip:24b6387c8c8c public - b2x-transactionclose hook: HG_NEW_OBSMARKERS=0 HG_SOURCE=pull HG_URL=file:$TESTTMP/main + txnclose hook: HG_NEW_OBSMARKERS=0 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + file:/*/$TESTTMP/main HG_URL=file:$TESTTMP/main (glob) $ hg -R other log -G o 2:24b6387c8c8c public Nicolas Dumazet F | @@ -151,14 +173,29 @@ $ hg -R main bookmark --rev eea13746799a book_eea1 $ hg -R main debugobsolete -d '0 0' 3333333333333333333333333333333333333333 `getmainid eea13746799a` + pre-close-tip:02de42196ebe draft + postclose-tip:02de42196ebe draft + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R main bookmark --rev 02de42196ebe book_02de $ hg -R main debugobsolete -d '0 0' 4444444444444444444444444444444444444444 `getmainid 02de42196ebe` + pre-close-tip:02de42196ebe draft book_02de + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R main bookmark --rev 42ccdea3bb16 book_42cc $ hg -R main debugobsolete -d '0 0' 5555555555555555555555555555555555555555 `getmainid 42ccdea3bb16` + pre-close-tip:02de42196ebe draft book_02de + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R main bookmark --rev 5fddd98957c8 book_5fdd $ hg -R main debugobsolete -d '0 0' 6666666666666666666666666666666666666666 `getmainid 5fddd98957c8` + pre-close-tip:02de42196ebe draft book_02de + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R main bookmark --rev 32af7686d403 book_32af $ hg -R main debugobsolete -d '0 0' 7777777777777777777777777777777777777777 `getmainid 32af7686d403` + pre-close-tip:02de42196ebe draft book_02de + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_NEW_OBSMARKERS=1 HG_TXNID=TXN:* HG_TXNNAME=debugobsolete (glob) $ hg -R other bookmark --rev cd010b8cd998 book_eea1 $ hg -R other bookmark --rev cd010b8cd998 book_02de @@ -167,6 +204,9 @@ $ hg -R other bookmark --rev cd010b8cd998 book_32af $ hg -R main phase --public eea13746799a + pre-close-tip:000000000000 public + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=phase (glob) push $ hg -R main push other --rev eea13746799a --bookmark book_eea1 @@ -180,8 +220,7 @@ lock: free wlock: free postclose-tip:eea13746799a public book_eea1 - b2x-transactionclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2-EXP=1 HG_NEW_OBSMARKERS=1 HG_NODE=eea13746799a9e0bfd88f29d3c2e9dc9389f524f HG_PHASES_MOVED=1 HG_SOURCE=push HG_URL=push - changegroup hook: HG_BUNDLE2-EXP=1 HG_NODE=eea13746799a9e0bfd88f29d3c2e9dc9389f524f HG_SOURCE=push HG_URL=push + txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_NEW_OBSMARKERS=1 HG_NODE=eea13746799a9e0bfd88f29d3c2e9dc9389f524f HG_PHASES_MOVED=1 HG_SOURCE=push HG_TXNID=TXN:* HG_TXNNAME=push HG_URL=push (glob) remote: adding changesets remote: adding manifests remote: adding file changes @@ -190,7 +229,8 @@ updating bookmark book_eea1 pre-close-tip:02de42196ebe draft book_02de postclose-tip:02de42196ebe draft book_02de - b2x-transactionclose hook: HG_SOURCE=push-response HG_URL=file:$TESTTMP/other + txnclose hook: HG_SOURCE=push-response HG_TXNID=TXN:* HG_TXNNAME=push-response (glob) + file:/*/$TESTTMP/other HG_URL=file:$TESTTMP/other (glob) $ hg -R other log -G o 3:eea13746799a public Nicolas Dumazet book_eea1 G |\ @@ -218,8 +258,8 @@ updating bookmark book_02de pre-close-tip:02de42196ebe draft book_02de postclose-tip:02de42196ebe draft book_02de - b2x-transactionclose hook: HG_BOOKMARK_MOVED=1 HG_NEW_OBSMARKERS=1 HG_NODE=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=ssh://user@dummy/main - changegroup hook: HG_NODE=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_SOURCE=pull HG_URL=ssh://user@dummy/main + txnclose hook: HG_BOOKMARK_MOVED=1 HG_NEW_OBSMARKERS=1 HG_NODE=02de42196ebee42ef284b6780a87cdc96e8eaab6 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + ssh://user@dummy/main HG_URL=ssh://user@dummy/main (run 'hg heads' to see heads, 'hg merge' to merge) $ hg -R other debugobsolete 1111111111111111111111111111111111111111 9520eea781bcca16c1e15acc0ba14335a0e8e5ba 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} @@ -243,8 +283,8 @@ updating bookmark book_42cc pre-close-tip:42ccdea3bb16 draft book_42cc postclose-tip:42ccdea3bb16 draft book_42cc - b2x-transactionclose hook: HG_BOOKMARK_MOVED=1 HG_NEW_OBSMARKERS=1 HG_NODE=42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=http://localhost:$HGPORT/ - changegroup hook: HG_NODE=42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 HG_SOURCE=pull HG_URL=http://localhost:$HGPORT/ + txnclose hook: HG_BOOKMARK_MOVED=1 HG_NEW_OBSMARKERS=1 HG_NODE=42ccdea3bb16d28e1848c95fe2e44c000f3f21b1 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_TXNNAME=pull (glob) + http://localhost:$HGPORT/ HG_URL=http://localhost:$HGPORT/ (run 'hg heads .' to see heads, 'hg merge' to merge) $ cat main-error.log $ hg -R other debugobsolete @@ -270,11 +310,11 @@ remote: lock: free remote: wlock: free remote: postclose-tip:5fddd98957c8 draft book_5fdd - remote: b2x-transactionclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2-EXP=1 HG_NEW_OBSMARKERS=1 HG_NODE=5fddd98957c8a54a4d436dfe1da9d87f21a1b97b HG_SOURCE=serve HG_URL=remote:ssh:127.0.0.1 - remote: changegroup hook: HG_BUNDLE2-EXP=1 HG_NODE=5fddd98957c8a54a4d436dfe1da9d87f21a1b97b HG_SOURCE=serve HG_URL=remote:ssh:127.0.0.1 + remote: txnclose hook: HG_BOOKMARK_MOVED=1 HG_BUNDLE2=1 HG_NEW_OBSMARKERS=1 HG_NODE=5fddd98957c8a54a4d436dfe1da9d87f21a1b97b HG_SOURCE=serve HG_TXNID=TXN:* HG_TXNNAME=serve HG_URL=remote:ssh:127.0.0.1 (glob) pre-close-tip:02de42196ebe draft book_02de postclose-tip:02de42196ebe draft book_02de - b2x-transactionclose hook: HG_SOURCE=push-response HG_URL=ssh://user@dummy/other + txnclose hook: HG_SOURCE=push-response HG_TXNID=TXN:* HG_TXNNAME=push-response (glob) + ssh://user@dummy/other HG_URL=ssh://user@dummy/other $ hg -R other log -G o 6:5fddd98957c8 draft Nicolas Dumazet book_5fdd C | @@ -304,6 +344,9 @@ $ cat other.pid >> $DAEMON_PIDS $ hg -R main phase --public 32af7686d403 + pre-close-tip:000000000000 public + postclose-tip:02de42196ebe draft book_02de + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=phase (glob) $ hg -R main push http://localhost:$HGPORT2/ -r 32af7686d403 --bookmark book_32af pushing to http://localhost:$HGPORT2/ searching for changes @@ -315,7 +358,8 @@ updating bookmark book_32af pre-close-tip:02de42196ebe draft book_02de postclose-tip:02de42196ebe draft book_02de - b2x-transactionclose hook: HG_SOURCE=push-response HG_URL=http://localhost:$HGPORT2/ + txnclose hook: HG_SOURCE=push-response HG_TXNID=TXN:* HG_TXNNAME=push-response (glob) + http://localhost:$HGPORT2/ HG_URL=http://localhost:$HGPORT2/ $ cat other-error.log Check final content. @@ -382,7 +426,7 @@ > bundler.newpart('test:unknown') > if reason == 'race': > # 20 Bytes of crap - > bundler.newpart('b2x:check:heads', data='01234567890123456789') + > bundler.newpart('check:heads', data='01234567890123456789') > > @bundle2.parthandler("test:abort") > def handleabort(op, part): @@ -400,6 +444,9 @@ $ echo 'I' > I $ hg add I $ hg ci -m 'I' + pre-close-tip:e7ec4e813ba6 draft + postclose-tip:e7ec4e813ba6 draft + txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) $ hg id e7ec4e813ba6 tip $ cd .. @@ -501,7 +548,7 @@ > [failpush] > reason = > [hooks] - > b2x-pretransactionclose.failpush = false + > pretxnclose.failpush = false > EOF $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS @@ -514,13 +561,21 @@ pre-close-tip:e7ec4e813ba6 draft transaction abort! rollback completed - abort: b2x-pretransactionclose.failpush hook exited with status 1 + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + abort: pretxnclose.failpush hook exited with status 1 [255] $ hg -R main push ssh://user@dummy/other -r e7ec4e813ba6 pushing to ssh://user@dummy/other searching for changes - abort: b2x-pretransactionclose.failpush hook exited with status 1 + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + abort: pretxnclose.failpush hook exited with status 1 remote: pre-close-tip:e7ec4e813ba6 draft remote: transaction abort! remote: rollback completed @@ -529,7 +584,11 @@ $ hg -R main push http://localhost:$HGPORT2/ -r e7ec4e813ba6 pushing to http://localhost:$HGPORT2/ searching for changes - abort: b2x-pretransactionclose.failpush hook exited with status 1 + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files + abort: pretxnclose.failpush hook exited with status 1 [255] (check that no 'pending' files remain) diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle2-format.t --- a/tests/test-bundle2-format.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle2-format.t Thu Apr 16 20:57:51 2015 -0500 @@ -92,11 +92,11 @@ > > if opts['reply']: > capsstring = 'ping-pong\nelephants=babar,celeste\ncity%3D%21=celeste%2Cville' - > bundler.newpart('b2x:replycaps', data=capsstring) + > bundler.newpart('replycaps', data=capsstring) > > if opts['pushrace']: > # also serve to test the assignement of data outside of init - > part = bundler.newpart('b2x:check:heads') + > part = bundler.newpart('check:heads') > part.data = '01234567890123456789' > > revs = opts['rev'] @@ -109,7 +109,7 @@ > headcommon = [c.node() for c in repo.set('parents(%ld) - %ld', revs, revs)] > outgoing = discovery.outgoing(repo.changelog, headcommon, headmissing) > cg = changegroup.getlocalchangegroup(repo, 'test:bundle2', outgoing, None) - > bundler.newpart('b2x:changegroup', data=cg.getchunks(), + > bundler.newpart('changegroup', data=cg.getchunks(), > mandatory=False) > > if opts['parts']: @@ -136,7 +136,7 @@ > def genraise(): > yield 'first line\n' > raise RuntimeError('Someone set up us the bomb!') - > bundler.newpart('b2x:output', data=genraise(), mandatory=False) + > bundler.newpart('output', data=genraise(), mandatory=False) > > if path is None: > file = sys.stdout @@ -157,7 +157,7 @@ > lock = repo.lock() > tr = repo.transaction('processbundle') > try: - > unbundler = bundle2.unbundle20(ui, sys.stdin) + > unbundler = bundle2.getunbundler(ui, sys.stdin) > op = bundle2.processbundle(repo, unbundler, lambda: tr) > tr.close() > except error.BundleValueError, exc: @@ -183,7 +183,7 @@ > @command('statbundle2', [], '') > def cmdstatbundle2(ui, repo): > """print statistic on the bundle2 container read from stdin""" - > unbundler = bundle2.unbundle20(ui, sys.stdin) + > unbundler = bundle2.getunbundler(ui, sys.stdin) > try: > params = unbundler.params > except error.BundleValueError, exc: @@ -237,7 +237,7 @@ Test bundling $ hg bundle2 - HG2Y\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc) Test unbundling @@ -267,7 +267,7 @@ Test generation simple option $ hg bundle2 --param 'caution' - HG2Y\x00\x00\x00\x07caution\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00\x07caution\x00\x00\x00\x00 (no-eol) (esc) Test unbundling @@ -279,7 +279,7 @@ Test generation multiple option $ hg bundle2 --param 'caution' --param 'meal' - HG2Y\x00\x00\x00\x0ccaution meal\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00\x0ccaution meal\x00\x00\x00\x00 (no-eol) (esc) Test unbundling @@ -295,7 +295,7 @@ Test generation $ hg bundle2 --param 'caution' --param 'meal=vegan' --param 'elephants' - HG2Y\x00\x00\x00\x1ccaution meal=vegan elephants\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00\x1ccaution meal=vegan elephants\x00\x00\x00\x00 (no-eol) (esc) Test unbundling @@ -313,7 +313,7 @@ Test generation $ hg bundle2 --param 'e|! 7/=babar%#==tutu' --param simple - HG2Y\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc) Test unbundling @@ -337,7 +337,7 @@ bundling debug $ hg bundle2 --debug --param 'e|! 7/=babar%#==tutu' --param simple ../out.hg2 - start emission of HG2Y stream + start emission of HG20 stream bundle parameter: e%7C%21%207/=babar%25%23%3D%3Dtutu simple start of parts end of bundle @@ -345,12 +345,12 @@ file content is ok $ cat ../out.hg2 - HG2Y\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00)e%7C%21%207/=babar%25%23%3D%3Dtutu simple\x00\x00\x00\x00 (no-eol) (esc) unbundling debug $ hg statbundle2 --debug < ../out.hg2 - start processing of HG2Y stream + start processing of HG20 stream reading bundle2 stream parameters ignoring unknown parameter 'e|! 7/' ignoring unknown parameter 'simple' @@ -384,7 +384,7 @@ ================= $ hg bundle2 --parts ../parts.hg2 --debug - start emission of HG2Y stream + start emission of HG20 stream bundle parameter: start of parts bundle part: "test:empty" @@ -397,7 +397,7 @@ end of bundle $ cat ../parts.hg2 - HG2Y\x00\x00\x00\x00\x00\x00\x00\x11 (esc) + HG20\x00\x00\x00\x00\x00\x00\x00\x11 (esc) test:empty\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11 (esc) test:empty\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10 test:song\x00\x00\x00\x02\x00\x00\x00\x00\x00\xb2Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko (esc) Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko @@ -437,7 +437,7 @@ parts count: 7 $ hg statbundle2 --debug < ../parts.hg2 - start processing of HG2Y stream + start processing of HG20 stream reading bundle2 stream parameters options count: 0 start extraction of bundle2 parts @@ -516,7 +516,7 @@ Process the bundle $ hg unbundle2 --debug < ../parts.hg2 - start processing of HG2Y stream + start processing of HG20 stream reading bundle2 stream parameters start extraction of bundle2 parts part header size: 17 @@ -610,21 +610,18 @@ The reply is a bundle $ cat ../reply.hg2 - HG2Y\x00\x00\x00\x00\x00\x00\x00\x1f (esc) - b2x:output\x00\x00\x00\x00\x00\x01\x0b\x01in-reply-to3\x00\x00\x00\xd9The choir starts singing: (esc) + HG20\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x00\x00\x01\x0b\x01in-reply-to3\x00\x00\x00\xd9The choir starts singing: (esc) Patali Dirapata, Cromda Cromda Ripalo, Pata Pata, Ko Ko Ko Bokoro Dipoulito, Rondi Rondi Pepino, Pata Pata, Ko Ko Ko Emana Karassoli, Loucra Loucra Ponponto, Pata Pata, Ko Ko Ko. - \x00\x00\x00\x00\x00\x00\x00\x1f (esc) - b2x:output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to4\x00\x00\x00\xc9debugreply: capabilities: (esc) + \x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to4\x00\x00\x00\xc9debugreply: capabilities: (esc) debugreply: 'city=!' debugreply: 'celeste,ville' debugreply: 'elephants' debugreply: 'babar' debugreply: 'celeste' debugreply: 'ping-pong' - \x00\x00\x00\x00\x00\x00\x00\x1e test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to7\x00\x00\x00\x00\x00\x00\x00\x1f (esc) - b2x:output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to7\x00\x00\x00=received ping request (id 7) (esc) + \x00\x00\x00\x00\x00\x00\x00\x1e test:pong\x00\x00\x00\x02\x01\x00\x0b\x01in-reply-to7\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x03\x00\x01\x0b\x01in-reply-to7\x00\x00\x00=received ping request (id 7) (esc) replying to ping request (id 7) \x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc) @@ -632,11 +629,11 @@ $ hg statbundle2 < ../reply.hg2 options count: 0 - :b2x:output: + :output: mandatory: 0 advisory: 1 payload: 217 bytes - :b2x:output: + :output: mandatory: 0 advisory: 1 payload: 201 bytes @@ -644,7 +641,7 @@ mandatory: 1 advisory: 0 payload: 0 bytes - :b2x:output: + :output: mandatory: 0 advisory: 1 payload: 61 bytes @@ -714,10 +711,10 @@ 9520eea781bcca16c1e15acc0ba14335a0e8e5ba eea13746799a9e0bfd88f29d3c2e9dc9389f524f 02de42196ebee42ef284b6780a87cdc96e8eaab6 - start emission of HG2Y stream + start emission of HG20 stream bundle parameter: start of parts - bundle part: "b2x:changegroup" + bundle part: "changegroup" bundling: 1/4 changesets (25.00%) bundling: 2/4 changesets (50.00%) bundling: 3/4 changesets (75.00%) @@ -732,7 +729,7 @@ end of bundle $ cat ../rev.hg2 - HG2Y\x00\x00\x00\x00\x00\x00\x00\x16\x0fb2x:changegroup\x00\x00\x00\x00\x00\x00\x00\x00\x06\x13\x00\x00\x00\xa42\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j_\xdd\xd9\x89W\xc8\xa5JMCm\xfe\x1d\xa9\xd8\x7f!\xa1\xb9{\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)6e1f4c47ecb533ffd0c8e52cdc88afb6cd39e20c (esc) + HG20\x00\x00\x00\x00\x00\x00\x00\x12\x0bchangegroup\x00\x00\x00\x00\x00\x00\x00\x00\x06\x13\x00\x00\x00\xa42\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j_\xdd\xd9\x89W\xc8\xa5JMCm\xfe\x1d\xa9\xd8\x7f!\xa1\xb9{\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002\xafv\x86\xd4\x03\xcfE\xb5\xd9_-p\xce\xbe\xa5\x87\xac\x80j\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)6e1f4c47ecb533ffd0c8e52cdc88afb6cd39e20c (esc) \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02D (esc) \x00\x00\x00i\x00\x00\x00j\x00\x00\x00\x01D\x00\x00\x00\xa4\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\xcd\x01\x0b\x8c\xd9\x98\xf3\x98\x1aZ\x81\x15\xf9O\x8d\xa4\xabP`\x89\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x95 \xee\xa7\x81\xbc\xca\x16\xc1\xe1Z\xcc\x0b\xa1C5\xa0\xe8\xe5\xba\x00\x00\x00\x00\x00\x00\x00)\x00\x00\x00)4dece9c826f69490507b98c6383a3009b295837d (esc) \x00\x00\x00f\x00\x00\x00h\x00\x00\x00\x02E (esc) @@ -757,7 +754,7 @@ $ hg debugbundle ../rev.hg2 Stream params: {} - b2x:changegroup -- '{}' + changegroup -- '{}' 32af7686d403cf45b5d95f2d70cebea587ac806a 9520eea781bcca16c1e15acc0ba14335a0e8e5ba eea13746799a9e0bfd88f29d3c2e9dc9389f524f @@ -776,8 +773,7 @@ addchangegroup return: 1 $ cat ../rev-reply.hg2 - HG2Y\x00\x00\x00\x00\x00\x00\x003\x15b2x:reply:changegroup\x00\x00\x00\x00\x00\x02\x0b\x01\x06\x01in-reply-to1return1\x00\x00\x00\x00\x00\x00\x00\x1f (esc) - b2x:output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to1\x00\x00\x00dadding changesets (esc) + HG20\x00\x00\x00\x00\x00\x00\x00/\x11reply:changegroup\x00\x00\x00\x00\x00\x02\x0b\x01\x06\x01in-reply-to1return1\x00\x00\x00\x00\x00\x00\x00\x1b\x06output\x00\x00\x00\x01\x00\x01\x0b\x01in-reply-to1\x00\x00\x00dadding changesets (esc) adding manifests adding file changes added 0 changesets with 0 changes to 3 files @@ -793,8 +789,8 @@ Should still be a valid bundle $ cat ../genfailed.hg2 - HG2Y\x00\x00\x00\x00\x00\x00\x00\x11 (esc) - b2x:output\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00L\x0fb2x:error:abort\x00\x00\x00\x00\x01\x00\x07-messageunexpected error: Someone set up us the bomb!\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc) + HG20\x00\x00\x00\x00\x00\x00\x00\r (no-eol) (esc) + \x06output\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00H\x0berror:abort\x00\x00\x00\x00\x01\x00\x07-messageunexpected error: Someone set up us the bomb!\x00\x00\x00\x00\x00\x00\x00\x00 (no-eol) (esc) And its handling on the other size raise a clean exception diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle2-multiple-changegroups.t --- a/tests/test-bundle2-multiple-changegroups.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle2-multiple-changegroups.t Thu Apr 16 20:57:51 2015 -0500 @@ -14,13 +14,13 @@ > intermediates = [repo[r].p1().node() for r in heads] > cg = changegroup.getchangegroup(repo, source, heads=intermediates, > common=common, bundlecaps=bundlecaps) - > bundler.newpart('b2x:output', data='changegroup1') - > bundler.newpart('b2x:changegroup', data=cg.getchunks()) + > bundler.newpart('output', data='changegroup1') + > bundler.newpart('changegroup', data=cg.getchunks()) > cg = changegroup.getchangegroup(repo, source, heads=heads, > common=common + intermediates, > bundlecaps=bundlecaps) - > bundler.newpart('b2x:output', data='changegroup2') - > bundler.newpart('b2x:changegroup', data=cg.getchunks()) + > bundler.newpart('output', data='changegroup2') + > bundler.newpart('changegroup', data=cg.getchunks()) > > def _pull(repo, *args, **kwargs): > pullop = _orig_pull(repo, *args, **kwargs) @@ -82,17 +82,17 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files - pretxnchangegroup hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) remote: changegroup2 adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files - pretxnchangegroup hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=27547f69f25460a52fff66ad004e58da7ad3fb56 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=f838bfaca5c7226600ebcfd84f3c3c13a28d3757 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) pullop.cgresult is 1 (run 'hg update' to get a working copy) $ hg update @@ -152,20 +152,20 @@ adding manifests adding file changes added 2 changesets with 2 changes to 2 files (+1 heads) - pretxnchangegroup hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) remote: changegroup2 adding changesets adding manifests adding file changes added 3 changesets with 3 changes to 3 files (+1 heads) - pretxnchangegroup hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=8a5212ebc8527f9fb821601504794e3eb11a1ed3 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=1d14c3ce6ac0582d2809220d33e8cd7a696e0156 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=5cd59d311f6508b8e0ed28a266756c859419c9f1 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=8a5212ebc8527f9fb821601504794e3eb11a1ed3 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=7f219660301fe4c8a116f714df5e769695cc2b46 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=1d14c3ce6ac0582d2809220d33e8cd7a696e0156 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=5cd59d311f6508b8e0ed28a266756c859419c9f1 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) pullop.cgresult is 3 (run 'hg heads' to see heads, 'hg merge' to merge) $ hg log -G @@ -225,17 +225,17 @@ adding manifests adding file changes added 1 changesets with 0 changes to 0 files (-1 heads) - pretxnchangegroup hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_PENDING=$TESTTMP/clone HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) remote: changegroup2 adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files - pretxnchangegroup hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - changegroup hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo - incoming hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_URL=file:$TESTTMP/repo + pretxnchangegroup hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PENDING=$TESTTMP/clone HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=71bd7b46de72e69a32455bf88d04757d542e6cf4 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + changegroup hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) + incoming hook: HG_NODE=9d18e5bd9ab09337802595d49f1dad0c98df4d84 HG_PHASES_MOVED=1 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/repo (glob) pullop.cgresult is -2 (run 'hg update' to get a working copy) $ hg log -G diff -r 612ed41ae359 -r e89f909edffa tests/test-bundle2-pushback.t --- a/tests/test-bundle2-pushback.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-bundle2-pushback.t Thu Apr 16 20:57:51 2015 -0500 @@ -6,21 +6,21 @@ > from mercurial import bundle2, pushkey, exchange, util > def _newhandlechangegroup(op, inpart): > """This function wraps the changegroup part handler for getbundle. - > It issues an additional b2x:pushkey part to send a new + > It issues an additional pushkey part to send a new > bookmark back to the client""" > result = bundle2.handlechangegroup(op, inpart) - > if 'b2x:pushback' in op.reply.capabilities: + > if 'pushback' in op.reply.capabilities: > params = {'namespace': 'bookmarks', > 'key': 'new-server-mark', > 'old': '', > 'new': 'tip'} > encodedparams = [(k, pushkey.encode(v)) for (k,v) in params.items()] - > op.reply.newpart('b2x:pushkey', mandatoryparams=encodedparams) + > op.reply.newpart('pushkey', mandatoryparams=encodedparams) > else: - > op.reply.newpart('b2x:output', data='pushback not enabled') + > op.reply.newpart('output', data='pushback not enabled') > return result > _newhandlechangegroup.params = bundle2.handlechangegroup.params - > bundle2.parthandlermapping['b2x:changegroup'] = _newhandlechangegroup + > bundle2.parthandlermapping['changegroup'] = _newhandlechangegroup > EOF $ cat >> $HGRCPATH < def newpart(name, data=''): > """wrapper around bundler.newpart adding an extra part making the > client output information about each processed part""" - > bundler.newpart('b2x:output', data=name) + > bundler.newpart('output', data=name) > part = bundler.newpart(name, data=data) > return part > @@ -50,13 +50,13 @@ > bundledata = open(file, 'rb').read() > digest = util.digester.preferred(b2caps['digests']) > d = util.digester([digest], bundledata) - > part = newpart('b2x:remote-changegroup') + > part = newpart('remote-changegroup') > part.addparam('url', url) > part.addparam('size', str(len(bundledata))) > part.addparam('digests', digest) > part.addparam('digest:%s' % digest, d[digest]) > elif verb == 'raw-remote-changegroup': - > part = newpart('b2x:remote-changegroup') + > part = newpart('remote-changegroup') > for k, v in eval(args).items(): > part.addparam(k, str(v)) > elif verb == 'changegroup': @@ -65,7 +65,7 @@ > heads = [repo.lookup(r) for r in repo.revs(heads)] > cg = changegroup.getchangegroup(repo, 'changegroup', > heads=heads, common=common) - > newpart('b2x:changegroup', cg.getchunks()) + > newpart('changegroup', cg.getchunks()) > else: > raise Exception('unknown verb') > @@ -137,7 +137,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -180,12 +180,12 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files (+1 heads) - remote: b2x:changegroup + remote: changegroup adding changesets adding manifests adding file changes @@ -228,12 +228,12 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:changegroup + remote: changegroup adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files (+1 heads) - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -279,17 +279,17 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files (+1 heads) - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes added 2 changesets with 1 changes to 1 files - remote: b2x:changegroup + remote: changegroup adding changesets adding manifests adding file changes @@ -324,7 +324,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -338,7 +338,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -354,7 +354,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -372,7 +372,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -388,7 +388,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -404,7 +404,7 @@ > EOF $ hg clone ssh://user@dummy/repo clone requesting all changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -433,12 +433,12 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes added 2 changesets with 2 changes to 2 files (+1 heads) - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -467,7 +467,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: remote-changegroup: missing "url" param [255] @@ -479,7 +479,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: remote-changegroup: missing "size" param [255] @@ -491,7 +491,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: remote-changegroup: invalid value for param "size" [255] @@ -503,7 +503,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup adding changesets adding manifests adding file changes @@ -522,8 +522,8 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup - abort: missing support for b2x:remote-changegroup - digest:foo + remote: remote-changegroup + abort: missing support for remote-changegroup - digest:foo [255] Missing digest @@ -534,7 +534,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: remote-changegroup: missing "digest:sha1" param [255] @@ -546,7 +546,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: remote-changegroup does not support ssh urls [255] @@ -561,14 +561,14 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: http://localhost:$HGPORT/notbundle.hg: not a Mercurial bundle [255] Not a bundle 1.0 $ cat > notbundle10.hg << EOF - > HG2Y + > HG20 > EOF $ cat > repo/.hg/bundle2maker << EOF > remote-changegroup http://localhost:$HGPORT/notbundle10.hg notbundle10.hg @@ -576,7 +576,7 @@ $ hg pull -R clone ssh://user@dummy/repo pulling from ssh://user@dummy/repo searching for changes - remote: b2x:remote-changegroup + remote: remote-changegroup abort: http://localhost:$HGPORT/notbundle10.hg: not a bundle version 1.0 [255] diff -r 612ed41ae359 -r e89f909edffa tests/test-casefolding.t --- a/tests/test-casefolding.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-casefolding.t Thu Apr 16 20:57:51 2015 -0500 @@ -28,7 +28,6 @@ a committing manifest committing changelog - couldn't read revision branch cache names: * (glob) committed changeset 0:07f4944404050f47db2e5c5071e0e84e7a27bba9 Case-changing renames should work: diff -r 612ed41ae359 -r e89f909edffa tests/test-cat.t --- a/tests/test-cat.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-cat.t Thu Apr 16 20:57:51 2015 -0500 @@ -22,10 +22,22 @@ $ hg cat -r 1 b 1 -Test fileset +Test multiple files $ echo 3 > c $ hg ci -Am addmore c + $ hg cat b c + 1 + 3 + $ hg cat . + 1 + 3 + $ hg cat . c + 1 + 3 + +Test fileset + $ hg cat 'set:not(b) or a' 3 $ hg cat 'set:c or b' @@ -51,3 +63,8 @@ tmp/h_45116003780e tmp/r_2 +Test working directory + + $ echo b-wdir > b + $ hg cat -r 'wdir()' b + b-wdir diff -r 612ed41ae359 -r e89f909edffa tests/test-censor.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-censor.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,480 @@ + $ cat >> $HGRCPATH < [extensions] + > censor= + > EOF + $ cp $HGRCPATH $HGRCPATH.orig + +Create repo with unimpeachable content + + $ hg init r + $ cd r + $ echo 'Initially untainted file' > target + $ echo 'Normal file here' > bystander + $ hg add target bystander + $ hg ci -m init + +Clone repo so we can test pull later + + $ cd .. + $ hg clone r rpull + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd r + +Introduce content which will ultimately require censorship. Name the first +censored node C1, second C2, and so on + + $ echo 'Tainted file' > target + $ echo 'Passwords: hunter2' >> target + $ hg ci -m taint target + $ C1=`hg id --debug -i` + + $ echo 'hunter3' >> target + $ echo 'Normal file v2' > bystander + $ hg ci -m moretaint target bystander + $ C2=`hg id --debug -i` + +Add a new sanitized versions to correct our mistake. Name the first head H1, +the second head H2, and so on + + $ echo 'Tainted file is now sanitized' > target + $ hg ci -m sanitized target + $ H1=`hg id --debug -i` + + $ hg update -r $C2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 'Tainted file now super sanitized' > target + $ hg ci -m 'super sanitized' target + created new head + $ H2=`hg id --debug -i` + +Verify target contents before censorship at each revision + + $ hg cat -r $H1 target + Tainted file is now sanitized + $ hg cat -r $H2 target + Tainted file now super sanitized + $ hg cat -r $C2 target + Tainted file + Passwords: hunter2 + hunter3 + $ hg cat -r $C1 target + Tainted file + Passwords: hunter2 + $ hg cat -r 0 target + Initially untainted file + +Try to censor revision with too large of a tombstone message + + $ hg censor -r $C1 -t 'blah blah blah blah blah blah blah blah bla' target + abort: censor tombstone must be no longer than censored data + [255] + +Censor revision with 2 offenses + + $ hg censor -r $C2 -t "remove password" target + $ hg cat -r $H1 target + Tainted file is now sanitized + $ hg cat -r $H2 target + Tainted file now super sanitized + $ hg cat -r $C2 target + abort: censored node: 1e0247a9a4b7 + (set censor.policy to ignore errors) + [255] + $ hg cat -r $C1 target + Tainted file + Passwords: hunter2 + $ hg cat -r 0 target + Initially untainted file + +Censor revision with 1 offense + + $ hg censor -r $C1 target + $ hg cat -r $H1 target + Tainted file is now sanitized + $ hg cat -r $H2 target + Tainted file now super sanitized + $ hg cat -r $C2 target + abort: censored node: 1e0247a9a4b7 + (set censor.policy to ignore errors) + [255] + $ hg cat -r $C1 target + abort: censored node: 613bc869fceb + (set censor.policy to ignore errors) + [255] + $ hg cat -r 0 target + Initially untainted file + +Can only checkout target at uncensored revisions, -X is workaround for --all + + $ hg revert -r $C2 target + abort: censored node: 1e0247a9a4b7 + (set censor.policy to ignore errors) + [255] + $ hg revert -r $C1 target + abort: censored node: 613bc869fceb + (set censor.policy to ignore errors) + [255] + $ hg revert -r $C1 --all + reverting bystander + reverting target + abort: censored node: 613bc869fceb + (set censor.policy to ignore errors) + [255] + $ hg revert -r $C1 --all -X target + $ cat target + Tainted file now super sanitized + $ hg revert -r 0 --all + reverting target + $ cat target + Initially untainted file + $ hg revert -r $H2 --all + reverting bystander + reverting target + $ cat target + Tainted file now super sanitized + +Uncensored file can be viewed at any revision + + $ hg cat -r $H1 bystander + Normal file v2 + $ hg cat -r $C2 bystander + Normal file v2 + $ hg cat -r $C1 bystander + Normal file here + $ hg cat -r 0 bystander + Normal file here + +Can update to children of censored revision + + $ hg update -r $H1 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Tainted file is now sanitized + $ hg update -r $H2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Tainted file now super sanitized + +Set censor policy to abort in trusted $HGRC so hg verify fails + + $ cp $HGRCPATH.orig $HGRCPATH + $ cat >> $HGRCPATH < [censor] + > policy = abort + > EOF + +Repo fails verification due to censorship + + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + target@1: censored file data + target@2: censored file data + 2 files, 5 changesets, 7 total revisions + 2 integrity errors encountered! + (first damaged changeset appears to be 1) + [1] + +Cannot update to revision with censored data + + $ hg update -r $C2 + abort: censored node: 1e0247a9a4b7 + (set censor.policy to ignore errors) + [255] + $ hg update -r $C1 + abort: censored node: 613bc869fceb + (set censor.policy to ignore errors) + [255] + $ hg update -r 0 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg update -r $H2 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Set censor policy to ignore in trusted $HGRC so hg verify passes + + $ cp $HGRCPATH.orig $HGRCPATH + $ cat >> $HGRCPATH < [censor] + > policy = ignore + > EOF + +Repo passes verification with warnings with explicit config + + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 5 changesets, 7 total revisions + +May update to revision with censored data with explicit config + + $ hg update -r $C2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + $ hg update -r $C1 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + $ hg update -r 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Initially untainted file + $ hg update -r $H2 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Tainted file now super sanitized + +Can merge in revision with censored data. Test requires one branch of history +with the file censored, but we can't censor at a head, so advance H1. + + $ hg update -r $H1 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ C3=$H1 + $ echo 'advanced head H1' > target + $ hg ci -m 'advance head H1' target + $ H1=`hg id --debug -i` + $ hg censor -r $C3 target + $ hg update -r $H2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge -r $C3 + merging target + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + +Revisions present in repository heads may not be censored + + $ hg update -C -r $H2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg censor -r $H2 target + abort: cannot censor file in heads (78a8fc215e79) + (clean/delete and commit first) + [255] + $ echo 'twiddling thumbs' > bystander + $ hg ci -m 'bystander commit' + $ H2=`hg id --debug -i` + $ hg censor -r "$H2^" target + abort: cannot censor file in heads (efbe78065929) + (clean/delete and commit first) + [255] + +Cannot censor working directory + + $ echo 'seriously no passwords' > target + $ hg ci -m 'extend second head arbitrarily' target + $ H2=`hg id --debug -i` + $ hg update -r "$H2^" + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg censor -r . target + abort: cannot censor working directory + (clean/delete/update first) + [255] + $ hg update -r $H2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Can re-add file after being deleted + censored + + $ C4=$H2 + $ hg rm target + $ hg ci -m 'delete target so it may be censored' + $ H2=`hg id --debug -i` + $ hg censor -r $C4 target + $ hg cat -r $C4 target + $ hg cat -r "$H2^^" target + Tainted file now super sanitized + $ echo 'fresh start' > target + $ hg add target + $ hg ci -m reincarnated target + $ H2=`hg id --debug -i` + $ hg cat -r $H2 target + fresh start + $ hg cat -r "$H2^" target + target: no such file in rev 452ec1762369 + [1] + $ hg cat -r $C4 target + $ hg cat -r "$H2^^^" target + Tainted file now super sanitized + +Can censor after revlog has expanded to no longer permit inline storage + + $ for x in `python $TESTDIR/seq.py 0 50000` + > do + > echo "Password: hunter$x" >> target + > done + $ hg ci -m 'add 100k passwords' + $ H2=`hg id --debug -i` + $ C5=$H2 + $ hg revert -r "$H2^" target + $ hg ci -m 'cleaned 100k passwords' + $ H2=`hg id --debug -i` + $ hg censor -r $C5 target + $ hg cat -r $C5 target + $ hg cat -r $H2 target + fresh start + +Repo with censored nodes can be cloned and cloned nodes are censored + + $ cd .. + $ hg clone r rclone + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd rclone + $ hg cat -r $H1 target + advanced head H1 + $ hg cat -r $H2~5 target + Tainted file now super sanitized + $ hg cat -r $C2 target + $ hg cat -r $C1 target + $ hg cat -r 0 target + Initially untainted file + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 12 changesets, 13 total revisions + +Repo cloned before tainted content introduced can pull censored nodes + + $ cd ../rpull + $ hg cat -r tip target + Initially untainted file + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 1 changesets, 2 total revisions + $ hg pull -r $H1 -r $H2 + pulling from $TESTTMP/r (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 11 changesets with 11 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg update 4 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Tainted file now super sanitized + $ hg cat -r $H1 target + advanced head H1 + $ hg cat -r $H2~5 target + Tainted file now super sanitized + $ hg cat -r $C2 target + $ hg cat -r $C1 target + $ hg cat -r 0 target + Initially untainted file + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 12 changesets, 13 total revisions + +Censored nodes can be pushed if they censor previously unexchanged nodes + + $ echo 'Passwords: hunter2hunter2' > target + $ hg ci -m 're-add password from clone' target + created new head + $ H3=`hg id --debug -i` + $ REV=$H3 + $ echo 'Re-sanitized; nothing to see here' > target + $ hg ci -m 're-sanitized' target + $ H2=`hg id --debug -i` + $ CLEANREV=$H2 + $ hg cat -r $REV target + Passwords: hunter2hunter2 + $ hg censor -r $REV target + $ hg cat -r $REV target + $ hg cat -r $CLEANREV target + Re-sanitized; nothing to see here + $ hg push -f -r $H2 + pushing to $TESTTMP/r (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files (+1 heads) + + $ cd ../r + $ hg cat -r $REV target + $ hg cat -r $CLEANREV target + Re-sanitized; nothing to see here + $ hg update $CLEANREV + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Re-sanitized; nothing to see here + +Censored nodes can be bundled up and unbundled in another repo + + $ hg bundle --base 0 ../pwbundle + 13 changesets found + $ cd ../rclone + $ hg unbundle ../pwbundle + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg cat -r $REV target + $ hg cat -r $CLEANREV target + Re-sanitized; nothing to see here + $ hg update $CLEANREV + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Re-sanitized; nothing to see here + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 14 changesets, 15 total revisions + +Censored nodes can be imported on top of censored nodes, consecutively + + $ hg init ../rimport + $ hg bundle --base 1 ../rimport/splitbundle + 12 changesets found + $ cd ../rimport + $ hg pull -r $H1 -r $H2 ../r + pulling from ../r + adding changesets + adding manifests + adding file changes + added 8 changesets with 10 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg unbundle splitbundle + adding changesets + adding manifests + adding file changes + added 6 changesets with 5 changes to 2 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg update $H2 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat target + Re-sanitized; nothing to see here + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 14 changesets, 15 total revisions + $ cd ../r + +Can import bundle where first revision of a file is censored + + $ hg init ../rinit + $ hg censor -r 0 target + $ hg bundle -r 0 --base null ../rinit/initbundle + 1 changesets found + $ cd ../rinit + $ hg unbundle initbundle + adding changesets + adding manifests + adding file changes + added 1 changesets with 2 changes to 2 files + (run 'hg update' to get a working copy) + $ hg cat -r 0 target diff -r 612ed41ae359 -r e89f909edffa tests/test-children.t --- a/tests/test-children.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-children.t Thu Apr 16 20:57:51 2015 -0500 @@ -122,4 +122,12 @@ summary: 2 +should be compatible with templater (don't pass fctx to displayer) + $ hg children file0 -Tdefault + changeset: 2:8f5eea5023c2 + user: test + date: Thu Jan 01 00:00:02 1970 +0000 + summary: 2 + + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-churn.t --- a/tests/test-churn.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-churn.t Thu Apr 16 20:57:51 2015 -0500 @@ -171,4 +171,27 @@ El Ni\xc3\xb1o 1 *************** (esc) with space 1 *************** +Test --template argument, with backwards compatiblity + + $ hg churn -t '{author|user}' + user1 4 *************************************************************** + user3 3 *********************************************** + user2 2 ******************************** + nino 1 **************** + with 1 **************** + 0 + user4 0 + $ hg churn -T '{author|user}' + user1 4 *************************************************************** + user3 3 *********************************************** + user2 2 ******************************** + nino 1 **************** + with 1 **************** + 0 + user4 0 + $ hg churn -t 'alltogether' + alltogether 11 ********************************************************* + $ hg churn -T 'alltogether' + alltogether 11 ********************************************************* + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-clone.t --- a/tests/test-clone.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-clone.t Thu Apr 16 20:57:51 2015 -0500 @@ -65,9 +65,25 @@ #if hardlink $ hg --debug clone -U . ../c + linking: 1 + linking: 2 + linking: 3 + linking: 4 + linking: 5 + linking: 6 + linking: 7 + linking: 8 linked 8 files #else $ hg --debug clone -U . ../c + linking: 1 + copying: 2 + copying: 3 + copying: 4 + copying: 5 + copying: 6 + copying: 7 + copying: 8 copied 8 files #endif $ cd ../c diff -r 612ed41ae359 -r e89f909edffa tests/test-command-template.t --- a/tests/test-command-template.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-command-template.t Thu Apr 16 20:57:51 2015 -0500 @@ -47,6 +47,9 @@ fourth (second) $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r . second -> fourth + $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7 + 8 t + 7 f Quoting for ui.logtemplate @@ -93,6 +96,10 @@ Default style is like normal output: + $ echo c >> c + $ hg add c + $ hg commit -qm ' ' + $ hg log > log.out $ hg log --style default > style.out $ cmp log.out style.out || diff -u log.out style.out @@ -132,6 +139,8 @@ $ mv $HGRCPATH-bak $HGRCPATH + $ hg --config extensions.strip= strip -q . + Revision with no copies (used to print a traceback): $ hg tip -v --template '\n' @@ -1868,6 +1877,16 @@ o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1 +Upper/lower filters: + + $ hg log -r0 --template '{branch|upper}\n' + DEFAULT + $ hg log -r0 --template '{author|lower}\n' + user name + $ hg log -r0 --template '{date|upper}\n' + abort: template filter 'upper' is not compatible with keyword 'date' + [255] + Error on syntax: $ echo 'x = "f' >> t @@ -1905,6 +1924,11 @@ hg: parse error: unknown function 'foo' [255] +Pass generator object created by template function to filter + + $ hg log -l 1 --template '{if(author, author)|user}\n' + test + Test diff function: $ hg diff -c 8 @@ -2290,6 +2314,14 @@ $ hg log -r 0 --template '{if(branches, "yes", "no")}\n' no +Test get function: + + $ hg log -r 0 --template '{get(extras, "branch")}\n' + default + $ hg log -r 0 --template '{get(files, "should_fail")}\n' + hg: parse error: get() expects a dict as first argument + [255] + Test shortest(node) function: $ echo b > b @@ -2393,6 +2425,10 @@ 2 bar foo 1 baz 0 + $ hg log --template "{rev} {ifcontains('foo', bookmarks, 't', 'f')}\n" + 2 t + 1 f + 0 f Test stringify on sub expressions diff -r 612ed41ae359 -r e89f909edffa tests/test-commandserver.t --- a/tests/test-commandserver.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-commandserver.t Thu Apr 16 20:57:51 2015 -0500 @@ -178,6 +178,7 @@ defaults.commit=-d "0 0" defaults.shelve=--date "0 0" defaults.tag=-d "0 0" + devel.all=true largefiles.usercache=$TESTTMP/.cache/largefiles ui.slash=True ui.interactive=False diff -r 612ed41ae359 -r e89f909edffa tests/test-commit-amend.t --- a/tests/test-commit-amend.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-commit-amend.t Thu Apr 16 20:57:51 2015 -0500 @@ -1096,7 +1096,7 @@ $ hg ci -m add $ $ hg debugrename newdirname/newfile.py - newdirname/newfile.py renamed from olddirname/newfile.py:690b295714aed510803d3020da9c70fca8336def + newdirname/newfile.py renamed from olddirname/newfile.py:690b295714aed510803d3020da9c70fca8336def (glob) $ hg status -C --change . A newdirname/newfile.py $ hg status -C --rev 1 @@ -1115,7 +1115,7 @@ $ echo a >> newdirname/commonfile.py $ hg ci --amend -m bug $ hg debugrename newdirname/newfile.py - newdirname/newfile.py renamed from olddirname/newfile.py:690b295714aed510803d3020da9c70fca8336def + newdirname/newfile.py renamed from olddirname/newfile.py:690b295714aed510803d3020da9c70fca8336def (glob) $ hg debugindex newdirname/newfile.py rev offset length base linkrev nodeid p1 p2 0 0 88 0 3 34a4d536c0c0 000000000000 000000000000 diff -r 612ed41ae359 -r e89f909edffa tests/test-commit-interactive-curses.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-commit-interactive-curses.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,203 @@ +Set up a repo + + $ cat <> $HGRCPATH + > [ui] + > interactive = true + > [experimental] + > crecord = true + > crecordtest = testModeCommands + > EOF + + $ hg init a + $ cd a + +Committing some changes but stopping on the way + + $ echo "a" > a + $ hg add a + $ cat <testModeCommands + > TOGGLE + > X + > EOF + $ hg commit -i -m "a" -d "0 0" + no changes to record + $ hg tip + changeset: -1:000000000000 + tag: tip + user: + date: Thu Jan 01 00:00:00 1970 +0000 + + +Committing some changes + + $ cat <testModeCommands + > X + > EOF + $ hg commit -i -m "a" -d "0 0" + $ hg tip + changeset: 0:cb9a9f314b8b + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: a + +Committing only one file + + $ echo "a" >> a + >>> open('b', 'wb').write("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n") + $ hg add b + $ cat <testModeCommands + > TOGGLE + > KEY_DOWN + > X + > EOF + $ hg commit -i -m "one file" -d "0 0" + $ hg tip + changeset: 1:fb2705a663ea + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: one file + + $ hg cat -r tip a + a + $ cat a + a + a + +Committing only one hunk + +- Untoggle all the hunks, go down to the second file +- unfold it +- go down to second hunk (1 for the first hunk, 1 for the first hunkline, 1 for the second hunk, 1 for the second hunklike) +- toggle the second hunk +- commit + + $ echo "x" > c + $ cat b >> c + $ echo "y" >> c + $ mv c b + $ cat <testModeCommands + > A + > KEY_DOWN + > f + > KEY_DOWN + > KEY_DOWN + > KEY_DOWN + > KEY_DOWN + > TOGGLE + > X + > EOF + $ hg commit -i -m "one hunk" -d "0 0" + $ hg tip + changeset: 2:7d10dfe755a8 + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: one hunk + + $ hg cat -r tip b + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + y + $ cat b + x + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + y + $ hg commit -m "other hunks" + $ hg tip + changeset: 3:a6735021574d + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: other hunks + + $ hg cat -r tip b + x + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + y + +Editing patch of newly added file + + $ cat > editor.sh << '__EOF__' + > cat "$1" | sed "s/first/very/g" > tt + > mv tt "$1" + > __EOF__ + $ cat > newfile << '__EOF__' + > This is the first line + > This is the second line + > This is the third line + > __EOF__ + $ hg add newfile + $ cat <testModeCommands + > f + > KEY_DOWN + > KEY_DOWN + > KEY_DOWN + > e + > X + > EOF + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new + $ hg tip + changeset: 4:6a0a43e9eff5 + tag: tip + user: test + date: Thu Jan 01 00:00:23 1970 +0000 + summary: edit-patch-new + + $ hg cat -r tip newfile + This is the very line + This is the second line + This is the third line + + $ cat newfile + This is the first line + This is the second line + This is the third line + +Newly added files can be selected with the curses interface + + $ hg update -C . + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo "hello" > x + $ hg add x + $ cat <testModeCommands + > TOGGLE + > TOGGLE + > X + > EOF + $ hg st + A x + ? editor.sh + ? testModeCommands + $ hg commit -i -m "newly added file" -d "0 0" + $ hg st + ? editor.sh + ? testModeCommands + diff -r 612ed41ae359 -r e89f909edffa tests/test-commit-interactive.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-commit-interactive.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,1434 @@ +Set up a repo + + $ cat <> $HGRCPATH + > [ui] + > interactive = true + > [extensions] + > record = + > EOF + + $ hg init a + $ cd a + +Select no files + + $ touch empty-rw + $ hg add empty-rw + + $ hg commit -i empty-rw< n + > EOF + diff --git a/empty-rw b/empty-rw + new file mode 100644 + examine changes to 'empty-rw'? [Ynesfdaq?] n + + no changes to record + + $ hg tip -p + changeset: -1:000000000000 + tag: tip + user: + date: Thu Jan 01 00:00:00 1970 +0000 + + + +Select files but no hunks + + $ hg commit -i empty-rw< y + > n + > EOF + diff --git a/empty-rw b/empty-rw + new file mode 100644 + examine changes to 'empty-rw'? [Ynesfdaq?] y + + abort: empty commit message + [255] + + $ hg tip -p + changeset: -1:000000000000 + tag: tip + user: + date: Thu Jan 01 00:00:00 1970 +0000 + + + +Record empty file + + $ hg commit -i -d '0 0' -m empty empty-rw< y + > y + > EOF + diff --git a/empty-rw b/empty-rw + new file mode 100644 + examine changes to 'empty-rw'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 0:c0708cf4e46e + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: empty + + + +Summary shows we updated to the new cset + + $ hg summary + parent: 0:c0708cf4e46e tip + empty + branch: default + commit: (clean) + update: (current) + +Rename empty file + + $ hg mv empty-rw empty-rename + $ hg commit -i -d '1 0' -m rename< y + > EOF + diff --git a/empty-rw b/empty-rename + rename from empty-rw + rename to empty-rename + examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 1:d695e8dcb197 + tag: tip + user: test + date: Thu Jan 01 00:00:01 1970 +0000 + summary: rename + + + +Copy empty file + + $ hg cp empty-rename empty-copy + $ hg commit -i -d '2 0' -m copy< y + > EOF + diff --git a/empty-rename b/empty-copy + copy from empty-rename + copy to empty-copy + examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 2:1d4b90bea524 + tag: tip + user: test + date: Thu Jan 01 00:00:02 1970 +0000 + summary: copy + + + +Delete empty file + + $ hg rm empty-copy + $ hg commit -i -d '3 0' -m delete< y + > EOF + diff --git a/empty-copy b/empty-copy + deleted file mode 100644 + examine changes to 'empty-copy'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 3:b39a238f01a1 + tag: tip + user: test + date: Thu Jan 01 00:00:03 1970 +0000 + summary: delete + + + +Add binary file + + $ hg bundle --base -2 tip.bundle + 1 changesets found + $ hg add tip.bundle + $ hg commit -i -d '4 0' -m binary< y + > EOF + diff --git a/tip.bundle b/tip.bundle + new file mode 100644 + this is a binary file + examine changes to 'tip.bundle'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 4:ad816da3711e + tag: tip + user: test + date: Thu Jan 01 00:00:04 1970 +0000 + summary: binary + + diff -r b39a238f01a1 -r ad816da3711e tip.bundle + Binary file tip.bundle has changed + + +Change binary file + + $ hg bundle --base -2 tip.bundle + 1 changesets found + $ hg commit -i -d '5 0' -m binary-change< y + > EOF + diff --git a/tip.bundle b/tip.bundle + this modifies a binary file (all or nothing) + examine changes to 'tip.bundle'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 5:dccd6f3eb485 + tag: tip + user: test + date: Thu Jan 01 00:00:05 1970 +0000 + summary: binary-change + + diff -r ad816da3711e -r dccd6f3eb485 tip.bundle + Binary file tip.bundle has changed + + +Rename and change binary file + + $ hg mv tip.bundle top.bundle + $ hg bundle --base -2 top.bundle + 1 changesets found + $ hg commit -i -d '6 0' -m binary-change-rename< y + > EOF + diff --git a/tip.bundle b/top.bundle + rename from tip.bundle + rename to top.bundle + this modifies a binary file (all or nothing) + examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 6:7fa44105f5b3 + tag: tip + user: test + date: Thu Jan 01 00:00:06 1970 +0000 + summary: binary-change-rename + + diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle + Binary file tip.bundle has changed + diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle + Binary file top.bundle has changed + + +Add plain file + + $ for i in 1 2 3 4 5 6 7 8 9 10; do + > echo $i >> plain + > done + + $ hg add plain + $ hg commit -i -d '7 0' -m plain plain< y + > y + > EOF + diff --git a/plain b/plain + new file mode 100644 + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -0,0 +1,10 @@ + +1 + +2 + +3 + +4 + +5 + +6 + +7 + +8 + +9 + +10 + record this change to 'plain'? [Ynesfdaq?] y + + $ hg tip -p + changeset: 7:11fb457c1be4 + tag: tip + user: test + date: Thu Jan 01 00:00:07 1970 +0000 + summary: plain + + diff -r 7fa44105f5b3 -r 11fb457c1be4 plain + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/plain Thu Jan 01 00:00:07 1970 +0000 + @@ -0,0 +1,10 @@ + +1 + +2 + +3 + +4 + +5 + +6 + +7 + +8 + +9 + +10 + +Modify end of plain file with username unset + + $ echo 11 >> plain + $ unset HGUSER + $ hg commit -i --config ui.username= -d '8 0' -m end plain + abort: no username supplied + (use "hg config --edit" to set your username) + [255] + + +Modify end of plain file, also test that diffopts are accounted for + + $ HGUSER="test" + $ export HGUSER + $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain < y + > y + > EOF + diff --git a/plain b/plain + 1 hunks, 1 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -8,3 +8,4 @@ 7 + 8 + 9 + 10 + +11 + record this change to 'plain'? [Ynesfdaq?] y + + +Modify end of plain file, no EOL + + $ hg tip --template '{node}' >> plain + $ hg commit -i -d '9 0' -m noeol plain < y + > y + > EOF + diff --git a/plain b/plain + 1 hunks, 1 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -9,3 +9,4 @@ + 9 + 10 + 11 + +7264f99c5f5ff3261504828afa4fb4d406c3af54 + \ No newline at end of file + record this change to 'plain'? [Ynesfdaq?] y + + +Modify end of plain file, add EOL + + $ echo >> plain + $ echo 1 > plain2 + $ hg add plain2 + $ hg commit -i -d '10 0' -m eol plain plain2 < y + > y + > y + > y + > EOF + diff --git a/plain b/plain + 1 hunks, 1 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -9,4 +9,4 @@ + 9 + 10 + 11 + -7264f99c5f5ff3261504828afa4fb4d406c3af54 + \ No newline at end of file + +7264f99c5f5ff3261504828afa4fb4d406c3af54 + record change 1/2 to 'plain'? [Ynesfdaq?] y + + diff --git a/plain2 b/plain2 + new file mode 100644 + examine changes to 'plain2'? [Ynesfdaq?] y + + @@ -0,0 +1,1 @@ + +1 + record change 2/2 to 'plain2'? [Ynesfdaq?] y + +Modify beginning, trim end, record both, add another file to test +changes numbering + + $ rm plain + $ for i in 2 2 3 4 5 6 7 8 9 10; do + > echo $i >> plain + > done + $ echo 2 >> plain2 + + $ hg commit -i -d '10 0' -m begin-and-end plain plain2 < y + > y + > y + > y + > y + > EOF + diff --git a/plain b/plain + 2 hunks, 3 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -1,4 +1,4 @@ + -1 + +2 + 2 + 3 + 4 + record change 1/3 to 'plain'? [Ynesfdaq?] y + + @@ -8,5 +8,3 @@ + 8 + 9 + 10 + -11 + -7264f99c5f5ff3261504828afa4fb4d406c3af54 + record change 2/3 to 'plain'? [Ynesfdaq?] y + + diff --git a/plain2 b/plain2 + 1 hunks, 1 lines changed + examine changes to 'plain2'? [Ynesfdaq?] y + + @@ -1,1 +1,2 @@ + 1 + +2 + record change 3/3 to 'plain2'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 11:21df83db12b8 + tag: tip + user: test + date: Thu Jan 01 00:00:10 1970 +0000 + summary: begin-and-end + + diff -r ddb8b281c3ff -r 21df83db12b8 plain + --- a/plain Thu Jan 01 00:00:10 1970 +0000 + +++ b/plain Thu Jan 01 00:00:10 1970 +0000 + @@ -1,4 +1,4 @@ + -1 + +2 + 2 + 3 + 4 + @@ -8,5 +8,3 @@ + 8 + 9 + 10 + -11 + -7264f99c5f5ff3261504828afa4fb4d406c3af54 + diff -r ddb8b281c3ff -r 21df83db12b8 plain2 + --- a/plain2 Thu Jan 01 00:00:10 1970 +0000 + +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000 + @@ -1,1 +1,2 @@ + 1 + +2 + + +Trim beginning, modify end + + $ rm plain + > for i in 4 5 6 7 8 9 10.new; do + > echo $i >> plain + > done + +Record end + + $ hg commit -i -d '11 0' -m end-only plain < y + > n + > y + > EOF + diff --git a/plain b/plain + 2 hunks, 4 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -1,9 +1,6 @@ + -2 + -2 + -3 + 4 + 5 + 6 + 7 + 8 + 9 + record change 1/2 to 'plain'? [Ynesfdaq?] n + + @@ -4,7 +1,7 @@ + 4 + 5 + 6 + 7 + 8 + 9 + -10 + +10.new + record change 2/2 to 'plain'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 12:99337501826f + tag: tip + user: test + date: Thu Jan 01 00:00:11 1970 +0000 + summary: end-only + + diff -r 21df83db12b8 -r 99337501826f plain + --- a/plain Thu Jan 01 00:00:10 1970 +0000 + +++ b/plain Thu Jan 01 00:00:11 1970 +0000 + @@ -7,4 +7,4 @@ + 7 + 8 + 9 + -10 + +10.new + + +Record beginning + + $ hg commit -i -d '12 0' -m begin-only plain < y + > y + > EOF + diff --git a/plain b/plain + 1 hunks, 3 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -1,6 +1,3 @@ + -2 + -2 + -3 + 4 + 5 + 6 + record this change to 'plain'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 13:bbd45465d540 + tag: tip + user: test + date: Thu Jan 01 00:00:12 1970 +0000 + summary: begin-only + + diff -r 99337501826f -r bbd45465d540 plain + --- a/plain Thu Jan 01 00:00:11 1970 +0000 + +++ b/plain Thu Jan 01 00:00:12 1970 +0000 + @@ -1,6 +1,3 @@ + -2 + -2 + -3 + 4 + 5 + 6 + + +Add to beginning, trim from end + + $ rm plain + $ for i in 1 2 3 4 5 6 7 8 9; do + > echo $i >> plain + > done + +Record end + + $ hg commit -i --traceback -d '13 0' -m end-again plain< y + > n + > y + > EOF + diff --git a/plain b/plain + 2 hunks, 4 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -1,6 +1,9 @@ + +1 + +2 + +3 + 4 + 5 + 6 + 7 + 8 + 9 + record change 1/2 to 'plain'? [Ynesfdaq?] n + + @@ -1,7 +4,6 @@ + 4 + 5 + 6 + 7 + 8 + 9 + -10.new + record change 2/2 to 'plain'? [Ynesfdaq?] y + + +Add to beginning, middle, end + + $ rm plain + $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do + > echo $i >> plain + > done + +Record beginning, middle, and test that format-breaking diffopts are ignored + + $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain < y + > y + > y + > n + > EOF + diff --git a/plain b/plain + 3 hunks, 7 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -1,2 +1,5 @@ + +1 + +2 + +3 + 4 + 5 + record change 1/3 to 'plain'? [Ynesfdaq?] y + + @@ -1,6 +4,8 @@ + 4 + 5 + +5.new + +5.reallynew + 6 + 7 + 8 + 9 + record change 2/3 to 'plain'? [Ynesfdaq?] y + + @@ -3,4 +8,6 @@ + 6 + 7 + 8 + 9 + +10 + +11 + record change 3/3 to 'plain'? [Ynesfdaq?] n + + + $ hg tip -p + changeset: 15:f34a7937ec33 + tag: tip + user: test + date: Thu Jan 01 00:00:14 1970 +0000 + summary: middle-only + + diff -r 82c065d0b850 -r f34a7937ec33 plain + --- a/plain Thu Jan 01 00:00:13 1970 +0000 + +++ b/plain Thu Jan 01 00:00:14 1970 +0000 + @@ -1,5 +1,10 @@ + +1 + +2 + +3 + 4 + 5 + +5.new + +5.reallynew + 6 + 7 + 8 + + +Record end + + $ hg commit -i -d '15 0' -m end-only plain < y + > y + > EOF + diff --git a/plain b/plain + 1 hunks, 2 lines changed + examine changes to 'plain'? [Ynesfdaq?] y + + @@ -9,3 +9,5 @@ + 7 + 8 + 9 + +10 + +11 + record this change to 'plain'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 16:f9900b71a04c + tag: tip + user: test + date: Thu Jan 01 00:00:15 1970 +0000 + summary: end-only + + diff -r f34a7937ec33 -r f9900b71a04c plain + --- a/plain Thu Jan 01 00:00:14 1970 +0000 + +++ b/plain Thu Jan 01 00:00:15 1970 +0000 + @@ -9,3 +9,5 @@ + 7 + 8 + 9 + +10 + +11 + + + $ mkdir subdir + $ cd subdir + $ echo a > a + $ hg ci -d '16 0' -Amsubdir + adding subdir/a + + $ echo a >> a + $ hg commit -i -d '16 0' -m subdir-change a < y + > y + > EOF + diff --git a/subdir/a b/subdir/a + 1 hunks, 1 lines changed + examine changes to 'subdir/a'? [Ynesfdaq?] y + + @@ -1,1 +1,2 @@ + a + +a + record this change to 'subdir/a'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 18:61be427a9deb + tag: tip + user: test + date: Thu Jan 01 00:00:16 1970 +0000 + summary: subdir-change + + diff -r a7ffae4d61cb -r 61be427a9deb subdir/a + --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000 + +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000 + @@ -1,1 +1,2 @@ + a + +a + + + $ echo a > f1 + $ echo b > f2 + $ hg add f1 f2 + + $ hg ci -mz -d '17 0' + + $ echo a >> f1 + $ echo b >> f2 + +Help, quit + + $ hg commit -i < ? + > q + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] ? + + y - yes, record this change + n - no, skip this change + e - edit this change manually + s - skip remaining changes to this file + f - record remaining changes to this file + d - done, skip remaining changes and files + a - record all changes to all remaining files + q - quit, recording no changes + ? - ? (display help) + examine changes to 'subdir/f1'? [Ynesfdaq?] q + + abort: user quit + [255] + +Skip + + $ hg commit -i < s + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] s + + diff --git a/subdir/f2 b/subdir/f2 + 1 hunks, 1 lines changed + examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected + [255] + +No + + $ hg commit -i < n + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] n + + diff --git a/subdir/f2 b/subdir/f2 + 1 hunks, 1 lines changed + examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected + [255] + +f, quit + + $ hg commit -i < f + > q + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] f + + diff --git a/subdir/f2 b/subdir/f2 + 1 hunks, 1 lines changed + examine changes to 'subdir/f2'? [Ynesfdaq?] q + + abort: user quit + [255] + +s, all + + $ hg commit -i -d '18 0' -mx < s + > a + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] s + + diff --git a/subdir/f2 b/subdir/f2 + 1 hunks, 1 lines changed + examine changes to 'subdir/f2'? [Ynesfdaq?] a + + + $ hg tip -p + changeset: 20:b3df3dda369a + tag: tip + user: test + date: Thu Jan 01 00:00:18 1970 +0000 + summary: x + + diff -r 6e02d6c9906d -r b3df3dda369a subdir/f2 + --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000 + +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000 + @@ -1,1 +1,2 @@ + b + +b + + +f + + $ hg commit -i -d '19 0' -my < f + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] f + + + $ hg tip -p + changeset: 21:38ec577f126b + tag: tip + user: test + date: Thu Jan 01 00:00:19 1970 +0000 + summary: y + + diff -r b3df3dda369a -r 38ec577f126b subdir/f1 + --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000 + +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000 + @@ -1,1 +1,2 @@ + a + +a + + +#if execbit + +Preserve chmod +x + + $ chmod +x f1 + $ echo a >> f1 + $ hg commit -i -d '20 0' -mz < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + old mode 100644 + new mode 100755 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -1,2 +1,3 @@ + a + a + +a + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 22:3261adceb075 + tag: tip + user: test + date: Thu Jan 01 00:00:20 1970 +0000 + summary: z + + diff --git a/subdir/f1 b/subdir/f1 + old mode 100644 + new mode 100755 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -1,2 +1,3 @@ + a + a + +a + + +Preserve execute permission on original + + $ echo b >> f1 + $ hg commit -i -d '21 0' -maa < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -1,3 +1,4 @@ + a + a + a + +b + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 23:b429867550db + tag: tip + user: test + date: Thu Jan 01 00:00:21 1970 +0000 + summary: aa + + diff --git a/subdir/f1 b/subdir/f1 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -1,3 +1,4 @@ + a + a + a + +b + + +Preserve chmod -x + + $ chmod -x f1 + $ echo c >> f1 + $ hg commit -i -d '22 0' -mab < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + old mode 100755 + new mode 100644 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -2,3 +2,4 @@ + a + a + b + +c + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 24:0b082130c20a + tag: tip + user: test + date: Thu Jan 01 00:00:22 1970 +0000 + summary: ab + + diff --git a/subdir/f1 b/subdir/f1 + old mode 100755 + new mode 100644 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -2,3 +2,4 @@ + a + a + b + +c + + +#else + +Slightly bogus tests to get almost same repo structure as when x bit is used +- but with different hashes. + +Mock "Preserve chmod +x" + + $ echo a >> f1 + $ hg commit -i -d '20 0' -mz < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -1,2 +1,3 @@ + a + a + +a + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 22:0d463bd428f5 + tag: tip + user: test + date: Thu Jan 01 00:00:20 1970 +0000 + summary: z + + diff --git a/subdir/f1 b/subdir/f1 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -1,2 +1,3 @@ + a + a + +a + + +Mock "Preserve execute permission on original" + + $ echo b >> f1 + $ hg commit -i -d '21 0' -maa < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -1,3 +1,4 @@ + a + a + a + +b + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 23:0eab41a3e524 + tag: tip + user: test + date: Thu Jan 01 00:00:21 1970 +0000 + summary: aa + + diff --git a/subdir/f1 b/subdir/f1 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -1,3 +1,4 @@ + a + a + a + +b + + +Mock "Preserve chmod -x" + + $ chmod -x f1 + $ echo c >> f1 + $ hg commit -i -d '22 0' -mab < y + > y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -2,3 +2,4 @@ + a + a + b + +c + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip --config diff.git=True -p + changeset: 24:f4f718f27b7c + tag: tip + user: test + date: Thu Jan 01 00:00:22 1970 +0000 + summary: ab + + diff --git a/subdir/f1 b/subdir/f1 + --- a/subdir/f1 + +++ b/subdir/f1 + @@ -2,3 +2,4 @@ + a + a + b + +c + + +#endif + + $ cd .. + + +Abort early when a merge is in progress + + $ hg up 4 + 1 files updated, 0 files merged, 6 files removed, 0 files unresolved + + $ touch iwillmergethat + $ hg add iwillmergethat + + $ hg branch thatbranch + marked working directory as branch thatbranch + (branches are permanent and global, did you want a bookmark?) + + $ hg ci -m'new head' + + $ hg up default + 6 files updated, 0 files merged, 2 files removed, 0 files unresolved + + $ hg merge thatbranch + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + + $ hg commit -i -m'will abort' + abort: cannot partially commit a merge (use "hg commit" instead) + [255] + + $ hg up -C + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + +Editing patch (and ignoring trailing text) + + $ cat > editor.sh << '__EOF__' + > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\ + > trailing\nditto' "$1" > tmp + > mv tmp "$1" + > __EOF__ + $ cat > editedfile << '__EOF__' + > This is the first line + > This is the second line + > This is the third line + > __EOF__ + $ hg add editedfile + $ hg commit -medit-patch-1 + $ cat > editedfile << '__EOF__' + > This line has changed + > This change will be committed + > This is the third line + > __EOF__ + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 < y + > e + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 2 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] y + + @@ -1,3 +1,3 @@ + -This is the first line + -This is the second line + +This line has changed + +This change will be committed + This is the third line + record this change to 'editedfile'? [Ynesfdaq?] e + + $ cat editedfile + This line has changed + This change will be committed + This is the third line + $ hg cat -r tip editedfile + This is the first line + This change will be committed + This is the third line + $ hg revert editedfile + +Trying to edit patch for whole file + + $ echo "This is the fourth line" >> editedfile + $ hg commit -i < e + > q + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 1 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] e + + cannot edit patch for whole file + examine changes to 'editedfile'? [Ynesfdaq?] q + + abort: user quit + [255] + $ hg revert editedfile + +Removing changes from patch + + $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp + $ mv tmp editedfile + $ echo "This line has been added" >> editedfile + $ cat > editor.sh << '__EOF__' + > sed -e 's/^[-+]/ /' "$1" > tmp + > mv tmp "$1" + > __EOF__ + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i < y + > e + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 3 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] y + + @@ -1,3 +1,3 @@ + -This is the first line + -This change will be committed + -This is the third line + +This change will not be committed + +This is the second line + +This line has been added + record this change to 'editedfile'? [Ynesfdaq?] e + + no changes to record + $ cat editedfile + This change will not be committed + This is the second line + This line has been added + $ hg cat -r tip editedfile + This is the first line + This change will be committed + This is the third line + $ hg revert editedfile + +Invalid patch + + $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp + $ mv tmp editedfile + $ echo "This line has been added" >> editedfile + $ cat > editor.sh << '__EOF__' + > sed s/This/That/ "$1" > tmp + > mv tmp "$1" + > __EOF__ + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i < y + > e + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 3 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] y + + @@ -1,3 +1,3 @@ + -This is the first line + -This change will be committed + -This is the third line + +This change will not be committed + +This is the second line + +This line has been added + record this change to 'editedfile'? [Ynesfdaq?] e + + patching file editedfile + Hunk #1 FAILED at 0 + 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej + abort: patch failed to apply + [255] + $ cat editedfile + This change will not be committed + This is the second line + This line has been added + $ hg cat -r tip editedfile + This is the first line + This change will be committed + This is the third line + $ cat editedfile.rej + --- editedfile + +++ editedfile + @@ -1,3 +1,3 @@ + -That is the first line + -That change will be committed + -That is the third line + +That change will not be committed + +That is the second line + +That line has been added + +Malformed patch - error handling + + $ cat > editor.sh << '__EOF__' + > sed -e '/^@/p' "$1" > tmp + > mv tmp "$1" + > __EOF__ + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i < y + > e + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 3 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] y + + @@ -1,3 +1,3 @@ + -This is the first line + -This change will be committed + -This is the third line + +This change will not be committed + +This is the second line + +This line has been added + record this change to 'editedfile'? [Ynesfdaq?] e + + abort: error parsing patch: unhandled transition: range -> range + [255] + +random text in random positions is still an error + + $ cat > editor.sh << '__EOF__' + > sed -e '/^@/i\ + > other' "$1" > tmp + > mv tmp "$1" + > __EOF__ + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i < y + > e + > EOF + diff --git a/editedfile b/editedfile + 1 hunks, 3 lines changed + examine changes to 'editedfile'? [Ynesfdaq?] y + + @@ -1,3 +1,3 @@ + -This is the first line + -This change will be committed + -This is the third line + +This change will not be committed + +This is the second line + +This line has been added + record this change to 'editedfile'? [Ynesfdaq?] e + + abort: error parsing patch: unhandled transition: file -> other + [255] + + $ hg up -C + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + +With win32text + + $ echo '[extensions]' >> .hg/hgrc + $ echo 'win32text = ' >> .hg/hgrc + $ echo '[decode]' >> .hg/hgrc + $ echo '** = cleverdecode:' >> .hg/hgrc + $ echo '[encode]' >> .hg/hgrc + $ echo '** = cleverencode:' >> .hg/hgrc + $ echo '[patch]' >> .hg/hgrc + $ echo 'eol = crlf' >> .hg/hgrc + +Ignore win32text deprecation warning for now: + + $ echo '[win32text]' >> .hg/hgrc + $ echo 'warn = no' >> .hg/hgrc + + $ echo d >> subdir/f1 + $ hg commit -i -d '24 0' -mw1 < y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -3,3 +3,4 @@ + a + b + c + +d + record this change to 'subdir/f1'? [Ynesfdaq?] y + + + $ hg tip -p + changeset: 28:* (glob) + tag: tip + user: test + date: Thu Jan 01 00:00:24 1970 +0000 + summary: w1 + + diff -r ???????????? -r ???????????? subdir/f1 (glob) + --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000 + +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000 + @@ -3,3 +3,4 @@ + a + b + c + +d + + + +Test --user when ui.username not set + $ unset HGUSER + $ echo e >> subdir/f1 + $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" < y + > y + > EOF + diff --git a/subdir/f1 b/subdir/f1 + 1 hunks, 1 lines changed + examine changes to 'subdir/f1'? [Ynesfdaq?] y + + @@ -4,3 +4,4 @@ + b + c + d + +e + record this change to 'subdir/f1'? [Ynesfdaq?] y + + $ hg log --template '{author}\n' -l 1 + xyz + $ HGUSER="test" + $ export HGUSER + + +Editing patch of newly added file + + $ cat > editor.sh << '__EOF__' + > cat "$1" | sed "s/first/very/g" > tt + > mv tt "$1" + > __EOF__ + $ cat > newfile << '__EOF__' + > This is the first line + > This is the second line + > This is the third line + > __EOF__ + $ hg add newfile + $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new < y + > e + > EOF + diff --git a/newfile b/newfile + new file mode 100644 + examine changes to 'newfile'? [Ynesfdaq?] y + + @@ -0,0 +1,3 @@ + +This is the first line + +This is the second line + +This is the third line + record this change to 'newfile'? [Ynesfdaq?] e + + $ hg cat -r tip newfile + This is the very line + This is the second line + This is the third line + + $ cat newfile + This is the first line + This is the second line + This is the third line + + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-completion.t --- a/tests/test-completion.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-completion.t Thu Apr 16 20:57:51 2015 -0500 @@ -202,8 +202,8 @@ add: include, exclude, subrepos, dry-run annotate: rev, follow, no-follow, text, user, file, date, number, changeset, line-number, ignore-all-space, ignore-space-change, ignore-blank-lines, include, exclude, template clone: noupdate, updaterev, rev, branch, pull, uncompressed, ssh, remotecmd, insecure - commit: addremove, close-branch, amend, secret, edit, include, exclude, message, logfile, date, user, subrepos - diff: rev, change, text, git, nodates, noprefix, show-function, reverse, ignore-all-space, ignore-space-change, ignore-blank-lines, unified, stat, include, exclude, subrepos + commit: addremove, close-branch, amend, secret, edit, interactive, include, exclude, message, logfile, date, user, subrepos + diff: rev, change, text, git, nodates, noprefix, show-function, reverse, ignore-all-space, ignore-space-change, ignore-blank-lines, unified, stat, root, include, exclude, subrepos export: output, switch-parent, rev, text, git, nodates forget: include, exclude init: ssh, remotecmd, insecure @@ -262,13 +262,13 @@ debugsuccessorssets: debugwalk: include, exclude debugwireargs: three, four, five, ssh, remotecmd, insecure - files: rev, print0, include, exclude, template + files: rev, print0, include, exclude, template, subrepos graft: rev, continue, edit, log, force, 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, keyword identify: rev, num, id, branch, tags, bookmarks, ssh, remotecmd, insecure - import: strip, base, edit, force, no-commit, bypass, partial, exact, import-branch, message, logfile, date, user, similarity + import: strip, base, edit, force, no-commit, bypass, partial, exact, prefix, import-branch, message, logfile, date, user, similarity incoming: force, newest-first, bundle, rev, bookmarks, branch, patch, git, limit, no-merges, stat, graph, style, template, ssh, remotecmd, insecure, subrepos locate: rev, print0, fullpath, include, exclude manifest: rev, all, template @@ -278,8 +278,8 @@ phase: public, draft, secret, force, rev recover: rename: after, force, include, exclude, dry-run - resolve: all, list, mark, unmark, no-status, tool, include, exclude - revert: all, date, rev, no-backup, include, exclude, dry-run + resolve: all, list, mark, unmark, no-status, tool, include, exclude, template + revert: all, date, rev, no-backup, interactive, include, exclude, dry-run rollback: dry-run, force root: tag: force, local, rev, remove, edit, message, date, user diff -r 612ed41ae359 -r e89f909edffa tests/test-context.py --- a/tests/test-context.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-context.py Thu Apr 16 20:57:51 2015 -0500 @@ -51,7 +51,7 @@ for d in ctxb.diff(ctxa, git=True): print d -# test safeness and correctness of "cxt.status()" +# test safeness and correctness of "ctx.status()" print '= checking context.status():' # ancestor "wcctx ~ 2" diff -r 612ed41ae359 -r e89f909edffa tests/test-convert-cvs.t --- a/tests/test-convert-cvs.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-convert-cvs.t Thu Apr 16 20:57:51 2015 -0500 @@ -397,11 +397,12 @@ Author: * (glob) Branch: HEAD Tag: (none) + Branchpoints: branch Log: ci1 Members: - b/c:1.2->1.3 + a:1.1->1.2 --------------------- PatchSet 6 @@ -409,12 +410,11 @@ Author: * (glob) Branch: HEAD Tag: (none) - Branchpoints: branch Log: ci1 Members: - a:1.1->1.2 + b/c:1.2->1.3 --------------------- PatchSet 7 diff -r 612ed41ae359 -r e89f909edffa tests/test-convert-datesort.t --- a/tests/test-convert-datesort.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-convert-datesort.t Thu Apr 16 20:57:51 2015 -0500 @@ -85,9 +85,9 @@ $ hg -R t-datesort log -G --template '{rev} "{desc}"\n' o 12 "c1" |\ - | o 11 "b2x" + | _ 11 "b2x" | | - | | o 10 "a7x" + | | _ 10 "a7x" | | | o | | 9 "c0" | | | @@ -136,9 +136,9 @@ $ hg -R t-sourcesort log -G --template '{rev} "{desc}"\n' o 12 "c1" |\ - | o 11 "b2x" + | _ 11 "b2x" | | - | | o 10 "a7x" + | | _ 10 "a7x" | | | o | | 9 "c0" | | | @@ -189,11 +189,11 @@ |\ | o 11 "c0" | | - o | 10 "b2x" + _ | 10 "b2x" | | o | 9 "b1" | | - | | o 8 "a7x" + | | _ 8 "a7x" | | | | | o 7 "a6" | | | diff -r 612ed41ae359 -r e89f909edffa tests/test-convert-filemap.t --- a/tests/test-convert-filemap.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-convert-filemap.t Thu Apr 16 20:57:51 2015 -0500 @@ -387,15 +387,15 @@ 1 addb 0 closedefault $ glog -R branchpruning-hg1 - o 5 "closedefault" files: + _ 5 "closedefault" files: | o 4 "addb" files: b | - | o 3 "closeempty" files: + | _ 3 "closeempty" files: | | | o 2 "emptybranch" files: |/ - | o 1 "closefoo" files: + | _ 1 "closefoo" files: |/ o 0 "adda" files: a @@ -422,7 +422,7 @@ 1 closeempty 0 closedefault $ glog -R branchpruning-hg2 - o 1 "closedefault" files: + _ 1 "closedefault" files: | o 0 "addb" files: b diff -r 612ed41ae359 -r e89f909edffa tests/test-convert-git.t --- a/tests/test-convert-git.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-convert-git.t Thu Apr 16 20:57:51 2015 -0500 @@ -170,7 +170,79 @@ full conversion - $ hg -q convert --datesort git-repo2 fullrepo + $ hg convert --datesort git-repo2 fullrepo \ + > --config extensions.progress= --config progress.assume-tty=1 \ + > --config progress.delay=0 --config progress.changedelay=0 \ + > --config progress.refresh=0 --config progress.width=60 + \r (no-eol) (esc) + scanning [===> ] 1/9\r (no-eol) (esc) + scanning [========> ] 2/9\r (no-eol) (esc) + scanning [=============> ] 3/9\r (no-eol) (esc) + scanning [==================> ] 4/9\r (no-eol) (esc) + scanning [=======================> ] 5/9\r (no-eol) (esc) + scanning [============================> ] 6/9\r (no-eol) (esc) + scanning [=================================> ] 7/9\r (no-eol) (esc) + scanning [======================================> ] 8/9\r (no-eol) (esc) + scanning [===========================================>] 9/9\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [ ] 0/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [===> ] 1/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [========> ] 2/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [=============> ] 3/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [=================> ] 4/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [======================> ] 5/9\r (no-eol) (esc) + getting files [===> ] 1/8\r (no-eol) (esc) + getting files [========> ] 2/8\r (no-eol) (esc) + getting files [=============> ] 3/8\r (no-eol) (esc) + getting files [==================> ] 4/8\r (no-eol) (esc) + getting files [=======================> ] 5/8\r (no-eol) (esc) + getting files [============================> ] 6/8\r (no-eol) (esc) + getting files [=================================> ] 7/8\r (no-eol) (esc) + getting files [======================================>] 8/8\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [===========================> ] 6/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [===============================> ] 7/9\r (no-eol) (esc) + getting files [======================================>] 1/1\r (no-eol) (esc) + \r (no-eol) (esc) + \r (no-eol) (esc) + converting [====================================> ] 8/9\r (no-eol) (esc) + getting files [==================> ] 1/2\r (no-eol) (esc) + getting files [======================================>] 2/2\r (no-eol) (esc) + \r (no-eol) (esc) + initializing destination fullrepo repository + scanning source... + sorting... + converting... + 8 add foo + 7 change foo + 6 add quux + 5 add bar + 4 add baz + 3 Octopus merge + 2 change bar + 1 change foo + 0 Discard change to foo + updating bookmarks $ hg up -q -R fullrepo $ glog -R fullrepo @ 9 "Discard change to foo" files: foo diff -r 612ed41ae359 -r e89f909edffa tests/test-convert-svn-encoding.t --- a/tests/test-convert-svn-encoding.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-convert-svn-encoding.t Thu Apr 16 20:57:51 2015 -0500 @@ -53,7 +53,6 @@ source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@1 converting: 0/6 revisions (0.00%) committing changelog - couldn't read revision branch cache names: * (glob) 4 hello source: svn:afeb9c47-92ff-4c0c-9f72-e1f6eb8ac9af/trunk@2 converting: 1/6 revisions (16.67%) diff -r 612ed41ae359 -r e89f909edffa tests/test-copy.t --- a/tests/test-copy.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-copy.t Thu Apr 16 20:57:51 2015 -0500 @@ -138,7 +138,7 @@ moving a missing file $ rm foo $ hg mv foo foo3 - foo: deleted in working copy + foo: deleted in working directory foo3 does not exist! $ hg up -qC . diff -r 612ed41ae359 -r e89f909edffa tests/test-debugcommands.t --- a/tests/test-debugcommands.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-debugcommands.t Thu Apr 16 20:57:51 2015 -0500 @@ -18,6 +18,7 @@ deltas : 0 ( 0.00%) avg chain length : 0 + max chain length : 0 compression ratio : 0 uncompressed data size (min/max/avg) : 43 / 43 / 43 diff -r 612ed41ae359 -r e89f909edffa tests/test-devel-warnings.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-devel-warnings.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,90 @@ + + $ cat << EOF > buggylocking.py + > """A small extension that acquire locks in the wrong order + > """ + > + > from mercurial import cmdutil + > + > cmdtable = {} + > command = cmdutil.command(cmdtable) + > + > @command('buggylocking', [], '') + > def buggylocking(ui, repo): + > tr = repo.transaction('buggy') + > lo = repo.lock() + > wl = repo.wlock() + > wl.release() + > lo.release() + > + > @command('properlocking', [], '') + > def properlocking(ui, repo): + > """check that reentrance is fine""" + > wl = repo.wlock() + > lo = repo.lock() + > tr = repo.transaction('proper') + > tr2 = repo.transaction('proper') + > lo2 = repo.lock() + > wl2 = repo.wlock() + > wl2.release() + > lo2.release() + > tr2.close() + > tr.close() + > lo.release() + > wl.release() + > + > @command('nowaitlocking', [], '') + > def nowaitlocking(ui, repo): + > lo = repo.lock() + > wl = repo.wlock(wait=False) + > wl.release() + > lo.release() + > EOF + + $ cat << EOF >> $HGRCPATH + > [extensions] + > buggylocking=$TESTTMP/buggylocking.py + > [devel] + > all=1 + > EOF + + $ hg init lock-checker + $ cd lock-checker + $ hg buggylocking + devel-warn: transaction with no lock at: $TESTTMP/buggylocking.py:11 (buggylocking) + devel-warn: "wlock" acquired after "lock" at: $TESTTMP/buggylocking.py:13 (buggylocking) + $ cat << EOF >> $HGRCPATH + > [devel] + > all=0 + > check-locks=1 + > EOF + $ hg buggylocking + devel-warn: transaction with no lock at: $TESTTMP/buggylocking.py:11 (buggylocking) + devel-warn: "wlock" acquired after "lock" at: $TESTTMP/buggylocking.py:13 (buggylocking) + $ hg buggylocking --traceback + devel-warn: transaction with no lock at: + */hg:* in * (glob) + */mercurial/dispatch.py:* in run (glob) + */mercurial/dispatch.py:* in dispatch (glob) + */mercurial/dispatch.py:* in _runcatch (glob) + */mercurial/dispatch.py:* in _dispatch (glob) + */mercurial/dispatch.py:* in runcommand (glob) + */mercurial/dispatch.py:* in _runcommand (glob) + */mercurial/dispatch.py:* in checkargs (glob) + */mercurial/dispatch.py:* in (glob) + */mercurial/util.py:* in check (glob) + $TESTTMP/buggylocking.py:* in buggylocking (glob) + devel-warn: "wlock" acquired after "lock" at: + */hg:* in * (glob) + */mercurial/dispatch.py:* in run (glob) + */mercurial/dispatch.py:* in dispatch (glob) + */mercurial/dispatch.py:* in _runcatch (glob) + */mercurial/dispatch.py:* in _dispatch (glob) + */mercurial/dispatch.py:* in runcommand (glob) + */mercurial/dispatch.py:* in _runcommand (glob) + */mercurial/dispatch.py:* in checkargs (glob) + */mercurial/dispatch.py:* in (glob) + */mercurial/util.py:* in check (glob) + $TESTTMP/buggylocking.py:* in buggylocking (glob) + $ hg properlocking + $ hg nowaitlocking + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-diff-subdir.t --- a/tests/test-diff-subdir.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-diff-subdir.t Thu Apr 16 20:57:51 2015 -0500 @@ -44,4 +44,24 @@ @@ -0,0 +1,1 @@ +2 +relative to beta + $ cd .. + $ hg diff --nodates --root beta + diff -r 7d5ef1aea329 two + --- a/two + +++ b/two + @@ -0,0 +1,1 @@ + +2 + +inside beta + + $ cd beta + $ hg diff --nodates --root . + diff -r 7d5ef1aea329 two + --- a/two + +++ b/two + @@ -0,0 +1,1 @@ + +2 + + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-diff-unified.t --- a/tests/test-diff-unified.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-diff-unified.t Thu Apr 16 20:57:51 2015 -0500 @@ -286,4 +286,51 @@ -b +a +showfunc diff + $ cat > f1 << EOF + > int main() { + > int a = 0; + > int b = 1; + > int c = 2; + > int d = 3; + > return a + b + c + d; + > } + > EOF + $ hg commit -m addfunction + $ cat > f1 << EOF + > int main() { + > int a = 0; + > int b = 1; + > int c = 2; + > int e = 3; + > return a + b + c + e; + > } + > EOF + $ hg diff --git + diff --git a/f1 b/f1 + --- a/f1 + +++ b/f1 + @@ -2,6 +2,6 @@ + int a = 0; + int b = 1; + int c = 2; + - int d = 3; + - return a + b + c + d; + + int e = 3; + + return a + b + c + e; + } + $ hg diff --config diff.showfunc=True --git + diff --git a/f1 b/f1 + --- a/f1 + +++ b/f1 + @@ -2,6 +2,6 @@ int main() { + int a = 0; + int b = 1; + int c = 2; + - int d = 3; + - return a + b + c + d; + + int e = 3; + + return a + b + c + e; + } + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-diffstat.t --- a/tests/test-diffstat.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-diffstat.t Thu Apr 16 20:57:51 2015 -0500 @@ -69,4 +69,39 @@ file with spaces | Bin 1 files changed, 0 insertions(+), 0 deletions(-) +diffstat within directories: + + $ hg rm -f 'file with spaces' + + $ mkdir dir1 dir2 + $ echo new1 > dir1/new + $ echo new2 > dir2/new + $ hg add dir1/new dir2/new + $ hg diff --stat + dir1/new | 1 + + dir2/new | 1 + + 2 files changed, 2 insertions(+), 0 deletions(-) + + $ hg diff --stat --root dir1 + new | 1 + + 1 files changed, 1 insertions(+), 0 deletions(-) + + $ hg diff --stat --root dir1 dir2 + warning: dir2 not inside relative root dir1 + + $ hg diff --stat --root dir1 -I dir1/old + + $ cd dir1 + $ hg diff --stat . + dir1/new | 1 + + 1 files changed, 1 insertions(+), 0 deletions(-) + $ hg diff --stat --root . + new | 1 + + 1 files changed, 1 insertions(+), 0 deletions(-) + + $ hg diff --stat --root ../dir1 ../dir2 + warning: ../dir2 not inside relative root . (glob) + + $ hg diff --stat --root . -I old + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-doctest.py --- a/tests/test-doctest.py Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-doctest.py Thu Apr 16 20:57:51 2015 -0500 @@ -19,6 +19,7 @@ testmod('mercurial.hgweb.hgwebdir_mod') testmod('mercurial.match') testmod('mercurial.minirst') +testmod('mercurial.patch') testmod('mercurial.pathutil') testmod('mercurial.revset') testmod('mercurial.store') diff -r 612ed41ae359 -r e89f909edffa tests/test-extension.t --- a/tests/test-extension.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-extension.t Thu Apr 16 20:57:51 2015 -0500 @@ -946,6 +946,9 @@ Declare the version as supporting this hg version, show regular bts link: $ hgver=`$PYTHON -c 'from mercurial import util; print util.version().split("+")[0]'` $ echo 'testedwith = """'"$hgver"'"""' >> throw.py + $ if [ -z "$hgver" ]; then + > echo "unable to fetch a mercurial version. Make sure __version__ is correct"; + > fi $ rm -f throw.pyc throw.pyo $ hg --config extensions.throw=throw.py throw 2>&1 | egrep '^\*\*' ** unknown exception encountered, please report by visiting @@ -1140,3 +1143,27 @@ C sub3/3 $ cd .. + +Test synopsis and docstring extending + + $ hg init exthelp + $ cat > exthelp.py < from mercurial import commands, extensions + > def exbookmarks(orig, *args, **opts): + > return orig(*args, **opts) + > def uisetup(ui): + > synopsis = ' GREPME [--foo] [-x]' + > docstring = ''' + > GREPME make sure that this is in the help! + > ''' + > extensions.wrapcommand(commands.table, 'bookmarks', exbookmarks, + > synopsis, docstring) + > EOF + $ abspath=`pwd`/exthelp.py + $ echo '[extensions]' >> $HGRCPATH + $ echo "exthelp = $abspath" >> $HGRCPATH + $ cd exthelp + $ hg help bookmarks | grep GREPME + hg bookmarks [OPTIONS]... [NAME]... GREPME [--foo] [-x] + GREPME make sure that this is in the help! + diff -r 612ed41ae359 -r e89f909edffa tests/test-fetch.t --- a/tests/test-fetch.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-fetch.t Thu Apr 16 20:57:51 2015 -0500 @@ -339,7 +339,8 @@ marked working directory as branch topic (branches are permanent and global, did you want a bookmark?) $ hg -R n2 fetch -m merge n1 - abort: working dir not at branch tip (use "hg update" to check out branch tip) + abort: working directory not at branch tip + (use "hg update" to check out branch tip) [255] parent should be 0 (fetch did not update or merge anything) diff -r 612ed41ae359 -r e89f909edffa tests/test-fileset-generated.t --- a/tests/test-fileset-generated.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-fileset-generated.t Thu Apr 16 20:57:51 2015 -0500 @@ -141,39 +141,34 @@ Test revert -BROKEN: the files that get undeleted were not modified, they were removed, -and content1_content2_missing-tracked was also not modified, it was deleted - $ hg revert 'set:modified()' reverting content1_content1_content3-tracked reverting content1_content2_content1-tracked - undeleting content1_content2_content1-untracked - undeleting content1_content2_content2-untracked reverting content1_content2_content3-tracked - undeleting content1_content2_content3-untracked - reverting content1_content2_missing-tracked - undeleting content1_content2_missing-untracked reverting missing_content2_content3-tracked -BROKEN: only the files that get forgotten are correct - $ hg revert 'set:added()' forgetting content1_missing_content1-tracked forgetting content1_missing_content3-tracked - undeleting missing_content2_content2-untracked - undeleting missing_content2_content3-untracked - reverting missing_content2_missing-tracked - undeleting missing_content2_missing-untracked forgetting missing_missing_content3-tracked $ hg revert 'set:removed()' undeleting content1_content1_content1-untracked undeleting content1_content1_content3-untracked undeleting content1_content1_missing-untracked + undeleting content1_content2_content1-untracked + undeleting content1_content2_content2-untracked + undeleting content1_content2_content3-untracked + undeleting content1_content2_missing-untracked + undeleting missing_content2_content2-untracked + undeleting missing_content2_content3-untracked + undeleting missing_content2_missing-untracked $ hg revert 'set:deleted()' reverting content1_content1_missing-tracked + reverting content1_content2_missing-tracked forgetting content1_missing_missing-tracked + reverting missing_content2_missing-tracked forgetting missing_missing_missing-tracked $ hg revert 'set:unknown()' diff -r 612ed41ae359 -r e89f909edffa tests/test-fileset.t --- a/tests/test-fileset.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-fileset.t Thu Apr 16 20:57:51 2015 -0500 @@ -111,6 +111,13 @@ $ hg add b2link #endif +#if no-windows + $ echo foo > con.xml + $ fileset 'not portable()' + con.xml + $ hg --config ui.portablefilenames=ignore add con.xml +#endif + >>> file('1k', 'wb').write(' '*1024) >>> file('2k', 'wb').write(' '*2048) $ hg add 1k 2k @@ -220,6 +227,12 @@ b2link #endif +#if no-windows + $ fileset -r1 'not portable()' + con.xml + $ hg forget 'con.xml' +#endif + $ fileset -r4 'subrepo("re:su.*")' sub $ fileset -r4 'subrepo("sub")' diff -r 612ed41ae359 -r e89f909edffa tests/test-gendoc.t --- a/tests/test-gendoc.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-gendoc.t Thu Apr 16 20:57:51 2015 -0500 @@ -1,4 +1,5 @@ #require docutils +#require gettext Test document extraction diff -r 612ed41ae359 -r e89f909edffa tests/test-getbundle.t --- a/tests/test-getbundle.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-getbundle.t Thu Apr 16 20:57:51 2015 -0500 @@ -170,7 +170,7 @@ $ hg debuggetbundle repo bundle -t bundle2 $ hg debugbundle bundle Stream params: {} - b2x:changegroup -- "{'version': '01'}" + changegroup -- "{'version': '01'}" 7704483d56b2a7b5db54dcee7c62378ac629b348 29a4d1f17bd3f0779ca0525bebb1cfb51067c738 713346a995c363120712aed1aee7e04afd867638 diff -r 612ed41ae359 -r e89f909edffa tests/test-git-export.t --- a/tests/test-git-export.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-git-export.t Thu Apr 16 20:57:51 2015 -0500 @@ -5,46 +5,279 @@ New file: - $ echo new > new + $ mkdir dir1 + $ echo new > dir1/new $ hg ci -Amnew - adding new + adding dir1/new $ hg diff --git -r 0 - diff --git a/new b/new + diff --git a/dir1/new b/dir1/new new file mode 100644 --- /dev/null - +++ b/new + +++ b/dir1/new @@ -0,0 +1,1 @@ +new Copy: - $ hg cp new copy + $ mkdir dir2 + $ hg cp dir1/new dir1/copy + $ echo copy1 >> dir1/copy + $ hg cp dir1/new dir2/copy + $ echo copy2 >> dir2/copy $ hg ci -mcopy $ hg diff --git -r 1:tip + diff --git a/dir1/new b/dir1/copy + copy from dir1/new + copy to dir1/copy + --- a/dir1/new + +++ b/dir1/copy + @@ -1,1 +1,2 @@ + new + +copy1 + diff --git a/dir1/new b/dir2/copy + copy from dir1/new + copy to dir2/copy + --- a/dir1/new + +++ b/dir2/copy + @@ -1,1 +1,2 @@ + new + +copy2 + +Cross and same-directory copies with a relative root: + + $ hg diff --git --root .. -r 1:tip + abort: .. not under root '$TESTTMP' + [255] + $ hg diff --git --root doesnotexist -r 1:tip + $ hg diff --git --root . -r 1:tip + diff --git a/dir1/new b/dir1/copy + copy from dir1/new + copy to dir1/copy + --- a/dir1/new + +++ b/dir1/copy + @@ -1,1 +1,2 @@ + new + +copy1 + diff --git a/dir1/new b/dir2/copy + copy from dir1/new + copy to dir2/copy + --- a/dir1/new + +++ b/dir2/copy + @@ -1,1 +1,2 @@ + new + +copy2 + $ hg diff --git --root dir1 -r 1:tip + diff --git a/new b/copy + copy from new + copy to copy + --- a/new + +++ b/copy + @@ -1,1 +1,2 @@ + new + +copy1 + + $ hg diff --git --root dir2/ -r 1:tip + diff --git a/copy b/copy + new file mode 100644 + --- /dev/null + +++ b/copy + @@ -0,0 +1,2 @@ + +new + +copy2 + + $ hg diff --git --root dir1 -r 1:tip -I '**/copy' diff --git a/new b/copy copy from new copy to copy + --- a/new + +++ b/copy + @@ -1,1 +1,2 @@ + new + +copy1 + + $ hg diff --git --root dir1 -r 1:tip dir2 + warning: dir2 not inside relative root dir1 + + $ hg diff --git --root dir1 -r 1:tip 'dir2/{copy}' + warning: dir2/{copy} not inside relative root dir1 (glob) + + $ cd dir1 + $ hg diff --git --root .. -r 1:tip + diff --git a/dir1/new b/dir1/copy + copy from dir1/new + copy to dir1/copy + --- a/dir1/new + +++ b/dir1/copy + @@ -1,1 +1,2 @@ + new + +copy1 + diff --git a/dir1/new b/dir2/copy + copy from dir1/new + copy to dir2/copy + --- a/dir1/new + +++ b/dir2/copy + @@ -1,1 +1,2 @@ + new + +copy2 + + $ hg diff --git --root ../.. -r 1:tip + abort: ../.. not under root '$TESTTMP' + [255] + $ hg diff --git --root ../doesnotexist -r 1:tip + $ hg diff --git --root .. -r 1:tip + diff --git a/dir1/new b/dir1/copy + copy from dir1/new + copy to dir1/copy + --- a/dir1/new + +++ b/dir1/copy + @@ -1,1 +1,2 @@ + new + +copy1 + diff --git a/dir1/new b/dir2/copy + copy from dir1/new + copy to dir2/copy + --- a/dir1/new + +++ b/dir2/copy + @@ -1,1 +1,2 @@ + new + +copy2 + + $ hg diff --git --root . -r 1:tip + diff --git a/new b/copy + copy from new + copy to copy + --- a/new + +++ b/copy + @@ -1,1 +1,2 @@ + new + +copy1 + $ hg diff --git --root . -r 1:tip copy + diff --git a/new b/copy + copy from new + copy to copy + --- a/new + +++ b/copy + @@ -1,1 +1,2 @@ + new + +copy1 + $ hg diff --git --root . -r 1:tip ../dir2 + warning: ../dir2 not inside relative root . (glob) + $ hg diff --git --root . -r 1:tip '../dir2/*' + warning: ../dir2/* not inside relative root . (glob) + $ cd .. Rename: - $ hg mv copy rename + $ hg mv dir1/copy dir1/rename1 + $ echo rename1 >> dir1/rename1 + $ hg mv dir2/copy dir1/rename2 + $ echo rename2 >> dir1/rename2 $ hg ci -mrename $ hg diff --git -r 2:tip - diff --git a/copy b/rename + diff --git a/dir1/copy b/dir1/rename1 + rename from dir1/copy + rename to dir1/rename1 + --- a/dir1/copy + +++ b/dir1/rename1 + @@ -1,2 +1,3 @@ + new + copy1 + +rename1 + diff --git a/dir2/copy b/dir1/rename2 + rename from dir2/copy + rename to dir1/rename2 + --- a/dir2/copy + +++ b/dir1/rename2 + @@ -1,2 +1,3 @@ + new + copy2 + +rename2 + +Cross and same-directory renames with a relative root: + + $ hg diff --root dir1 --git -r 2:tip + diff --git a/copy b/rename1 rename from copy - rename to rename + rename to rename1 + --- a/copy + +++ b/rename1 + @@ -1,2 +1,3 @@ + new + copy1 + +rename1 + diff --git a/rename2 b/rename2 + new file mode 100644 + --- /dev/null + +++ b/rename2 + @@ -0,0 +1,3 @@ + +new + +copy2 + +rename2 + + $ hg diff --root dir2 --git -r 2:tip + diff --git a/copy b/copy + deleted file mode 100644 + --- a/copy + +++ /dev/null + @@ -1,2 +0,0 @@ + -new + -copy2 + + $ hg diff --root dir1 --git -r 2:tip -I '**/copy' + diff --git a/copy b/copy + deleted file mode 100644 + --- a/copy + +++ /dev/null + @@ -1,2 +0,0 @@ + -new + -copy1 + + $ hg diff --root dir1 --git -r 2:tip -I '**/rename*' + diff --git a/copy b/rename1 + copy from copy + copy to rename1 + --- a/copy + +++ b/rename1 + @@ -1,2 +1,3 @@ + new + copy1 + +rename1 + diff --git a/rename2 b/rename2 + new file mode 100644 + --- /dev/null + +++ b/rename2 + @@ -0,0 +1,3 @@ + +new + +copy2 + +rename2 Delete: - $ hg rm rename + $ hg rm dir1/* $ hg ci -mdelete $ hg diff --git -r 3:tip - diff --git a/rename b/rename + diff --git a/dir1/new b/dir1/new deleted file mode 100644 - --- a/rename + --- a/dir1/new +++ /dev/null @@ -1,1 +0,0 @@ -new + diff --git a/dir1/rename1 b/dir1/rename1 + deleted file mode 100644 + --- a/dir1/rename1 + +++ /dev/null + @@ -1,3 +0,0 @@ + -new + -copy1 + -rename1 + diff --git a/dir1/rename2 b/dir1/rename2 + deleted file mode 100644 + --- a/dir1/rename2 + +++ /dev/null + @@ -1,3 +0,0 @@ + -new + -copy2 + -rename2 $ cat > src < 1 diff -r 612ed41ae359 -r e89f909edffa tests/test-globalopts.t --- a/tests/test-globalopts.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-globalopts.t Thu Apr 16 20:57:51 2015 -0500 @@ -309,7 +309,7 @@ grep search for a pattern in specified files and revisions heads show branch heads help show help for a given topic or a help overview - identify identify the working copy or specified revision + identify identify the working directory or specified revision import import an ordered set of patches incoming show new changesets found in source init create a new repository in the given directory @@ -390,7 +390,7 @@ grep search for a pattern in specified files and revisions heads show branch heads help show help for a given topic or a help overview - identify identify the working copy or specified revision + identify identify the working directory or specified revision import import an ordered set of patches incoming show new changesets found in source init create a new repository in the given directory diff -r 612ed41ae359 -r e89f909edffa tests/test-glog.t --- a/tests/test-glog.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-glog.t Thu Apr 16 20:57:51 2015 -0500 @@ -1541,6 +1541,9 @@ $ testlog --follow [] [] + $ testlog -rnull + ['null'] + [] $ echo a > a $ echo aa > aa $ echo f > f @@ -1764,6 +1767,13 @@ nodetag 1 nodetag 0 +Test --follow null parent + + $ hg up -q null + $ testlog -f + [] + [] + Test --follow-first $ hg up -q 3 @@ -2192,13 +2202,6 @@ (func ('symbol', 'rev') ('symbol', '6')))) - --- log.nodes * (glob) - +++ glog.nodes * (glob) - @@ -1,3 +1,3 @@ - -nodetag 6 - nodetag 8 - nodetag 7 - +nodetag 6 Test --follow-first and forward --rev @@ -2240,6 +2243,14 @@ ('symbol', 'rev') ('symbol', '6')))) +Test --follow with --rev of graphlog extension + + $ hg --config extensions.graphlog= glog -qfr1 + o 1:216d4c92cf98 + | + o 0:f8035bb17114 + + Test subdir $ hg up -q 3 @@ -2354,4 +2365,14 @@ date: Thu Jan 01 00:00:00 1970 +0000 +should not draw line down to null due to the magic of fullreposet + + $ hg log -G -r 'all()' | tail -6 + | + o changeset: 0:f8035bb17114 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add a + + $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-graft.t --- a/tests/test-graft.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-graft.t Thu Apr 16 20:57:51 2015 -0500 @@ -313,7 +313,7 @@ 2:5c095ad7e90f871700f02dd1fa5012cb4498a2d4 $ hg log --debug -r tip - changeset: 13:9db0f28fd3747e92c57d015f53b5593aeec53c2d + changeset: 13:7a4785234d87ec1aa420ed6b11afe40fa73e12a9 tag: tip phase: draft parent: 12:b592ea63bb0c19a6c5c44685ee29a2284f9f1b8f @@ -324,6 +324,7 @@ files+: b files-: a extra: branch=default + extra: intermediate-source=ef0ef43d49e79e81ddafdc7997401ba0041efc82 extra: source=5c095ad7e90f871700f02dd1fa5012cb4498a2d4 description: 2 @@ -338,10 +339,10 @@ Disallow grafting already grafted csets with the same origin onto each other $ hg up -q 13 $ hg graft 2 - skipping revision 2:5c095ad7e90f (already grafted to 13:9db0f28fd374) + skipping revision 2:5c095ad7e90f (already grafted to 13:7a4785234d87) [255] $ hg graft 7 - skipping already grafted revision 7:ef0ef43d49e7 (13:9db0f28fd374 also has origin 2:5c095ad7e90f) + skipping already grafted revision 7:ef0ef43d49e7 (13:7a4785234d87 also has origin 2:5c095ad7e90f) [255] $ hg up -q 7 @@ -349,7 +350,7 @@ skipping revision 2:5c095ad7e90f (already grafted to 7:ef0ef43d49e7) [255] $ hg graft tip - skipping already grafted revision 13:9db0f28fd374 (7:ef0ef43d49e7 also has origin 2:5c095ad7e90f) + skipping already grafted revision 13:7a4785234d87 (7:ef0ef43d49e7 also has origin 2:5c095ad7e90f) [255] Graft with --log @@ -543,7 +544,7 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: 3 - changeset: 13:9db0f28fd374 + changeset: 13:7a4785234d87 user: foo date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 @@ -578,7 +579,7 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 - changeset: 13:9db0f28fd374 + changeset: 13:7a4785234d87 user: foo date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 @@ -621,7 +622,7 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 - changeset: 13:9db0f28fd374 + changeset: 13:7a4785234d87 user: foo date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 @@ -637,7 +638,7 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: 2 - changeset: 22:e95864da75a0 + changeset: 22:d1cb6591fa4b branch: dev tag: tip user: foo @@ -649,11 +650,11 @@ $ hg graft 'origin(13) or destination(origin(13))' skipping ancestor revision 21:7e61b508e709 - skipping ancestor revision 22:e95864da75a0 - skipping revision 2:5c095ad7e90f (already grafted to 22:e95864da75a0) + skipping ancestor revision 22:d1cb6591fa4b + skipping revision 2:5c095ad7e90f (already grafted to 22:d1cb6591fa4b) grafting 7:ef0ef43d49e7 "2" warning: can't find ancestor for 'b' copied from 'a'! - grafting 13:9db0f28fd374 "2" + grafting 13:7a4785234d87 "2" warning: can't find ancestor for 'b' copied from 'a'! grafting 19:9627f653b421 "2" merging b @@ -664,7 +665,7 @@ $ hg graft 19 0 6 skipping ungraftable merge revision 6 skipping ancestor revision 0:68795b066622 - skipping already grafted revision 19:9627f653b421 (22:e95864da75a0 also has origin 2:5c095ad7e90f) + skipping already grafted revision 19:9627f653b421 (22:d1cb6591fa4b also has origin 2:5c095ad7e90f) [255] $ hg graft 19 0 6 --force skipping ungraftable merge revision 6 @@ -679,12 +680,12 @@ $ hg ci -m 28 $ hg backout 28 reverting a - changeset 29:8389853bba65 backs out changeset 28:cd42a33e1848 + changeset 29:53177ba928f6 backs out changeset 28:50a516bb8b57 $ hg graft 28 - skipping ancestor revision 28:cd42a33e1848 + skipping ancestor revision 28:50a516bb8b57 [255] $ hg graft 28 --force - grafting 28:cd42a33e1848 "28" + grafting 28:50a516bb8b57 "28" merging a $ cat a abc @@ -694,7 +695,7 @@ $ echo def > a $ hg ci -m 31 $ hg graft 28 --force --tool internal:fail - grafting 28:cd42a33e1848 "28" + grafting 28:50a516bb8b57 "28" abort: unresolved conflicts, can't continue (use hg resolve and hg graft --continue) [255] @@ -707,7 +708,7 @@ $ hg resolve -m a (no more unresolved files) $ hg graft -c - grafting 28:cd42a33e1848 "28" + grafting 28:50a516bb8b57 "28" $ cat a abc @@ -719,7 +720,7 @@ $ hg --config extensions.strip= strip 2 saved backup bundle to $TESTTMP/a/.hg/strip-backup/5c095ad7e90f-d323a1e4-backup.hg (glob) $ hg graft tmp - skipping already grafted revision 8:9db0f28fd374 (2:ef0ef43d49e7 also has unknown origin 5c095ad7e90f) + skipping already grafted revision 8:7a4785234d87 (2:ef0ef43d49e7 also has unknown origin 5c095ad7e90f) [255] Empty graft @@ -728,5 +729,45 @@ $ hg tag -f something $ hg graft -qr 27 $ hg graft -f 27 - grafting 27:3d35c4c79e5a "28" - note: graft of 27:3d35c4c79e5a created no changes to commit + grafting 27:ed6c7e54e319 "28" + note: graft of 27:ed6c7e54e319 created no changes to commit + + $ cd .. + +Graft to duplicate a commit + + $ hg init graftsibling + $ cd graftsibling + $ touch a + $ hg commit -qAm a + $ touch b + $ hg commit -qAm b + $ hg log -G -T '{rev}\n' + @ 1 + | + o 0 + + $ hg up -q 0 + $ hg graft -r 1 + grafting 1:0e067c57feba "b" (tip) + $ hg log -G -T '{rev}\n' + @ 2 + | + | o 1 + |/ + o 0 + +Graft to duplicate a commit twice + + $ hg up -q 0 + $ hg graft -r 2 + grafting 2:044ec77f6389 "b" (tip) + $ hg log -G -T '{rev}\n' + @ 3 + | + | o 2 + |/ + | o 1 + |/ + o 0 + diff -r 612ed41ae359 -r e89f909edffa tests/test-grep.t --- a/tests/test-grep.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-grep.t Thu Apr 16 20:57:51 2015 -0500 @@ -82,6 +82,10 @@ port:1:2:+:eggs:export port:0:1:+:spam:import + $ hg up -q null + $ hg grep -f port + [1] + $ cd .. $ hg init t2 $ cd t2 diff -r 612ed41ae359 -r e89f909edffa tests/test-hardlinks.t --- a/tests/test-hardlinks.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hardlinks.t Thu Apr 16 20:57:51 2015 -0500 @@ -58,6 +58,13 @@ Create hardlinked clone r2: $ hg clone -U --debug r1 r2 + linking: 1 + linking: 2 + linking: 3 + linking: 4 + linking: 5 + linking: 6 + linking: 7 linked 7 files Create non-hardlinked clone r3: diff -r 612ed41ae359 -r e89f909edffa tests/test-help.t --- a/tests/test-help.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-help.t Thu Apr 16 20:57:51 2015 -0500 @@ -72,7 +72,7 @@ grep search for a pattern in specified files and revisions heads show branch heads help show help for a given topic or a help overview - identify identify the working copy or specified revision + identify identify the working directory or specified revision import import an ordered set of patches incoming show new changesets found in source init create a new repository in the given directory @@ -147,7 +147,7 @@ grep search for a pattern in specified files and revisions heads show branch heads help show help for a given topic or a help overview - identify identify the working copy or specified revision + identify identify the working directory or specified revision import import an ordered set of patches incoming show new changesets found in source init create a new repository in the given directory @@ -245,6 +245,7 @@ acl hooks for controlling repository access blackbox log repository events to a blackbox for debugging bugzilla hooks for integrating with the Bugzilla bug tracker + censor erase file content at a given revision churn command to display statistics about repository history color colorize output from some commands convert import revisions from foreign VCS repositories into @@ -411,7 +412,7 @@ Mercurial Distributed SCM (version *) (glob) (see http://mercurial.selenic.com for more information) - Copyright (C) 2005-2014 Matt Mackall and others + Copyright (C) 2005-2015 Matt Mackall and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. @@ -508,6 +509,7 @@ -B --ignore-blank-lines ignore changes whose lines are all blank -U --unified NUM number of lines of context to show --stat output diffstat-style summary of changes + --root DIR produce diffs relative to subdirectory -I --include PATTERN [+] include names matching the given patterns -X --exclude PATTERN [+] exclude names matching the given patterns -S --subrepos recurse into subrepositories @@ -689,7 +691,7 @@ grep search for a pattern in specified files and revisions heads show branch heads help show help for a given topic or a help overview - identify identify the working copy or specified revision + identify identify the working directory or specified revision import import an ordered set of patches incoming show new changesets found in source init create a new repository in the given directory @@ -1101,6 +1103,125 @@ abort: help section not found [255] +Test dynamic list of merge tools only shows up once + $ hg help merge-tools + Merge Tools + """"""""""" + + To merge files Mercurial uses merge tools. + + A merge tool combines two different versions of a file into a merged file. + Merge tools are given the two files and the greatest common ancestor of + the two file versions, so they can determine the changes made on both + branches. + + Merge tools are used both for "hg resolve", "hg merge", "hg update", "hg + backout" and in several extensions. + + Usually, the merge tool tries to automatically reconcile the files by + combining all non-overlapping changes that occurred separately in the two + different evolutions of the same initial base file. Furthermore, some + interactive merge programs make it easier to manually resolve conflicting + merges, either in a graphical way, or by inserting some conflict markers. + Mercurial does not include any interactive merge programs but relies on + external tools for that. + + Available merge tools + ===================== + + External merge tools and their properties are configured in the merge- + tools configuration section - see hgrc(5) - but they can often just be + named by their executable. + + A merge tool is generally usable if its executable can be found on the + system and if it can handle the merge. The executable is found if it is an + absolute or relative executable path or the name of an application in the + executable search path. The tool is assumed to be able to handle the merge + if it can handle symlinks if the file is a symlink, if it can handle + binary files if the file is binary, and if a GUI is available if the tool + requires a GUI. + + There are some internal merge tools which can be used. The internal merge + tools are: + + ":dump" + Creates three versions of the files to merge, containing the contents of + local, other and base. These files can then be used to perform a merge + manually. If the file to be merged is named "a.txt", these files will + accordingly be named "a.txt.local", "a.txt.other" and "a.txt.base" and + they will be placed in the same directory as "a.txt". + + ":fail" + Rather than attempting to merge files that were modified on both + branches, it marks them as unresolved. The resolve command must be used + to resolve these conflicts. + + ":local" + Uses the local version of files as the merged version. + + ":merge" + Uses the internal non-interactive simple merge algorithm for merging + files. It will fail if there are any conflicts and leave markers in the + partially merged file. Markers will have two sections, one for each side + of merge. + + ":merge3" + Uses the internal non-interactive simple merge algorithm for merging + files. It will fail if there are any conflicts and leave markers in the + partially merged file. Marker will have three sections, one from each + side of the merge and one for the base content. + + ":other" + Uses the other version of files as the merged version. + + ":prompt" + Asks the user which of the local or the other version to keep as the + merged version. + + ":tagmerge" + Uses the internal tag merge algorithm (experimental). + + Internal tools are always available and do not require a GUI but will by + default not handle symlinks or binary files. + + Choosing a merge tool + ===================== + + Mercurial uses these rules when deciding which merge tool to use: + + 1. If a tool has been specified with the --tool option to merge or + resolve, it is used. If it is the name of a tool in the merge-tools + configuration, its configuration is used. Otherwise the specified tool + must be executable by the shell. + 2. If the "HGMERGE" environment variable is present, its value is used and + must be executable by the shell. + 3. If the filename of the file to be merged matches any of the patterns in + the merge-patterns configuration section, the first usable merge tool + corresponding to a matching pattern is used. Here, binary capabilities + of the merge tool are not considered. + 4. If ui.merge is set it will be considered next. If the value is not the + name of a configured tool, the specified value is used and must be + executable by the shell. Otherwise the named tool is used if it is + usable. + 5. If any usable merge tools are present in the merge-tools configuration + section, the one with the highest priority is used. + 6. If a program named "hgmerge" can be found on the system, it is used - + but it will by default not be used for symlinks and binary files. + 7. If the file to be merged is not binary and is not a symlink, then + internal ":merge" is used. + 8. The merge of the file fails and must be resolved before commit. + + Note: + After selecting a merge program, Mercurial will by default attempt to + merge the files using a simple merge algorithm first. Only if it + doesn't succeed because of conflicting changes Mercurial will actually + execute the merge program. Whether to use the simple merge algorithm + first can be controlled by the premerge setting of the merge tool. + Premerge is enabled by default unless the file is binary or a symlink. + + See the merge-tools and ui sections of hgrc(5) for details on the + configuration of merge tools. + Test usage of section marks in help documents $ cd "$TESTDIR"/../doc @@ -1536,7 +1657,7 @@ identify
    tag node
    - identify the working copy or specified revision + identify the working directory or specified revision
    diff -r 612ed41ae359 -r e89f909edffa tests/test-hgrc.t --- a/tests/test-hgrc.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgrc.t Thu Apr 16 20:57:51 2015 -0500 @@ -71,7 +71,7 @@ Mercurial Distributed SCM (version *) (glob) (see http://mercurial.selenic.com for more information) - Copyright (C) 2005-2014 Matt Mackall and others + Copyright (C) 2005-2015 Matt Mackall and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ unset FAKEPATH diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-commands.t --- a/tests/test-hgweb-commands.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-commands.t Thu Apr 16 20:57:51 2015 -0500 @@ -726,7 +726,6 @@ -

    + + @@ -873,7 +874,8 @@ - + + @@ -894,8 +896,7 @@ [+]
    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    dateThu, 01 Jan 1970 00:00:00 +0000
    Thu, 01 Jan 1970 00:00:00 +0000
    parents
    +
    da/foo 1 @@ -1012,11 +1013,13 @@ + + @@ -1869,7 +1872,7 @@ $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '?cmd=capabilities'; echo 200 Script output follows - lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1*%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 (glob) heads @@ -2049,7 +2052,7 @@ $ "$TESTDIR/get-with-headers.py" 127.0.0.1:$HGPORT '?cmd=capabilities'; echo 200 Script output follows - lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream-preferred stream unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 + lookup changegroupsubset branchmap pushkey known getbundle unbundlehash batch stream-preferred stream bundle2=HG20%0Achangegroup%3D01%2C02%0Adigests%3Dmd5%2Csha1*%0Alistkeys%0Apushkey%0Aremote-changegroup%3Dhttp%2Chttps unbundle=HG10GZ,HG10BZ,HG10UN httpheader=1024 (glob) heads diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-descend-empties.t --- a/tests/test-hgweb-descend-empties.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-descend-empties.t Thu Apr 16 20:57:51 2015 -0500 @@ -81,11 +81,13 @@
    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    + + diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-diffs.t --- a/tests/test-hgweb-diffs.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-diffs.t Thu Apr 16 20:57:51 2015 -0500 @@ -97,7 +97,8 @@ - + + @@ -118,8 +119,7 @@ [+]
    name size permissions
    [up]
    dateThu, 01 Jan 1970 00:00:00 +0000
    Thu, 01 Jan 1970 00:00:00 +0000
    parents
    +
    - + + @@ -390,8 +391,7 @@ [+]
    a 1 @@ -369,7 +369,8 @@
    dateThu, 01 Jan 1970 00:00:00 +0000
    Thu, 01 Jan 1970 00:00:00 +0000
    parents
    +
    a 1 diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-empty.t --- a/tests/test-hgweb-empty.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-empty.t Thu Apr 16 20:57:51 2015 -0500 @@ -48,7 +48,6 @@ -

    + + @@ -158,7 +159,6 @@ -

    age author description
    + + @@ -264,7 +266,6 @@ -

    age author description
    + + diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-filelog.t --- a/tests/test-hgweb-filelog.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-filelog.t Thu Apr 16 20:57:51 2015 -0500 @@ -156,7 +156,6 @@ -

    name size permissions
    [up]
    + + @@ -266,7 +267,6 @@ -

    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    + + @@ -376,7 +378,6 @@ -

    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    + + @@ -481,7 +484,6 @@ -

    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    + + diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-json.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-hgweb-json.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,1111 @@ +#require json +#require serve + + $ request() { + > $TESTDIR/get-with-headers.py --json localhost:$HGPORT "$1" + > } + + $ hg init test + $ cd test + $ mkdir da + $ echo foo > da/foo + $ echo foo > foo + $ hg -q ci -A -m initial + $ echo bar > foo + $ hg ci -m 'modify foo' + $ echo bar > da/foo + $ hg ci -m 'modify da/foo' + $ hg bookmark bookmark1 + $ hg up default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + (leaving bookmark bookmark1) + $ hg mv foo foo-new + $ hg commit -m 'move foo' + $ hg tag -m 'create tag' tag1 + $ hg phase --public -r . + $ echo baz > da/foo + $ hg commit -m 'another commit to da/foo' + $ hg tag -m 'create tag2' tag2 + $ hg bookmark bookmark2 + $ hg -q up -r 0 + $ hg -q branch test-branch + $ echo branch > foo + $ hg commit -m 'create test branch' + $ echo branch_commit_2 > foo + $ hg commit -m 'another commit in test-branch' + $ hg -q up default + $ hg merge --tool :local test-branch + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg commit -m 'merge test-branch into default' + + $ hg log -G + @ changeset: 9:cc725e08502a + |\ tag: tip + | | parent: 6:ceed296fe500 + | | parent: 8:ed66c30e87eb + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: merge test-branch into default + | | + | o changeset: 8:ed66c30e87eb + | | branch: test-branch + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: another commit in test-branch + | | + | o changeset: 7:6ab967a8ab34 + | | branch: test-branch + | | parent: 0:06e557f3edf6 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: create test branch + | | + o | changeset: 6:ceed296fe500 + | | bookmark: bookmark2 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: create tag2 + | | + o | changeset: 5:f2890a05fea4 + | | tag: tag2 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: another commit to da/foo + | | + o | changeset: 4:93a8ce14f891 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: create tag + | | + o | changeset: 3:78896eb0e102 + | | tag: tag1 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: move foo + | | + o | changeset: 2:8d7c456572ac + | | bookmark: bookmark1 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: modify da/foo + | | + o | changeset: 1:f8bbb9024b10 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: modify foo + | + o changeset: 0:06e557f3edf6 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: initial + + + $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E error.log + $ cat hg.pid >> $DAEMON_PIDS + +(Try to keep these in roughly the order they are defined in webcommands.py) + +(log is handled by filelog/ and changelog/ - ignore it) + +(rawfile/ doesn't use templating - nothing to test) + +file/{revision}/{path} shows file revision + + $ request json-file/06e557f3edf6/foo + 200 Script output follows + + "not yet implemented" + +file/{revision} shows root directory info + + $ request json-file/cc725e08502a + 200 Script output follows + + { + "abspath": "/", + "bookmarks": [], + "directories": [ + { + "abspath": "/da", + "basename": "da", + "emptydirs": "" + } + ], + "files": [ + { + "abspath": ".hgtags", + "basename": ".hgtags", + "date": [ + 0.0, + 0 + ], + "flags": "", + "size": 92 + }, + { + "abspath": "foo-new", + "basename": "foo-new", + "date": [ + 0.0, + 0 + ], + "flags": "", + "size": 4 + } + ], + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "tags": [ + "tip" + ] + } + +changelog/ shows information about several changesets + + $ request json-changelog + 200 Script output follows + + { + "changeset_count": 10, + "changesets": [ + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "merge test-branch into default", + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "tags": [ + "tip" + ], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "another commit in test-branch", + "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "create test branch", + "node": "6ab967a8ab3489227a83f80e920faa039a71819f", + "tags": [], + "user": "test" + }, + { + "bookmarks": [ + "bookmark2" + ], + "date": [ + 0.0, + 0 + ], + "desc": "create tag2", + "node": "ceed296fe500c3fac9541e31dad860cb49c89e45", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "another commit to da/foo", + "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78", + "tags": [ + "tag2" + ], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "create tag", + "node": "93a8ce14f89156426b7fa981af8042da53f03aa0", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "move foo", + "node": "78896eb0e102174ce9278438a95e12543e4367a7", + "tags": [ + "tag1" + ], + "user": "test" + }, + { + "bookmarks": [ + "bookmark1" + ], + "date": [ + 0.0, + 0 + ], + "desc": "modify da/foo", + "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "initial", + "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", + "tags": [], + "user": "test" + } + ], + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7" + } + +changelog/{revision} shows information starting at a specific changeset + + $ request json-changelog/f8bbb9024b10 + 200 Script output follows + + { + "changeset_count": 10, + "changesets": [ + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "initial", + "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", + "tags": [], + "user": "test" + } + ], + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8" + } + +shortlog/ shows information about a set of changesets + + $ request json-shortlog + 200 Script output follows + + { + "changeset_count": 10, + "changesets": [ + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "merge test-branch into default", + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "tags": [ + "tip" + ], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "another commit in test-branch", + "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "create test branch", + "node": "6ab967a8ab3489227a83f80e920faa039a71819f", + "tags": [], + "user": "test" + }, + { + "bookmarks": [ + "bookmark2" + ], + "date": [ + 0.0, + 0 + ], + "desc": "create tag2", + "node": "ceed296fe500c3fac9541e31dad860cb49c89e45", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "another commit to da/foo", + "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78", + "tags": [ + "tag2" + ], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "create tag", + "node": "93a8ce14f89156426b7fa981af8042da53f03aa0", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "move foo", + "node": "78896eb0e102174ce9278438a95e12543e4367a7", + "tags": [ + "tag1" + ], + "user": "test" + }, + { + "bookmarks": [ + "bookmark1" + ], + "date": [ + 0.0, + 0 + ], + "desc": "modify da/foo", + "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "tags": [], + "user": "test" + }, + { + "bookmarks": [], + "date": [ + 0.0, + 0 + ], + "desc": "initial", + "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", + "tags": [], + "user": "test" + } + ], + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7" + } + +changeset/ renders the tip changeset + + $ request json-rev + 200 Script output follows + + { + "bookmarks": [], + "branch": "default", + "date": [ + 0.0, + 0 + ], + "desc": "merge test-branch into default", + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "parents": [ + "ceed296fe500c3fac9541e31dad860cb49c89e45", + "ed66c30e87eb65337c05a4229efaa5f1d5285a90" + ], + "phase": "draft", + "tags": [ + "tip" + ], + "user": "test" + } + +changeset/{revision} shows tags + + $ request json-rev/78896eb0e102 + 200 Script output follows + + { + "bookmarks": [], + "branch": "default", + "date": [ + 0.0, + 0 + ], + "desc": "move foo", + "node": "78896eb0e102174ce9278438a95e12543e4367a7", + "parents": [ + "8d7c456572acf3557e8ed8a07286b10c408bcec5" + ], + "phase": "public", + "tags": [ + "tag1" + ], + "user": "test" + } + +changeset/{revision} shows bookmarks + + $ request json-rev/8d7c456572ac + 200 Script output follows + + { + "bookmarks": [ + "bookmark1" + ], + "branch": "default", + "date": [ + 0.0, + 0 + ], + "desc": "modify da/foo", + "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5", + "parents": [ + "f8bbb9024b10f93cdbb8d940337398291d40dea8" + ], + "phase": "public", + "tags": [], + "user": "test" + } + +changeset/{revision} shows branches + + $ request json-rev/6ab967a8ab34 + 200 Script output follows + + { + "bookmarks": [], + "branch": "test-branch", + "date": [ + 0.0, + 0 + ], + "desc": "create test branch", + "node": "6ab967a8ab3489227a83f80e920faa039a71819f", + "parents": [ + "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e" + ], + "phase": "draft", + "tags": [], + "user": "test" + } + +manifest/{revision}/{path} shows info about a directory at a revision + + $ request json-manifest/06e557f3edf6/ + 200 Script output follows + + { + "abspath": "/", + "bookmarks": [], + "directories": [ + { + "abspath": "/da", + "basename": "da", + "emptydirs": "" + } + ], + "files": [ + { + "abspath": "foo", + "basename": "foo", + "date": [ + 0.0, + 0 + ], + "flags": "", + "size": 4 + } + ], + "node": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", + "tags": [] + } + +tags/ shows tags info + + $ request json-tags + 200 Script output follows + + { + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "tags": [ + { + "date": [ + 0.0, + 0 + ], + "node": "f2890a05fea49bfaf9fb27ed5490894eba32da78", + "tag": "tag2" + }, + { + "date": [ + 0.0, + 0 + ], + "node": "78896eb0e102174ce9278438a95e12543e4367a7", + "tag": "tag1" + } + ] + } + +bookmarks/ shows bookmarks info + + $ request json-bookmarks + 200 Script output follows + + { + "bookmarks": [ + { + "bookmark": "bookmark1", + "date": [ + 0.0, + 0 + ], + "node": "8d7c456572acf3557e8ed8a07286b10c408bcec5" + }, + { + "bookmark": "bookmark2", + "date": [ + 0.0, + 0 + ], + "node": "ceed296fe500c3fac9541e31dad860cb49c89e45" + } + ], + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7" + } + +branches/ shows branches info + + $ request json-branches + 200 Script output follows + + { + "branches": [ + { + "branch": "default", + "date": [ + 0.0, + 0 + ], + "node": "cc725e08502a79dd1eda913760fbe06ed7a9abc7", + "status": "open" + }, + { + "branch": "test-branch", + "date": [ + 0.0, + 0 + ], + "node": "ed66c30e87eb65337c05a4229efaa5f1d5285a90", + "status": "inactive" + } + ] + } + +summary/ shows a summary of repository state + + $ request json-summary + 200 Script output follows + + "not yet implemented" + +filediff/{revision}/{path} shows changes to a file in a revision + + $ request json-diff/f8bbb9024b10/foo + 200 Script output follows + + { + "author": "test", + "children": [], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "diff": [ + { + "blockno": 1, + "lines": [ + { + "l": "--- a/foo\tThu Jan 01 00:00:00 1970 +0000\n", + "n": 1, + "t": "-" + }, + { + "l": "+++ b/foo\tThu Jan 01 00:00:00 1970 +0000\n", + "n": 2, + "t": "+" + }, + { + "l": "@@ -1,1 +1,1 @@\n", + "n": 3, + "t": "@" + }, + { + "l": "-foo\n", + "n": 4, + "t": "-" + }, + { + "l": "+bar\n", + "n": 5, + "t": "+" + } + ] + } + ], + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "parents": [ + "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e" + ], + "path": "foo" + } + +comparison/{revision}/{path} shows information about before and after for a file + + $ request json-comparison/f8bbb9024b10/foo + 200 Script output follows + + { + "author": "test", + "children": [], + "comparison": [ + { + "lines": [ + { + "ll": "foo", + "ln": 1, + "rl": "bar", + "rn": 1, + "t": "replace" + } + ] + } + ], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "leftnode": "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e", + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "parents": [ + "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e" + ], + "path": "foo", + "rightnode": "f8bbb9024b10f93cdbb8d940337398291d40dea8" + } + +annotate/{revision}/{path} shows annotations for each line + + $ request json-annotate/f8bbb9024b10/foo + 200 Script output follows + + { + "abspath": "foo", + "annotate": [ + { + "abspath": "foo", + "author": "test", + "desc": "modify foo", + "line": "bar\n", + "lineno": 1, + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "revdate": [ + 0.0, + 0 + ], + "targetline": 1 + } + ], + "author": "test", + "children": [], + "date": [ + 0.0, + 0 + ], + "desc": "modify foo", + "node": "f8bbb9024b10f93cdbb8d940337398291d40dea8", + "parents": [ + "06e557f3edf66faa1ccaba5dd8c203c21cc79f1e" + ], + "permissions": "" + } + +filelog/{revision}/{path} shows history of a single file + + $ request json-filelog/f8bbb9024b10/foo + 200 Script output follows + + "not yet implemented" + +(archive/ doesn't use templating, so ignore it) + +(static/ doesn't use templating, so ignore it) + +graph/ shows information that can be used to render a graph of the DAG + + $ request json-graph + 200 Script output follows + + "not yet implemented" + +help/ shows help topics + + $ request json-help + 200 Script output follows + + { + "earlycommands": [ + { + "summary": "add the specified files on the next commit", + "topic": "add" + }, + { + "summary": "show changeset information by line for each file", + "topic": "annotate" + }, + { + "summary": "make a copy of an existing repository", + "topic": "clone" + }, + { + "summary": "commit the specified files or all outstanding changes", + "topic": "commit" + }, + { + "summary": "diff repository (or selected files)", + "topic": "diff" + }, + { + "summary": "dump the header and diffs for one or more changesets", + "topic": "export" + }, + { + "summary": "forget the specified files on the next commit", + "topic": "forget" + }, + { + "summary": "create a new repository in the given directory", + "topic": "init" + }, + { + "summary": "show revision history of entire repository or files", + "topic": "log" + }, + { + "summary": "merge another revision into working directory", + "topic": "merge" + }, + { + "summary": "pull changes from the specified source", + "topic": "pull" + }, + { + "summary": "push changes to the specified destination", + "topic": "push" + }, + { + "summary": "remove the specified files on the next commit", + "topic": "remove" + }, + { + "summary": "start stand-alone webserver", + "topic": "serve" + }, + { + "summary": "show changed files in the working directory", + "topic": "status" + }, + { + "summary": "summarize working directory state", + "topic": "summary" + }, + { + "summary": "update working directory (or switch revisions)", + "topic": "update" + } + ], + "othercommands": [ + { + "summary": "add all new files, delete all missing files", + "topic": "addremove" + }, + { + "summary": "create an unversioned archive of a repository revision", + "topic": "archive" + }, + { + "summary": "reverse effect of earlier changeset", + "topic": "backout" + }, + { + "summary": "subdivision search of changesets", + "topic": "bisect" + }, + { + "summary": "create a new bookmark or list existing bookmarks", + "topic": "bookmarks" + }, + { + "summary": "set or show the current branch name", + "topic": "branch" + }, + { + "summary": "list repository named branches", + "topic": "branches" + }, + { + "summary": "create a changegroup file", + "topic": "bundle" + }, + { + "summary": "output the current or given revision of files", + "topic": "cat" + }, + { + "summary": "show combined config settings from all hgrc files", + "topic": "config" + }, + { + "summary": "mark files as copied for the next commit", + "topic": "copy" + }, + { + "summary": "list tracked files", + "topic": "files" + }, + { + "summary": "copy changes from other branches onto the current branch", + "topic": "graft" + }, + { + "summary": "search for a pattern in specified files and revisions", + "topic": "grep" + }, + { + "summary": "show branch heads", + "topic": "heads" + }, + { + "summary": "show help for a given topic or a help overview", + "topic": "help" + }, + { + "summary": "identify the working directory or specified revision", + "topic": "identify" + }, + { + "summary": "import an ordered set of patches", + "topic": "import" + }, + { + "summary": "show new changesets found in source", + "topic": "incoming" + }, + { + "summary": "output the current or given revision of the project manifest", + "topic": "manifest" + }, + { + "summary": "show changesets not found in the destination", + "topic": "outgoing" + }, + { + "summary": "show aliases for remote repositories", + "topic": "paths" + }, + { + "summary": "set or show the current phase name", + "topic": "phase" + }, + { + "summary": "roll back an interrupted transaction", + "topic": "recover" + }, + { + "summary": "rename files; equivalent of copy + remove", + "topic": "rename" + }, + { + "summary": "redo merges or set/view the merge status of files", + "topic": "resolve" + }, + { + "summary": "restore files to their checkout state", + "topic": "revert" + }, + { + "summary": "print the root (top) of the current working directory", + "topic": "root" + }, + { + "summary": "add one or more tags for the current or given revision", + "topic": "tag" + }, + { + "summary": "list repository tags", + "topic": "tags" + }, + { + "summary": "apply one or more changegroup files", + "topic": "unbundle" + }, + { + "summary": "verify the integrity of the repository", + "topic": "verify" + }, + { + "summary": "output version and copyright information", + "topic": "version" + } + ], + "topics": [ + { + "summary": "Configuration Files", + "topic": "config" + }, + { + "summary": "Date Formats", + "topic": "dates" + }, + { + "summary": "Diff Formats", + "topic": "diffs" + }, + { + "summary": "Environment Variables", + "topic": "environment" + }, + { + "summary": "Using Additional Features", + "topic": "extensions" + }, + { + "summary": "Specifying File Sets", + "topic": "filesets" + }, + { + "summary": "Glossary", + "topic": "glossary" + }, + { + "summary": "Syntax for Mercurial Ignore Files", + "topic": "hgignore" + }, + { + "summary": "Configuring hgweb", + "topic": "hgweb" + }, + { + "summary": "Merge Tools", + "topic": "merge-tools" + }, + { + "summary": "Specifying Multiple Revisions", + "topic": "multirevs" + }, + { + "summary": "File Name Patterns", + "topic": "patterns" + }, + { + "summary": "Working with Phases", + "topic": "phases" + }, + { + "summary": "Specifying Single Revisions", + "topic": "revisions" + }, + { + "summary": "Specifying Revision Sets", + "topic": "revsets" + }, + { + "summary": "Subrepositories", + "topic": "subrepos" + }, + { + "summary": "Template Usage", + "topic": "templating" + }, + { + "summary": "URL Paths", + "topic": "urls" + } + ] + } + +help/{topic} shows an individual help topic + + $ request json-help/phases + 200 Script output follows + + { + "rawdoc": "Working with Phases\n*", (glob) + "topic": "phases" + } diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb-removed.t --- a/tests/test-hgweb-removed.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb-removed.t Thu Apr 16 20:57:51 2015 -0500 @@ -78,7 +78,8 @@ - + + @@ -99,8 +100,7 @@ [+]
    age author description
    Thu, 01 Jan 1970 00:00:00 +0000
    dateThu, 01 Jan 1970 00:00:00 +0000
    Thu, 01 Jan 1970 00:00:00 +0000
    parents cb9a9f314b8b
    +
    a 1 diff -r 612ed41ae359 -r e89f909edffa tests/test-hgweb.t --- a/tests/test-hgweb.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgweb.t Thu Apr 16 20:57:51 2015 -0500 @@ -272,11 +272,13 @@ + + diff -r 612ed41ae359 -r e89f909edffa tests/test-hgwebdir.t --- a/tests/test-hgwebdir.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hgwebdir.t Thu Apr 16 20:57:51 2015 -0500 @@ -201,6 +201,7 @@
    name size permissions
    [up]
    + @@ -209,6 +210,7 @@ + @@ -699,6 +701,7 @@
    Name Description   
    + @@ -707,6 +710,7 @@ + @@ -1128,6 +1132,7 @@
    Name Description   
    + @@ -1136,6 +1141,7 @@ + diff -r 612ed41ae359 -r e89f909edffa tests/test-highlight.t --- a/tests/test-highlight.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-highlight.t Thu Apr 16 20:57:51 2015 -0500 @@ -268,10 +268,12 @@
    Name Description   
    + + diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-arguments.t --- a/tests/test-histedit-arguments.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-arguments.t Thu Apr 16 20:57:51 2015 -0500 @@ -103,6 +103,15 @@ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg up --quiet +Test config specified default +----------------------------- + + $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF + > pick c8e68270e35a 3 four + > pick 08d98a8350f3 4 five + > EOF + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + Run on a revision not descendants of the initial parent -------------------------------------------------------------------- @@ -111,6 +120,13 @@ Mercurial earlier than 2.7 by renaming ".hg/histedit-state" temporarily. + $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2:: + @ 4 08d9 five + | + o 3 c8e6 four + | + o 2 eb57 three + | $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF > edit 08d98a8350f3 4 five > EOF @@ -122,15 +138,23 @@ $ mv .hg/histedit-state .hg/histedit-state.back $ hg update --quiet --clean 2 + $ echo alpha >> alpha $ mv .hg/histedit-state.back .hg/histedit-state $ hg histedit --continue - abort: c8e68270e35a is not an ancestor of working directory - (use "histedit --abort" to clear broken state) - [255] + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-backup.hg (glob) + $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2:: + @ 4 f5ed five + | + | o 3 c8e6 four + |/ + o 2 eb57 three + | - $ hg histedit --abort - $ hg update --quiet --clean + $ hg unbundle -q $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-backup.hg + $ hg strip -q -r f5ed --config extensions.strip= + $ hg up -q 08d98a8350f3 Test that missing revisions are detected --------------------------------------- diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-bookmark-motion.t --- a/tests/test-histedit-bookmark-motion.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-bookmark-motion.t Thu Apr 16 20:57:51 2015 -0500 @@ -92,7 +92,7 @@ histedit: moving bookmarks two from 177f92b77385 to b346ab9a313d histedit: moving bookmarks will-move-backwards from d2ae7f538514 to cb9a9f314b8b saved backup bundle to $TESTTMP/r/.hg/strip-backup/d2ae7f538514-48787b8d-backup.hg (glob) - saved backup bundle to $TESTTMP/r/.hg/strip-backup/96e494a2d553-60cea58b-backup.hg (glob) + saved backup bundle to $TESTTMP/r/.hg/strip-backup/96e494a2d553-3c6c5d92-backup.hg (glob) $ hg log --graph @ changeset: 3:cacdfd884a93 | bookmark: five diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-drop.t --- a/tests/test-histedit-drop.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-drop.t Thu Apr 16 20:57:51 2015 -0500 @@ -96,7 +96,6 @@ Check histedit_source $ hg log --debug --rev f518305ce889 - invalid branchheads cache (visible): tip differs changeset: 4:f518305ce889c07cb5bd05522176d75590ef3324 tag: tip phase: draft diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-edit.t --- a/tests/test-histedit-edit.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-edit.t Thu Apr 16 20:57:51 2015 -0500 @@ -3,13 +3,14 @@ $ cat >> $HGRCPATH < [extensions] > histedit= + > strip= > EOF $ initrepo () > { > hg init r > cd r - > for x in a b c d e f ; do + > for x in a b c d e f g; do > echo $x > $x > hg add $x > hg ci -m $x @@ -20,10 +21,15 @@ log before edit $ hg log --graph - @ changeset: 5:652413bf663e + @ changeset: 6:3c6a8ed2ebe8 | tag: tip | user: test | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: g + | + o changeset: 5:652413bf663e + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 | summary: f | o changeset: 4:e860deea161a @@ -58,11 +64,19 @@ > pick 055a42cdd887 d > edit e860deea161a e > pick 652413bf663e f + > pick 3c6a8ed2ebe8 g > EOF - 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + 0 files updated, 0 files merged, 3 files removed, 0 files unresolved Make changes as needed, you may commit or record as needed now. When you are finished, run hg histedit --continue to resume. +edit the plan + $ hg histedit --edit-plan --commands - 2>&1 << EOF + > edit e860deea161a e + > pick 652413bf663e f + > drop 3c6a8ed2ebe8 g + > EOF + Go at a random point and try to continue $ hg id -n @@ -72,10 +86,22 @@ (use 'hg histedit --continue' or 'hg histedit --abort') [255] +Try to delete necessary commit + $ hg strip -r 652413b + abort: histedit in progress, can't strip 652413bf663e + [255] + commit, then edit the revision $ hg ci -m 'wat' created new head $ echo a > e + +qnew should fail while we're in the middle of the edit step + + $ hg --config extensions.mq= qnew please-fail + abort: histedit in progress + (use 'hg histedit --continue' or 'hg histedit --abort') + [255] $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -121,6 +147,34 @@ $ hg cat e a +Stripping necessary commits should not break --abort + + $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle + > edit 1a60820cd1f6 wat + > pick a5e1ba2f7afb foobaz + > pick b5f70786f9b0 g + > EOF + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + Make changes as needed, you may commit or record as needed now. + When you are finished, run hg histedit --continue to resume. + + $ mv .hg/histedit-state .hg/histedit-state.bak + $ hg strip -q -r b5f70786f9b0 + $ mv .hg/histedit-state.bak .hg/histedit-state + $ hg histedit --abort + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 3 files + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -r . + changeset: 6:b5f70786f9b0 + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: f + + check histedit_source $ hg log --debug --rev 5 diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-fold-non-commute.t --- a/tests/test-histedit-fold-non-commute.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-fold-non-commute.t Thu Apr 16 20:57:51 2015 -0500 @@ -132,6 +132,7 @@ $ hg resolve --mark e (no more unresolved files) $ hg histedit --continue 2>&1 | fixbundle + 7b4e2f4b7bcd: empty changeset 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -274,6 +275,7 @@ $ hg resolve --mark e (no more unresolved files) $ hg histedit --continue 2>&1 | fixbundle + 7b4e2f4b7bcd: empty changeset 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-fold.t --- a/tests/test-histedit-fold.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-fold.t Thu Apr 16 20:57:51 2015 -0500 @@ -307,6 +307,7 @@ $ hg resolve --mark file (no more unresolved files) $ hg histedit --continue + 251d831eeec5: empty changeset 0 files updated, 0 files merged, 0 files removed, 0 files unresolved saved backup bundle to $TESTTMP/*-backup.hg (glob) $ hg logt --graph diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-non-commute-abort.t --- a/tests/test-histedit-non-commute-abort.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-non-commute-abort.t Thu Apr 16 20:57:51 2015 -0500 @@ -70,8 +70,6 @@ > pick 652413bf663e f > EOF 0 files updated, 0 files merged, 2 files removed, 0 files unresolved - remote changed e which local deleted - use (c)hanged version or leave (d)eleted? c 0 files updated, 0 files merged, 0 files removed, 0 files unresolved merging e warning: conflicts during merge. diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-non-commute.t --- a/tests/test-histedit-non-commute.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-non-commute.t Thu Apr 16 20:57:51 2015 -0500 @@ -170,6 +170,7 @@ $ hg resolve --mark e (no more unresolved files) $ hg histedit --continue 2>&1 | fixbundle + 7b4e2f4b7bcd: empty changeset 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -253,6 +254,7 @@ $ hg resolve --mark e (no more unresolved files) $ hg histedit --continue 2>&1 | fixbundle + 7b4e2f4b7bcd: empty changeset 0 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved diff -r 612ed41ae359 -r e89f909edffa tests/test-histedit-obsolete.t --- a/tests/test-histedit-obsolete.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-histedit-obsolete.t Thu Apr 16 20:57:51 2015 -0500 @@ -64,7 +64,7 @@ > fold e860deea161a 4 e > pick 652413bf663e 5 f > EOF - saved backup bundle to $TESTTMP/base/.hg/strip-backup/96e494a2d553-60cea58b-backup.hg (glob) + saved backup bundle to $TESTTMP/base/.hg/strip-backup/96e494a2d553-3c6c5d92-backup.hg (glob) $ hg log --graph --hidden @ 8:cacdfd884a93 f | @@ -427,9 +427,9 @@ 0 files updated, 0 files merged, 2 files removed, 0 files unresolved 2 files updated, 0 files merged, 0 files removed, 0 files unresolved 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - saved backup bundle to $TESTTMP/folding/.hg/strip-backup/58019c66f35f-be4b3835-backup.hg (glob) - saved backup bundle to $TESTTMP/folding/.hg/strip-backup/83d1858e070b-08306a6b-backup.hg (glob) - saved backup bundle to $TESTTMP/folding/.hg/strip-backup/859969f5ed7e-86c99c41-backup.hg (glob) + saved backup bundle to $TESTTMP/folding/.hg/strip-backup/58019c66f35f-96092fce-backup.hg (glob) + saved backup bundle to $TESTTMP/folding/.hg/strip-backup/83d1858e070b-f3469cf8-backup.hg (glob) + saved backup bundle to $TESTTMP/folding/.hg/strip-backup/859969f5ed7e-d89a19d7-backup.hg (glob) $ hg log -G @ 19:f9daec13fb98 (secret) i | diff -r 612ed41ae359 -r e89f909edffa tests/test-hook.t --- a/tests/test-hook.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-hook.t Thu Apr 16 20:57:51 2015 -0500 @@ -12,13 +12,20 @@ > pre-identify = python "$TESTDIR/printenv.py" pre-identify 1 > pre-cat = python "$TESTDIR/printenv.py" pre-cat > post-cat = python "$TESTDIR/printenv.py" post-cat + > pretxnopen = sh -c "HG_LOCAL= HG_TAG= python \"$TESTDIR/printenv.py\" pretxnopen" + > pretxnclose = sh -c "HG_LOCAL= HG_TAG= python \"$TESTDIR/printenv.py\" pretxnclose" + > txnclose = sh -c "HG_LOCAL= HG_TAG= python \"$TESTDIR/printenv.py\" txnclose" + > txnabort = sh -c "HG_LOCAL= HG_TAG= python \"$TESTDIR/printenv.py\" txnabort" > EOF $ echo a > a $ hg add a $ hg commit -m a precommit hook: HG_PARENT1=0000000000000000000000000000000000000000 + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000 HG_PENDING=$TESTTMP/a 0:cb9a9f314b8b + pretxnclose hook: HG_PENDING=$TESTTMP/a HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_XNNAME=commit (glob) + txnclose hook: HG_PHASES_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=commit (glob) commit hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000 commit.b hook: HG_NODE=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PARENT1=0000000000000000000000000000000000000000 @@ -42,8 +49,11 @@ $ echo b >> a $ hg commit -m a1 -d "1 0" precommit hook: HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a 1:ab228980c14d + pretxnclose hook: HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_XNNAME=commit (glob) + txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) commit hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b commit.b hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b $ hg update -C 0 @@ -52,8 +62,11 @@ $ hg add b $ hg commit -m b -d '1 0' precommit hook: HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b HG_PENDING=$TESTTMP/a 2:ee9deb46ab31 + pretxnclose hook: HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_XNNAME=commit (glob) + txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) commit hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b commit.b hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT1=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b created new head @@ -62,8 +75,11 @@ (branch merge, don't forget to commit) $ hg commit -m merge -d '2 0' precommit hook: HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd HG_PENDING=$TESTTMP/a 3:07f3376c1e65 + pretxnclose hook: HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_XNNAME=commit (glob) + txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) commit hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd commit.b hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PARENT1=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_PARENT2=ab228980c14deea8b9555d91c9581127383e40fd @@ -82,15 +98,15 @@ $ hg pull ../a pulling from ../a searching for changes - prechangegroup hook: HG_SOURCE=pull HG_URL=file:$TESTTMP/a + prechangegroup hook: HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) adding changesets adding manifests adding file changes added 3 changesets with 2 changes to 2 files - changegroup hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_URL=file:$TESTTMP/a - incoming hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_URL=file:$TESTTMP/a - incoming hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_SOURCE=pull HG_URL=file:$TESTTMP/a - incoming hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_SOURCE=pull HG_URL=file:$TESTTMP/a + changegroup hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) + incoming hook: HG_NODE=ab228980c14deea8b9555d91c9581127383e40fd HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) + incoming hook: HG_NODE=ee9deb46ab31e4cc3310f3cf0c3d668e4d8fffc2 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) + incoming hook: HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) (run 'hg update' to get a working copy) tag hooks can see env vars @@ -103,9 +119,12 @@ $ hg tag -d '3 0' a pretag hook: HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a precommit hook: HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 HG_PENDING=$TESTTMP/a 4:539e4b31b6dc + pretxnclose hook: HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_XNNAME=commit (glob) tag hook: HG_LOCAL=0 HG_NODE=07f3376c1e655977439df2a814e3cc14b27abac2 HG_TAG=a + txnclose hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) commit hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 commit.b hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PARENT1=07f3376c1e655977439df2a814e3cc14b27abac2 $ hg tag -l la @@ -137,11 +156,13 @@ 4:539e4b31b6dc $ hg commit -m 'fail' -d '4 0' precommit hook: HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 + pretxnopen hook: HG_TXNNAME=commit pretxncommit hook: HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a 5:6f611f8018c1 5:6f611f8018c1 pretxncommit.forbid hook: HG_NODE=6f611f8018c10e827fee6bd2bc807f937e761567 HG_PARENT1=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/a transaction abort! + txnabort hook: HG_TXNID=TXN:* HG_TXNNAME=commit (glob) rollback completed abort: pretxncommit.forbid1 hook exited with status 1 [255] @@ -198,6 +219,9 @@ pushing to ../a searching for changes no changes found + pretxnopen hook: HG_TXNNAME=bookmarks + pretxnclose hook: HG_BOOKMARK_MOVED=1 HG_PENDING=$TESTTMP/a HG_TXNID=TXN:* HG_XNNAME=bookmarks (glob) + txnclose hook: HG_BOOKMARK_MOVED=1 HG_TXNID=TXN:* HG_TXNNAME=bookmarks (glob) pushkey hook: HG_KEY=foo HG_NAMESPACE=bookmarks HG_NEW=0000000000000000000000000000000000000000 HG_RET=1 exporting bookmark foo [1] @@ -260,7 +284,7 @@ $ hg pull ../a pulling from ../a searching for changes - prechangegroup.forbid hook: HG_SOURCE=pull HG_URL=file:$TESTTMP/a + prechangegroup.forbid hook: HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) abort: prechangegroup.forbid hook exited with status 1 [255] @@ -280,7 +304,7 @@ adding file changes added 1 changesets with 1 changes to 1 files 4:539e4b31b6dc - pretxnchangegroup.forbid hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/b HG_SOURCE=pull HG_URL=file:$TESTTMP/a + pretxnchangegroup.forbid hook: HG_NODE=539e4b31b6dc99b3cfbaa6b53cbc1c1f9a1e3a10 HG_PENDING=$TESTTMP/b HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=file:$TESTTMP/a (glob) transaction abort! rollback completed abort: pretxnchangegroup.forbid1 hook exited with status 1 diff -r 612ed41ae359 -r e89f909edffa tests/test-http.t --- a/tests/test-http.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-http.t Thu Apr 16 20:57:51 2015 -0500 @@ -127,7 +127,7 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files - changegroup hook: HG_NODE=5fed3813f7f5e1824344fdc9cf8f63bb662c292d HG_SOURCE=pull HG_URL=http://localhost:$HGPORT1/ + changegroup hook: HG_NODE=5fed3813f7f5e1824344fdc9cf8f63bb662c292d HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=http://localhost:$HGPORT1/ (glob) (run 'hg update' to get a working copy) $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-https.t --- a/tests/test-https.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-https.t Thu Apr 16 20:57:51 2015 -0500 @@ -119,12 +119,12 @@ Apple's OpenSSL. This trick do not work with plain OpenSSL. $ DISABLEOSXDUMMYCERT= -#if osx +#if defaultcacerts $ hg clone https://localhost:$HGPORT/ copy-pull abort: error: *certificate verify failed* (glob) [255] - $ DISABLEOSXDUMMYCERT="--config=web.cacerts=" + $ DISABLEOSXDUMMYCERT="--config=web.cacerts=!" #endif clone via pull @@ -156,14 +156,14 @@ $ echo '[hooks]' >> .hg/hgrc $ echo "changegroup = python \"$TESTDIR/printenv.py\" changegroup" >> .hg/hgrc $ hg pull $DISABLEOSXDUMMYCERT + pulling from https://localhost:$HGPORT/ warning: localhost certificate with fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca not verified (check hostfingerprints or web.cacerts config setting) - pulling from https://localhost:$HGPORT/ searching for changes adding changesets adding manifests adding file changes added 1 changesets with 1 changes to 1 files - changegroup hook: HG_NODE=5fed3813f7f5e1824344fdc9cf8f63bb662c292d HG_SOURCE=pull HG_URL=https://localhost:$HGPORT/ + changegroup hook: HG_NODE=5fed3813f7f5e1824344fdc9cf8f63bb662c292d HG_SOURCE=pull HG_TXNID=TXN:* HG_URL=https://localhost:$HGPORT/ (glob) (run 'hg update' to get a working copy) $ cd .. @@ -188,28 +188,30 @@ searching for changes no changes found $ P=`pwd` hg -R copy-pull pull --insecure + pulling from https://localhost:$HGPORT/ warning: localhost certificate with fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca not verified (check hostfingerprints or web.cacerts config setting) - pulling from https://localhost:$HGPORT/ searching for changes no changes found cacert mismatch $ hg -R copy-pull pull --config web.cacerts=pub.pem https://127.0.0.1:$HGPORT/ + pulling from https://127.0.0.1:$HGPORT/ abort: 127.0.0.1 certificate error: certificate is for localhost (configure hostfingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca or use --insecure to connect insecurely) [255] $ hg -R copy-pull pull --config web.cacerts=pub.pem https://127.0.0.1:$HGPORT/ --insecure + pulling from https://127.0.0.1:$HGPORT/ warning: 127.0.0.1 certificate with fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca not verified (check hostfingerprints or web.cacerts config setting) - pulling from https://127.0.0.1:$HGPORT/ searching for changes no changes found $ hg -R copy-pull pull --config web.cacerts=pub-other.pem + pulling from https://localhost:$HGPORT/ abort: error: *certificate verify failed* (glob) [255] $ hg -R copy-pull pull --config web.cacerts=pub-other.pem --insecure + pulling from https://localhost:$HGPORT/ warning: localhost certificate with fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca not verified (check hostfingerprints or web.cacerts config setting) - pulling from https://localhost:$HGPORT/ searching for changes no changes found @@ -218,6 +220,7 @@ $ hg -R test serve -p $HGPORT1 -d --pid-file=hg1.pid --certificate=server-not-yet.pem $ cat hg1.pid >> $DAEMON_PIDS $ hg -R copy-pull pull --config web.cacerts=pub-not-yet.pem https://localhost:$HGPORT1/ + pulling from https://localhost:$HGPORT1/ abort: error: *certificate verify failed* (glob) [255] @@ -226,6 +229,7 @@ $ hg -R test serve -p $HGPORT2 -d --pid-file=hg2.pid --certificate=server-expired.pem $ cat hg2.pid >> $DAEMON_PIDS $ hg -R copy-pull pull --config web.cacerts=pub-expired.pem https://localhost:$HGPORT2/ + pulling from https://localhost:$HGPORT2/ abort: error: *certificate verify failed* (glob) [255] @@ -236,7 +240,7 @@ $ echo "127.0.0.1 = 914f1aff87249c09b6859b88b1906d30756491ca" >> copy-pull/.hg/hgrc - works without cacerts - $ hg -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts= + $ hg -R copy-pull id https://localhost:$HGPORT/ --config web.cacerts=! 5fed3813f7f5 - fails when cert doesn't match hostname (port is ignored) @@ -267,8 +271,8 @@ Test unvalidated https through proxy $ http_proxy=http://localhost:$HGPORT1/ hg -R copy-pull pull --insecure --traceback + pulling from https://localhost:$HGPORT/ warning: localhost certificate with fingerprint 91:4f:1a:ff:87:24:9c:09:b6:85:9b:88:b1:90:6d:30:75:64:91:ca not verified (check hostfingerprints or web.cacerts config setting) - pulling from https://localhost:$HGPORT/ searching for changes no changes found @@ -286,8 +290,10 @@ Test https with cert problems through proxy $ http_proxy=http://localhost:$HGPORT1/ hg -R copy-pull pull --config web.cacerts=pub-other.pem + pulling from https://localhost:$HGPORT/ abort: error: *certificate verify failed* (glob) [255] $ http_proxy=http://localhost:$HGPORT1/ hg -R copy-pull pull --config web.cacerts=pub-expired.pem https://localhost:$HGPORT2/ + pulling from https://localhost:$HGPORT2/ abort: error: *certificate verify failed* (glob) [255] diff -r 612ed41ae359 -r e89f909edffa tests/test-import-bypass.t --- a/tests/test-import-bypass.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-import-bypass.t Thu Apr 16 20:57:51 2015 -0500 @@ -104,6 +104,86 @@ $ hg rollback repository tip rolled back to revision 1 (undo import) +Test --strip with --bypass + + $ mkdir -p dir/dir2 + $ echo bb > dir/dir2/b + $ echo cc > dir/dir2/c + $ echo d > dir/d + $ hg ci -Am 'addabcd' + adding dir/d + adding dir/dir2/b + adding dir/dir2/c + $ shortlog + @ 2:d805bc8236b6 test 0 0 - default - addabcd + | + | o 1:4e322f7ce8e3 test 0 0 - foo - changea + |/ + o 0:07f494440405 test 0 0 - default - adda + + $ hg import --bypass --strip 2 --prefix dir/ - < # HG changeset patch + > # User test + > # Date 0 0 + > # Branch foo + > changeabcd + > + > diff --git a/foo/a b/foo/a + > new file mode 100644 + > --- /dev/null + > +++ b/foo/a + > @@ -0,0 +1 @@ + > +a + > diff --git a/foo/dir2/b b/foo/dir2/b2 + > rename from foo/dir2/b + > rename to foo/dir2/b2 + > diff --git a/foo/dir2/c b/foo/dir2/c + > --- a/foo/dir2/c + > +++ b/foo/dir2/c + > @@ -0,0 +1 @@ + > +cc + > diff --git a/foo/d b/foo/d + > deleted file mode 100644 + > --- a/foo/d + > +++ /dev/null + > @@ -1,1 +0,0 @@ + > -d + > EOF + applying patch from stdin + + $ shortlog + o 3:5bd46886ca3e test 0 0 - default - changeabcd + | + @ 2:d805bc8236b6 test 0 0 - default - addabcd + | + | o 1:4e322f7ce8e3 test 0 0 - foo - changea + |/ + o 0:07f494440405 test 0 0 - default - adda + + $ hg diff --change 3 --git + diff --git a/dir/a b/dir/a + new file mode 100644 + --- /dev/null + +++ b/dir/a + @@ -0,0 +1,1 @@ + +a + diff --git a/dir/d b/dir/d + deleted file mode 100644 + --- a/dir/d + +++ /dev/null + @@ -1,1 +0,0 @@ + -d + diff --git a/dir/dir2/b b/dir/dir2/b2 + rename from dir/dir2/b + rename to dir/dir2/b2 + diff --git a/dir/dir2/c b/dir/dir2/c + --- a/dir/dir2/c + +++ b/dir/dir2/c + @@ -1,1 +1,2 @@ + cc + +cc + $ hg -q --config extensions.strip= strip . + Test unsupported combinations $ hg import --bypass --no-commit ../test.diff @@ -112,6 +192,9 @@ $ hg import --bypass --similarity 50 ../test.diff abort: cannot use --similarity with --bypass [255] + $ hg import --exact --prefix dir/ ../test.diff + abort: cannot use --exact with --prefix + [255] Test commit editor (this also tests that editor is invoked, if the patch doesn't contain diff -r 612ed41ae359 -r e89f909edffa tests/test-import-git.t --- a/tests/test-import-git.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-import-git.t Thu Apr 16 20:57:51 2015 -0500 @@ -612,12 +612,114 @@ a R a -Renames, similarity and git diff +Prefix with strip, renames, creates etc $ hg revert -aC undeleting a forgetting b $ rm b + $ mkdir -p dir/dir2 + $ echo b > dir/dir2/b + $ echo c > dir/dir2/c + $ echo d > dir/d + $ hg ci -Am addbcd + adding dir/d + adding dir/dir2/b + adding dir/dir2/c + +prefix '.' is the same as no prefix + $ hg import --no-commit --prefix . - < diff --git a/dir/a b/dir/a + > --- /dev/null + > +++ b/dir/a + > @@ -0,0 +1 @@ + > +aaaa + > diff --git a/dir/d b/dir/d + > --- a/dir/d + > +++ b/dir/d + > @@ -1,1 +1,2 @@ + > d + > +dddd + > EOF + applying patch from stdin + $ cat dir/a + aaaa + $ cat dir/d + d + dddd + $ hg revert -aC + forgetting dir/a (glob) + reverting dir/d (glob) + $ rm dir/a + +prefix with default strip + $ hg import --no-commit --prefix dir/ - < diff --git a/a b/a + > --- /dev/null + > +++ b/a + > @@ -0,0 +1 @@ + > +aaa + > diff --git a/d b/d + > --- a/d + > +++ b/d + > @@ -1,1 +1,2 @@ + > d + > +dd + > EOF + applying patch from stdin + $ cat dir/a + aaa + $ cat dir/d + d + dd + $ hg revert -aC + forgetting dir/a (glob) + reverting dir/d (glob) + $ rm dir/a +(test that prefixes are relative to the cwd) + $ mkdir tmpdir + $ cd tmpdir + $ hg import --no-commit -p2 --prefix ../dir/ - < diff --git a/foo/a b/foo/a + > new file mode 100644 + > --- /dev/null + > +++ b/foo/a + > @@ -0,0 +1 @@ + > +a + > diff --git a/foo/dir2/b b/foo/dir2/b2 + > rename from foo/dir2/b + > rename to foo/dir2/b2 + > diff --git a/foo/dir2/c b/foo/dir2/c + > --- a/foo/dir2/c + > +++ b/foo/dir2/c + > @@ -0,0 +1 @@ + > +cc + > diff --git a/foo/d b/foo/d + > deleted file mode 100644 + > --- a/foo/d + > +++ /dev/null + > @@ -1,1 +0,0 @@ + > -d + > EOF + applying patch from stdin + $ hg st --copies + M dir/dir2/c + A dir/a + A dir/dir2/b2 + dir/dir2/b + R dir/d + R dir/dir2/b + $ cd .. + +Renames, similarity and git diff + + $ hg revert -aC + forgetting dir/a (glob) + undeleting dir/d (glob) + undeleting dir/dir2/b (glob) + forgetting dir/dir2/b2 (glob) + reverting dir/dir2/c (glob) + $ rm dir/a dir/dir2/b2 $ hg import --similarity 90 --no-commit - < diff --git a/a b/b > rename from a diff -r 612ed41ae359 -r e89f909edffa tests/test-import.t --- a/tests/test-import.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-import.t Thu Apr 16 20:57:51 2015 -0500 @@ -670,6 +670,25 @@ $ hg status $ cat a bb + +test --prefix + + $ mkdir -p dir/dir2 + $ echo b > dir/dir2/b + $ hg ci -Am b + adding dir/dir2/b + $ hg import -p2 --prefix dir - << EOF + > foobar + > --- drop1/drop2/dir2/b + > +++ drop1/drop2/dir2/b + > @@ -1,1 +1,1 @@ + > -b + > +cc + > EOF + applying patch from stdin + $ hg status + $ cat dir/dir2/b + cc $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-issue3084.t --- a/tests/test-issue3084.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-issue3084.t Thu Apr 16 20:57:51 2015 -0500 @@ -113,8 +113,6 @@ $ echo "l" | hg merge --config ui.interactive=Yes remote turned local largefile foo into a normal file keep (l)argefile or use (n)ormal file? l - getting changed largefiles - 0 largefiles updated, 0 removed 0 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) @@ -249,8 +247,6 @@ $ hg up -Cqr large $ hg merge -r normal-id - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cat f @@ -271,8 +267,6 @@ $ hg up -Cqr large $ hg merge -r normal-same - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cat f @@ -307,8 +301,6 @@ $ hg merge -r normal2 remote turned local largefile f into a normal file keep (l)argefile or use (n)ormal file? l - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cat f @@ -372,8 +364,6 @@ $ hg merge -r normal remote turned local largefile f into a normal file keep (l)argefile or use (n)ormal file? l - getting changed largefiles - 0 largefiles updated, 0 removed 0 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cat f diff -r 612ed41ae359 -r e89f909edffa tests/test-keyword.t --- a/tests/test-keyword.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-keyword.t Thu Apr 16 20:57:51 2015 -0500 @@ -473,18 +473,24 @@ $ hg -v record -l msg -d '12 2' r< y + > y > EOF diff --git a/r b/r new file mode 100644 examine changes to 'r'? [Ynesfdaq?] y + @@ -0,0 +1,1 @@ + +$Id$ + record this change to 'r'? [Ynesfdaq?] y + + resolving manifests + patching file r committing files: r committing manifest committing changelog committed changeset 3:82a2f715724d overwriting r expanding keywords - - status call required for dirstate.normallookup() check $ hg status r $ hg --verbose rollback repository tip rolled back to revision 2 (undo commit) @@ -501,11 +507,18 @@ $ hg add i $ hg --verbose record -d '13 1' -m recignored< y + > y > EOF diff --git a/i b/i new file mode 100644 examine changes to 'i'? [Ynesfdaq?] y + @@ -0,0 +1,1 @@ + +$Id$ + record this change to 'i'? [Ynesfdaq?] y + + resolving manifests + patching file i committing files: i committing manifest diff -r 612ed41ae359 -r e89f909edffa tests/test-largefiles-cache.t --- a/tests/test-largefiles-cache.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-largefiles-cache.t Thu Apr 16 20:57:51 2015 -0500 @@ -136,7 +136,7 @@ #endif Test issue 4053 (remove --after on a deleted, uncommitted file shouldn't say -it is missing, but a remove on a nonexistant unknown file still should. Same +it is missing, but a remove on a nonexistent unknown file still should. Same for a forget.) $ cd src @@ -153,3 +153,29 @@ ENOENT: * (glob) not removing z: file is already untracked [1] + +Largefiles are accessible from the share's store + $ cd .. + $ hg share -q src share_dst --config extensions.share= + $ hg -R share_dst update -r0 + getting changed largefiles + 1 largefiles updated, 0 removed + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ echo modified > share_dst/large + $ hg -R share_dst ci -m modified + created new head + +Only dirstate is in the local store for the share, and the largefile is in the +share source's local store. Avoid the extra largefiles added in the unix +conditional above. + $ hash=`hg -R share_dst cat share_dst/.hglf/large` + $ echo $hash + e2fb5f2139d086ded2cb600d5a91a196e76bf020 + + $ find share_dst/.hg/largefiles/* | sort + share_dst/.hg/largefiles/dirstate + + $ find src/.hg/largefiles/* | egrep "(dirstate|$hash)" | sort + src/.hg/largefiles/dirstate + src/.hg/largefiles/e2fb5f2139d086ded2cb600d5a91a196e76bf020 diff -r 612ed41ae359 -r e89f909edffa tests/test-largefiles-misc.t --- a/tests/test-largefiles-misc.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-largefiles-misc.t Thu Apr 16 20:57:51 2015 -0500 @@ -248,7 +248,7 @@ commit: 1 subrepos update: (current) $ hg ci -m "this commit should fail without -S" - abort: uncommitted changes in subrepo subrepo + abort: uncommitted changes in subrepository 'subrepo' (use --subrepos for recursive commit) [255] @@ -336,6 +336,13 @@ ../lf_subrepo_archive/subrepo ../lf_subrepo_archive/subrepo/large.txt ../lf_subrepo_archive/subrepo/normal.txt + $ cat ../lf_subrepo_archive/.hg_archival.txt + repo: 41bd42f10efa43698cc02052ea0977771cba506d + node: d56a95e6522858bc08a724c4fe2bdee066d1c30b + branch: default + latesttag: null + latesttagdistance: 4 + changessincelatesttag: 4 Test update with subrepos. @@ -357,11 +364,17 @@ $ hg update -C getting changed largefiles 1 largefiles updated, 0 removed - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg status -S + $ hg forget -v subrepo/large.txt + removing subrepo/large.txt (glob) + +Test reverting a forgotten file + $ hg revert -R subrepo subrepo/large.txt + $ hg status -SA subrepo/large.txt + C subrepo/large.txt + $ hg rm -v subrepo/large.txt removing subrepo/large.txt (glob) $ hg revert -R subrepo subrepo/large.txt @@ -443,6 +456,10 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: anotherlarge + $ hg --debug log -T '{rev}: {desc}\n' ../sub/anotherlarge + updated patterns: ['../.hglf/sub/../sub/anotherlarge', '../sub/anotherlarge'] + 1: anotherlarge + $ hg log -G anotherlarge @ changeset: 1:9627a577c5e9 | tag: tip @@ -450,6 +467,30 @@ | date: Thu Jan 01 00:00:00 1970 +0000 | summary: anotherlarge | + + $ hg log glob:another* + changeset: 1:9627a577c5e9 + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: anotherlarge + + $ hg --debug log -T '{rev}: {desc}\n' -G glob:another* + updated patterns: ['glob:../.hglf/sub/another*', 'glob:another*'] + @ 1: anotherlarge + | + +#if no-msys + $ hg --debug log -T '{rev}: {desc}\n' 'glob:../.hglf/sub/another*' # no-msys + updated patterns: ['glob:../.hglf/sub/another*'] + 1: anotherlarge + + $ hg --debug log -G -T '{rev}: {desc}\n' 'glob:../.hglf/sub/another*' # no-msys + updated patterns: ['glob:../.hglf/sub/another*'] + @ 1: anotherlarge + | +#endif + $ echo more >> anotherlarge $ hg st . M anotherlarge @@ -460,8 +501,33 @@ ? sub/anotherlarge.orig $ cd .. +Test glob logging from the root dir + $ hg log glob:**another* + changeset: 1:9627a577c5e9 + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: anotherlarge + + $ hg log -G glob:**another* + @ changeset: 1:9627a577c5e9 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: anotherlarge + | + $ cd .. +Log from outer space + $ hg --debug log -R addrm2 -T '{rev}: {desc}\n' 'addrm2/sub/anotherlarge' + updated patterns: ['addrm2/.hglf/sub/anotherlarge', 'addrm2/sub/anotherlarge'] + 1: anotherlarge + $ hg --debug log -R addrm2 -T '{rev}: {desc}\n' 'addrm2/.hglf/sub/anotherlarge' + updated patterns: ['addrm2/.hglf/sub/anotherlarge'] + 1: anotherlarge + + Check error message while exchange ========================================================= @@ -737,8 +803,6 @@ R d1/f $ hg merge merging d2/f and d1/f to d2/f - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 1 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-largefiles.t --- a/tests/test-largefiles.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-largefiles.t Thu Apr 16 20:57:51 2015 -0500 @@ -581,8 +581,6 @@ C sub2/large6 C sub2/large7 $ hg up -C '.^' - getting changed largefiles - 0 largefiles updated, 0 removed 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg remove large $ hg addremove --traceback @@ -1183,12 +1181,12 @@ adding manifests adding file changes added 1 changesets with 2 changes to 2 files (+1 heads) - 0 largefiles cached rebasing 8:f574fb32bb45 "modify normal file largefile in repo d" Invoking status precommit hook M sub/normal4 M sub2/large6 saved backup bundle to $TESTTMP/d/.hg/strip-backup/f574fb32bb45-dd1d9f80-backup.hg (glob) + 0 largefiles cached $ [ -f .hg/largefiles/e166e74c7303192238d60af5a9c4ce9bef0b7928 ] $ hg log --template '{rev}:{node|short} {desc|firstline}\n' 9:598410d3eb9a modify normal file largefile in repo d @@ -1431,8 +1429,6 @@ verify that largefile .orig file no longer is overwritten on every update -C: $ hg update --clean - getting changed largefiles - 0 largefiles updated, 0 removed 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ cat sub2/large7.orig mistake diff -r 612ed41ae359 -r e89f909edffa tests/test-log.t --- a/tests/test-log.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-log.t Thu Apr 16 20:57:51 2015 -0500 @@ -46,18 +46,31 @@ $ hg ci -me -d '5 0' Make sure largefiles doesn't interfere with logging a regular file - $ hg log a --config extensions.largefiles= - changeset: 0:9161b9aeaf16 - user: test - date: Thu Jan 01 00:00:01 1970 +0000 - summary: a - + $ hg --debug log a -T '{rev}: {desc}\n' --config extensions.largefiles= + updated patterns: ['.hglf/a', 'a'] + 0: a $ hg log a changeset: 0:9161b9aeaf16 user: test date: Thu Jan 01 00:00:01 1970 +0000 summary: a + $ hg log glob:a* + changeset: 3:2ca5ba701980 + user: test + date: Thu Jan 01 00:00:04 1970 +0000 + summary: d + + changeset: 0:9161b9aeaf16 + user: test + date: Thu Jan 01 00:00:01 1970 +0000 + summary: a + + $ hg --debug log glob:a* -T '{rev}: {desc}\n' --config extensions.largefiles= + updated patterns: ['glob:.hglf/a*', 'glob:a*'] + 3: d + 0: a + log on directory $ hg log dir @@ -634,7 +647,7 @@ -log -f -r 1:tip +log -f -r '1 + 4' $ hg up -C 0 1 files updated, 0 files merged, 1 files removed, 0 files unresolved @@ -642,25 +655,24 @@ $ hg ci -Amb2 -d '1 0' adding b2 created new head - $ hg log -f -r 1:tip + $ hg log -f -r '1 + 4' + changeset: 4:ddb82e70d1a1 + tag: tip + parent: 0:67e992f2c4f3 + user: test + date: Thu Jan 01 00:00:01 1970 +0000 + summary: b2 + changeset: 1:3d5bf5654eda user: test date: Thu Jan 01 00:00:01 1970 +0000 summary: r1 - changeset: 2:60c670bf5b30 + changeset: 0:67e992f2c4f3 user: test date: Thu Jan 01 00:00:01 1970 +0000 - summary: r2 + summary: base - changeset: 3:e62f78d544b4 - parent: 1:3d5bf5654eda - user: test - date: Thu Jan 01 00:00:01 1970 +0000 - summary: b1 - - - log -f -r null $ hg log -f -r null @@ -675,10 +687,17 @@ +log -f with null parent + + $ hg up -C null + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg log -f + + log -r . with two parents $ hg up -C 3 - 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg merge tip 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) @@ -1342,6 +1361,11 @@ date: Thu Jan 01 00:00:00 1970 +0000 summary: add foo, related + changeset: 2:c4c64aedf0f7 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add unrelated old foo + $ cd .. Issue2383: hg log showing _less_ differences than hg diff @@ -1599,6 +1623,70 @@ user: date: Thu Jan 01 00:00:00 1970 +0000 +working-directory revision requires special treatment + + $ hg log -r 'wdir()' + changeset: 0:65624cd9070a+ + user: test + date: [A-Za-z0-9:+ ]+ (re) + + $ hg log -r 'wdir()' -q + 0:65624cd9070a+ + + $ hg log -r 'wdir()' --debug + changeset: 0:65624cd9070a035fa7191a54f2b8af39f16b0c08+ + phase: draft + parent: 0:65624cd9070a035fa7191a54f2b8af39f16b0c08 + parent: -1:0000000000000000000000000000000000000000 + user: test + date: [A-Za-z0-9:+ ]+ (re) + extra: branch=default + + $ hg log -r 'wdir()' -Tjson + [ + { + "rev": null, + "node": null, + "branch": "default", + "phase": "draft", + "user": "test", + "date": [*, 0], (glob) + "desc": "", + "bookmarks": [], + "tags": ["tip"], + "parents": ["65624cd9070a035fa7191a54f2b8af39f16b0c08"] + } + ] + + $ hg log -r 'wdir()' -Tjson -q + [ + { + "rev": null, + "node": null + } + ] + + $ hg log -r 'wdir()' -Tjson --debug + [ + { + "rev": null, + "node": null, + "branch": "default", + "phase": "draft", + "user": "test", + "date": [*, 0], (glob) + "desc": "", + "bookmarks": [], + "tags": ["tip"], + "parents": ["65624cd9070a035fa7191a54f2b8af39f16b0c08"], + "manifest": null, + "extra": {"branch": "default"}, + "modified": [], + "added": [], + "removed": [] + } + ] + Check that adding an arbitrary name shows up in log automatically $ cat > ../names.py < file1 + $ echo b > file2 + $ echo c > file3 + $ hg ci -Aqm 'initial' + $ echo d > file2 + $ hg ci -m 'modify file2' + +Check that 'hg verify', which uses manifest.readdelta(), works + + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 3 files, 2 changesets, 4 total revisions + +Check that manifest revlog is smaller than for v1 + + $ hg debugindex -m + rev offset length base linkrev nodeid p1 p2 + 0 0 81 0 0 57361477c778 000000000000 000000000000 + 1 81 33 0 1 aeaab5a2ef74 57361477c778 000000000000 diff -r 612ed41ae359 -r e89f909edffa tests/test-merge-tools.t --- a/tests/test-merge-tools.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-merge-tools.t Thu Apr 16 20:57:51 2015 -0500 @@ -603,7 +603,8 @@ true.priority=1 true.executable=cat # hg update -C 1 - $ hg debugsetparent 0 + $ hg update -q 0 + $ hg revert -q -r 1 . $ hg update -r 2 merging f revision 1 @@ -628,7 +629,8 @@ true.priority=1 true.executable=cat # hg update -C 1 - $ hg debugsetparent 0 + $ hg update -q 0 + $ hg revert -q -r 1 . $ hg update -r 2 --tool false merging f merging f failed! diff -r 612ed41ae359 -r e89f909edffa tests/test-module-imports.t --- a/tests/test-module-imports.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-module-imports.t Thu Apr 16 20:57:51 2015 -0500 @@ -21,6 +21,9 @@ these may expose other cycles. $ hg locate 'mercurial/**.py' | sed 's-\\-/-g' | xargs python "$import_checker" + mercurial/crecord.py mixed imports + stdlib: fcntl, termios + relative: curses mercurial/dispatch.py mixed imports stdlib: commands relative: error, extensions, fancyopts, hg, hook, util @@ -29,11 +32,11 @@ relative: error, merge, util mercurial/revset.py mixed imports stdlib: parser - relative: discovery, error, hbisect, phases, util + relative: error, hbisect, phases, util mercurial/templater.py mixed imports stdlib: parser relative: config, error, templatefilters, templatekw, util mercurial/ui.py mixed imports stdlib: formatter relative: config, error, scmutil, util - Import cycle: mercurial.cmdutil -> mercurial.context -> mercurial.subrepo -> mercurial.cmdutil -> mercurial.cmdutil + Import cycle: mercurial.cmdutil -> mercurial.context -> mercurial.subrepo -> mercurial.cmdutil diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-eol.t --- a/tests/test-mq-eol.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-eol.t Thu Apr 16 20:57:51 2015 -0500 @@ -60,7 +60,7 @@ Hunk #1 FAILED at 0 1 out of 1 hunks FAILED -- saving rejects to file a.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh eol.diff [2] $ hg qpop @@ -72,7 +72,7 @@ $ hg --config patch.eol='LFCR' qpush applying eol.diff patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh eol.diff [2] $ hg qpop @@ -169,7 +169,7 @@ Hunk #1 FAILED at 0 1 out of 1 hunks FAILED -- saving rejects to file a.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh patch1 [2] $ hg qpop @@ -192,7 +192,7 @@ Hunk #1 FAILED at 0 1 out of 1 hunks FAILED -- saving rejects to file a.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh patch1 [2] $ hg qpop diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-missingfiles.t --- a/tests/test-mq-missingfiles.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-missingfiles.t Thu Apr 16 20:57:51 2015 -0500 @@ -44,7 +44,7 @@ unable to find 'b' for patching 2 out of 2 hunks FAILED -- saving rejects to file b.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh changeb [2] @@ -97,7 +97,7 @@ 2 out of 2 hunks FAILED -- saving rejects to file bb.rej b not tracked! patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh changebb [2] $ cat a @@ -149,7 +149,7 @@ unable to find 'b' for patching 1 out of 1 hunks FAILED -- saving rejects to file b.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh changeb [2] $ hg st diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-qpush-exact.t --- a/tests/test-mq-qpush-exact.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-qpush-exact.t Thu Apr 16 20:57:51 2015 -0500 @@ -203,7 +203,7 @@ file fp0 already exists 1 out of 1 hunks FAILED -- saving rejects to file fp0.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh p0 [2] $ cat fp0 @@ -230,7 +230,7 @@ file fp1 already exists 1 out of 1 hunks FAILED -- saving rejects to file fp1.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh p1 [2] $ cat fp1 diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-qpush-fail.t --- a/tests/test-mq-qpush-fail.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-qpush-fail.t Thu Apr 16 20:57:51 2015 -0500 @@ -284,7 +284,7 @@ b committing manifest committing changelog - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh p3 [2] $ cat a.orig diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-subrepo-svn.t --- a/tests/test-mq-subrepo-svn.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-subrepo-svn.t Thu Apr 16 20:57:51 2015 -0500 @@ -50,7 +50,7 @@ $ cd .. $ hg status -S # doesn't show status for svn subrepos (yet) $ hg qnew -m1 1.diff - abort: uncommitted changes in subrepository sub + abort: uncommitted changes in subrepository 'sub' [255] $ cd .. diff -r 612ed41ae359 -r e89f909edffa tests/test-mq-subrepo.t --- a/tests/test-mq-subrepo.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq-subrepo.t Thu Apr 16 20:57:51 2015 -0500 @@ -102,7 +102,7 @@ A .hgsub A sub/a % qnew -X path:no-effect -m0 0.diff - abort: uncommitted changes in subrepository sub + abort: uncommitted changes in subrepository 'sub' [255] % update substate when adding .hgsub w/clean updated subrepo A .hgsub @@ -117,7 +117,7 @@ M .hgsub A sub2/a % qnew --cwd .. -R repo-2499-qnew -X path:no-effect -m1 1.diff - abort: uncommitted changes in subrepository sub2 + abort: uncommitted changes in subrepository 'sub2' [255] % update substate when modifying .hgsub w/clean updated subrepo M .hgsub @@ -161,7 +161,7 @@ A .hgsub A sub/a % qrefresh - abort: uncommitted changes in subrepository sub + abort: uncommitted changes in subrepository 'sub' [255] % update substate when adding .hgsub w/clean updated subrepo A .hgsub @@ -177,7 +177,7 @@ M .hgsub A sub2/a % qrefresh - abort: uncommitted changes in subrepository sub2 + abort: uncommitted changes in subrepository 'sub2' [255] % update substate when modifying .hgsub w/clean updated subrepo M .hgsub @@ -295,7 +295,12 @@ new file mode 100644 examine changes to '.hgsub'? [Ynesfdaq?] y - abort: uncommitted changes in subrepository sub + @@ -0,0 +1,1 @@ + +sub = sub + record this change to '.hgsub'? [Ynesfdaq?] y + + warning: subrepo spec file '.hgsub' not found + abort: uncommitted changes in subrepository 'sub' [255] % update substate when adding .hgsub w/clean updated subrepo A .hgsub @@ -304,10 +309,14 @@ new file mode 100644 examine changes to '.hgsub'? [Ynesfdaq?] y + @@ -0,0 +1,1 @@ + +sub = sub + record this change to '.hgsub'? [Ynesfdaq?] y + + warning: subrepo spec file '.hgsub' not found path sub source sub revision b2fdb12cd82b021c3b7053d67802e77b6eeaee31 - $ testmod qrecord --config ui.interactive=1 -m1 1.diff < y > y @@ -326,7 +335,7 @@ +sub2 = sub2 record this change to '.hgsub'? [Ynesfdaq?] y - abort: uncommitted changes in subrepository sub2 + abort: uncommitted changes in subrepository 'sub2' [255] % update substate when modifying .hgsub w/clean updated subrepo M .hgsub diff -r 612ed41ae359 -r e89f909edffa tests/test-mq.t --- a/tests/test-mq.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-mq.t Thu Apr 16 20:57:51 2015 -0500 @@ -311,14 +311,13 @@ qpush with dump of tag cache Dump the tag cache to ensure that it has exactly one head after qpush. - $ rm -f .hg/cache/tags + $ rm -f .hg/cache/tags2-visible $ hg tags > /dev/null -.hg/cache/tags (pre qpush): +.hg/cache/tags2-visible (pre qpush): - $ cat .hg/cache/tags + $ cat .hg/cache/tags2-visible 1 [\da-f]{40} (re) - $ hg qpush applying test.patch now at: test.patch @@ -326,11 +325,10 @@ 2: draft $ hg tags > /dev/null -.hg/cache/tags (post qpush): +.hg/cache/tags2-visible (post qpush): - $ cat .hg/cache/tags + $ cat .hg/cache/tags2-visible 2 [\da-f]{40} (re) - $ checkundo qpush $ cd .. @@ -870,7 +868,7 @@ file foo already exists 1 out of 1 hunks FAILED -- saving rejects to file foo.rej patch failed, unable to continue (try -v) - patch failed, rejects left in working dir + patch failed, rejects left in working directory errors during apply, please fix and refresh bar [2] $ hg st diff -r 612ed41ae359 -r e89f909edffa tests/test-obsolete-tag-cache.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete-tag-cache.t Thu Apr 16 20:57:51 2015 -0500 @@ -0,0 +1,113 @@ + $ cat >> $HGRCPATH << EOF + > [extensions] + > blackbox= + > rebase= + > mock=$TESTDIR/mockblackbox.py + > + > [experimental] + > evolution = createmarkers + > EOF + +Create a repo with some tags + + $ hg init repo + $ cd repo + $ echo initial > foo + $ hg -q commit -A -m initial + $ hg tag -m 'test tag' test1 + $ echo first > first + $ hg -q commit -A -m first + $ hg tag -m 'test2 tag' test2 + $ hg -q up -r 0 + $ echo newhead > newhead + $ hg commit -A -m newhead + adding newhead + created new head + $ hg tag -m 'test head 2 tag' head2 + + $ hg log -G -T '{rev}:{node|short} {tags} {desc}\n' + @ 5:2942a772f72a tip test head 2 tag + | + o 4:042eb6bfcc49 head2 newhead + | + | o 3:c3cb30f2d2cd test2 tag + | | + | o 2:d75775ffbc6b test2 first + | | + | o 1:5f97d42da03f test tag + |/ + o 0:55482a6fb4b1 test1 initial + + +Trigger tags cache population by doing something that accesses tags info + + $ hg tags + tip 5:2942a772f72a + head2 4:042eb6bfcc49 + test2 2:d75775ffbc6b + test1 0:55482a6fb4b1 + + $ cat .hg/cache/tags2-visible + 5 2942a772f72a444bef4bef13874d515f50fa27b6 + 042eb6bfcc4909bad84a1cbf6eb1ddf0ab587d41 head2 + 55482a6fb4b1881fa8f746fd52cf6f096bb21c89 test1 + d75775ffbc6bca1794d300f5571272879bd280da test2 + +Hiding a non-tip changeset should change filtered hash and cause tags recompute + + $ hg debugobsolete -d '0 0' c3cb30f2d2cd0aae008cc91a07876e3c5131fd22 -u dummyuser + + $ hg tags + tip 5:2942a772f72a + head2 4:042eb6bfcc49 + test1 0:55482a6fb4b1 + + $ cat .hg/cache/tags2-visible + 5 2942a772f72a444bef4bef13874d515f50fa27b6 f34fbc9a9769ba9eff5aff3d008a6b49f85c08b1 + 042eb6bfcc4909bad84a1cbf6eb1ddf0ab587d41 head2 + 55482a6fb4b1881fa8f746fd52cf6f096bb21c89 test1 + + $ hg blackbox -l 4 + 1970/01/01 00:00:00 bob> tags + 1970/01/01 00:00:00 bob> 2/2 cache hits/lookups in * seconds (glob) + 1970/01/01 00:00:00 bob> writing .hg/cache/tags2-visible with 2 tags + 1970/01/01 00:00:00 bob> tags exited 0 after * seconds (glob) + +Hiding another changeset should cause the filtered hash to change + + $ hg debugobsolete -d '0 0' d75775ffbc6bca1794d300f5571272879bd280da -u dummyuser + $ hg debugobsolete -d '0 0' 5f97d42da03fd56f3b228b03dfe48af5c0adf75b -u dummyuser + + $ hg tags + tip 5:2942a772f72a + head2 4:042eb6bfcc49 + + $ cat .hg/cache/tags2-visible + 5 2942a772f72a444bef4bef13874d515f50fa27b6 2fce1eec33263d08a4d04293960fc73a555230e4 + 042eb6bfcc4909bad84a1cbf6eb1ddf0ab587d41 head2 + + $ hg blackbox -l 4 + 1970/01/01 00:00:00 bob> tags + 1970/01/01 00:00:00 bob> 1/1 cache hits/lookups in * seconds (glob) + 1970/01/01 00:00:00 bob> writing .hg/cache/tags2-visible with 1 tags + 1970/01/01 00:00:00 bob> tags exited 0 after * seconds (glob) + +Resolving tags on an unfiltered repo writes a separate tags cache + + $ hg --hidden tags + tip 5:2942a772f72a + head2 4:042eb6bfcc49 + test2 2:d75775ffbc6b + test1 0:55482a6fb4b1 + + $ cat .hg/cache/tags2 + 5 2942a772f72a444bef4bef13874d515f50fa27b6 + 042eb6bfcc4909bad84a1cbf6eb1ddf0ab587d41 head2 + 55482a6fb4b1881fa8f746fd52cf6f096bb21c89 test1 + d75775ffbc6bca1794d300f5571272879bd280da test2 + + $ hg blackbox -l 4 + 1970/01/01 00:00:00 bob> --hidden tags + 1970/01/01 00:00:00 bob> 2/2 cache hits/lookups in * seconds (glob) + 1970/01/01 00:00:00 bob> writing .hg/cache/tags2 with 3 tags + 1970/01/01 00:00:00 bob> --hidden tags exited 0 after * seconds (glob) diff -r 612ed41ae359 -r e89f909edffa tests/test-obsolete.t --- a/tests/test-obsolete.t Thu Apr 16 22:33:53 2015 +0900 +++ b/tests/test-obsolete.t Thu Apr 16 20:57:51 2015 -0500 @@ -11,7 +11,7 @@ > hg ci -m "add $1" > } $ getid() { - > hg id --debug --hidden -ir "desc('$1')" + > hg log -T "{node}\n" --hidden -r "desc('$1')" > } $ cat > debugkeys.py <
    rev   line source