# HG changeset patch # User Martin Geisler # Date 1304684993 -7200 # Node ID 680c3c6fcb48652ac982b568430ab8a9f03e857f # Parent 21b8ce4d33318dc916659bd62c706cdfadf7eb34# Parent 7b627923739f1f22b9ef30e203d366d5de656a91 merge with stable The change from 7b627923739f (parent 2) is not needed after switching to the new url parser. diff -r 7b627923739f -r 680c3c6fcb48 .hgignore --- a/.hgignore Thu May 05 16:01:09 2011 +0200 +++ b/.hgignore Fri May 06 14:29:53 2011 +0200 @@ -7,6 +7,7 @@ *.mergebackup *.o *.so +*.dll *.pyd *.pyc *.pyo diff -r 7b627923739f -r 680c3c6fcb48 Makefile --- a/Makefile Thu May 05 16:01:09 2011 +0200 +++ b/Makefile Fri May 06 14:29:53 2011 +0200 @@ -78,6 +78,8 @@ dist-notests: doc MANIFEST TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist +check: tests + tests: cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) diff -r 7b627923739f -r 680c3c6fcb48 contrib/check-code.py --- a/contrib/check-code.py Thu May 05 16:01:09 2011 +0200 +++ b/contrib/check-code.py Fri May 06 14:29:53 2011 +0200 @@ -42,6 +42,7 @@ testpats = [ + [ (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"), (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"), (r'^function', "don't use 'function', use old style"), @@ -66,6 +67,10 @@ (r'^source\b', "don't use 'source', use '.'"), (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"), (r'ls\s+[^|-]+\s+-', "options to 'ls' must come before filenames"), + (r'[^>]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"), + ], + # warnings + [] ] testfilters = [ @@ -76,6 +81,7 @@ uprefix = r"^ \$ " uprefixc = r"^ > " utestpats = [ + [ (r'^(\S| $ ).*(\S\s+|^\s+)\n', "trailing whitespace on non-output"), (uprefix + r'.*\|\s*sed', "use regex test output patterns instead of sed"), (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"), @@ -84,20 +90,25 @@ "explicit exit code checks unnecessary"), (uprefix + r'set -e', "don't use set -e"), (uprefixc + r'( *)\t', "don't use tabs to indent"), + ], + # warnings + [] ] -for p, m in testpats: - if p.startswith('^'): - p = uprefix + p[1:] - else: - p = uprefix + p - utestpats.append((p, m)) +for i in [0, 1]: + for p, m in testpats[i]: + if p.startswith('^'): + p = uprefix + p[1:] + else: + p = uprefix + p + utestpats[i].append((p, m)) utestfilters = [ (r"( *)(#([^\n]*\S)?)", repcomment), ] pypats = [ + [ (r'^\s*def\s*\w+\s*\(.*,\s*\(', "tuple parameter unpacking not available in Python 3+"), (r'lambda\s*\(.*,.*\)', @@ -111,7 +122,6 @@ (r'\w[+/*\-<>]\w', "missing whitespace in expression"), (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"), (r'.{85}', "line too long"), - (r'.{81}', "warning: line over 80 characters"), (r'[^\n]\Z', "no trailing newline"), (r'(\S\s+|^\s+)\n', "trailing whitespace"), # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"), @@ -149,11 +159,29 @@ (r'[^+=*!<>&| -](\s=|=\s)[^= ]', "wrong whitespace around ="), (r'raise Exception', "don't raise generic exceptions"), - (r'ui\.(status|progress|write|note|warn)\([\'\"]x', - "warning: unwrapped ui message"), (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"), (r' [=!]=\s+(True|False|None)', "comparison with singleton, use 'is' or 'is not' instead"), + (r'opener\([^)]*\).read\(', + "use opener.read() instead"), + (r'opener\([^)]*\).write\(', + "use opener.write() instead"), + (r'[\s\(](open|file)\([^)]*\)\.read\(', + "use util.readfile() instead"), + (r'[\s\(](open|file)\([^)]*\)\.write\(', + "use util.readfile() instead"), + (r'^[\s\(]*(open(er)?|file)\([^)]*\)', + "always assign an opened file to a variable, and close it afterwards"), + (r'[\s\(](open|file)\([^)]*\)\.', + "always assign an opened file to a variable, and close it afterwards"), + ], + # warnings + [ + (r'.{81}', "warning: line over 80 characters"), + (r'^\s*except:$', "warning: naked except clause"), + (r'ui\.(status|progress|write|note|warn)\([\'\"]x', + "warning: unwrapped ui message"), + ] ] pyfilters = [ @@ -164,6 +192,7 @@ ] cpats = [ + [ (r'//', "don't use //-style comments"), (r'^ ', "don't use spaces to indent"), (r'\S\t', "don't use tabs except for indent"), @@ -176,9 +205,13 @@ (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"), (r'\S+ (\+\+|--)', "use foo++, not foo ++"), (r'\w,\w', "missing whitespace after ,"), - (r'\w[+/*]\w', "missing whitespace in expression"), + (r'^[^#]\w[+/*]\w', "missing whitespace in expression"), (r'^#\s+\w', "use #foo, not # foo"), (r'[^\n]\Z', "no trailing newline"), + (r'^\s*#import\b', "use only #include in standard C code"), + ], + # warnings + [] ] cfilters = [ @@ -188,11 +221,31 @@ (r'(\()([^)]+\))', repcallspaces), ] +inutilpats = [ + [ + (r'\bui\.', "don't use ui in util"), + ], + # warnings + [] +] + +inrevlogpats = [ + [ + (r'\brepo\.', "don't use repo in revlog"), + ], + # warnings + [] +] + checks = [ ('python', r'.*\.(py|cgi)$', pyfilters, pypats), ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats), ('c', r'.*\.c$', cfilters, cpats), ('unified test', r'.*\.t$', utestfilters, utestpats), + ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters, + inrevlogpats), + ('layering violation ui in util', r'mercurial/util\.py', pyfilters, + inutilpats), ] class norepeatlogger(object): @@ -231,7 +284,7 @@ return lines def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False, - blame=False): + blame=False, debug=False): """checks style and portability of a given file :f: filepath @@ -245,24 +298,39 @@ blamecache = None result = True for name, match, filters, pats in checks: + if debug: + print name, f fc = 0 if not re.match(match, f): + if debug: + print "Skipping %s for %s it doesn't match %s" % ( + name, match, f) continue fp = open(f) pre = post = fp.read() fp.close() if "no-" + "check-code" in pre: + if debug: + print "Skipping %s for %s it has no- and check-code" % ( + name, f) break for p, r in filters: post = re.sub(p, r, post) + if warnings: + pats = pats[0] + pats[1] + else: + pats = pats[0] # print post # uncomment to show filtered version z = enumerate(zip(pre.splitlines(), post.splitlines(True))) + if debug: + print "Checking %s for %s" % (name, f) for n, l in z: if "check-code" + "-ignore" in l[0]: + if debug: + print "Skipping %s for %s:%s (check-code -ignore)" % ( + name, f, n) continue for p, msg in pats: - if not warnings and msg.startswith("warning"): - continue if re.search(p, l[1]): bd = "" if blame: @@ -279,7 +347,6 @@ if maxerr is not None and fc >= maxerr: print " (too many errors, giving up)" break - break return result if __name__ == "__main__": @@ -290,8 +357,10 @@ help="max warnings per file") parser.add_option("-b", "--blame", action="store_true", help="use annotate to generate blame info") + parser.add_option("", "--debug", action="store_true", + help="show debug information") - parser.set_defaults(per_file=15, warnings=False, blame=False) + parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False) (options, args) = parser.parse_args() if len(args) == 0: @@ -302,6 +371,6 @@ for f in check: ret = 0 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings, - blame=options.blame): + blame=options.blame, debug=options.debug): ret = 1 sys.exit(ret) diff -r 7b627923739f -r 680c3c6fcb48 contrib/hg-ssh --- a/contrib/hg-ssh Thu May 05 16:01:09 2011 +0200 +++ b/contrib/hg-ssh Fri May 06 14:29:53 2011 +0200 @@ -16,7 +16,7 @@ (probably together with these other useful options: no-port-forwarding,no-X11-forwarding,no-agent-forwarding) -This allows pull/push over ssh to to the repositories given as arguments. +This allows pull/push over ssh from/to the repositories given as arguments. If all your repositories are subdirectories of a common directory, you can allow shorter paths with: diff -r 7b627923739f -r 680c3c6fcb48 contrib/python-hook-examples.py --- a/contrib/python-hook-examples.py Thu May 05 16:01:09 2011 +0200 +++ b/contrib/python-hook-examples.py Fri May 06 14:29:53 2011 +0200 @@ -13,7 +13,7 @@ if kwargs.get('parent2'): return node = kwargs['node'] - first = repo[node].parents()[0].node() + first = repo[node].p1().node() if 'url' in kwargs: last = repo['tip'].node() else: diff -r 7b627923739f -r 680c3c6fcb48 contrib/shrink-revlog.py --- a/contrib/shrink-revlog.py Thu May 05 16:01:09 2011 +0200 +++ b/contrib/shrink-revlog.py Fri May 06 14:29:53 2011 +0200 @@ -1,5 +1,4 @@ -"""\ -reorder a revlog (the manifest by default) to save space +"""reorder a revlog (the manifest by default) to save space Specifically, this topologically sorts the revisions in the revlog so that revisions on the same branch are adjacent as much as possible. This is a @@ -18,7 +17,7 @@ # (maybe: export before, shrink, export after, diff). import os, tempfile, errno -from mercurial import revlog, transaction, node, util +from mercurial import revlog, transaction, node, util, scmutil from mercurial import changegroup from mercurial.i18n import _ @@ -31,7 +30,10 @@ while visit: cur = visit[-1] for p in edges[cur]: - if p not in finished: + # defend against node.nullrev because it's occasionally + # possible for a node to have parents (null, something) + # rather than (something, null) + if p not in finished and p != node.nullrev: visit.append(p) break else: @@ -102,19 +104,21 @@ ui.status(_('writing revs\n')) - count = [0] - def progress(*args): - ui.progress(_('writing'), count[0], total=len(order)) - count[0] += 1 order = [r1.node(r) for r in order] # this is a bit ugly, but it works - lookup = lambda x: "%020d" % r1.linkrev(r1.rev(x)) + count = [0] + def lookup(revl, x): + count[0] += 1 + ui.progress(_('writing'), count[0], total=len(order)) + return "%020d" % revl.linkrev(revl.rev(x)) + unlookup = lambda x: int(x, 10) try: - group = util.chunkbuffer(r1.group(order, lookup, progress)) + bundler = changegroup.bundle10(lookup) + group = util.chunkbuffer(r1.group(order, bundler)) group = changegroup.unbundle10(group, "UN") r2.addgroup(group, unlookup, tr) finally: @@ -190,8 +194,8 @@ prefix = os.path.basename(indexfn)[:-1] tmpindexfn = util.mktempcopy(indexfn, emptyok=True) - r1 = revlog.revlog(util.opener(os.getcwd(), audit=False), indexfn) - r2 = revlog.revlog(util.opener(os.getcwd(), audit=False), tmpindexfn) + r1 = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), indexfn) + r2 = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), tmpindexfn) datafn, tmpdatafn = r1.datafile, r2.datafile diff -r 7b627923739f -r 680c3c6fcb48 contrib/undumprevlog --- a/contrib/undumprevlog Thu May 05 16:01:09 2011 +0200 +++ b/contrib/undumprevlog Fri May 06 14:29:53 2011 +0200 @@ -4,12 +4,12 @@ # $ undumprevlog < repo.dump import sys -from mercurial import revlog, node, util, transaction +from mercurial import revlog, node, scmutil, util, transaction for fp in (sys.stdin, sys.stdout, sys.stderr): util.set_binary(fp) -opener = util.opener('.', False) +opener = scmutil.opener('.', False) tr = transaction.transaction(sys.stderr.write, opener, "undump.journal") while 1: l = sys.stdin.readline() diff -r 7b627923739f -r 680c3c6fcb48 contrib/win32/mercurial.ini --- a/contrib/win32/mercurial.ini Thu May 05 16:01:09 2011 +0200 +++ b/contrib/win32/mercurial.ini Fri May 06 14:29:53 2011 +0200 @@ -37,7 +37,6 @@ [extensions] ;acl = ;alias = -;bookmarks = ;bugzilla = ;children = ;churn = @@ -56,7 +55,6 @@ ;mq = ;notify = ;pager = -;parentrevspec = ;patchbomb = ;progress = ;purge = diff -r 7b627923739f -r 680c3c6fcb48 contrib/wix/guids.wxi --- a/contrib/wix/guids.wxi Thu May 05 16:01:09 2011 +0200 +++ b/contrib/wix/guids.wxi Fri May 06 14:29:53 2011 +0200 @@ -33,7 +33,7 @@ - + diff -r 7b627923739f -r 680c3c6fcb48 contrib/wix/templates.wxs --- a/contrib/wix/templates.wxs Thu May 05 16:01:09 2011 +0200 +++ b/contrib/wix/templates.wxs Fri May 06 14:29:53 2011 +0200 @@ -195,7 +195,7 @@ - + diff -r 7b627923739f -r 680c3c6fcb48 contrib/zsh_completion --- a/contrib/zsh_completion Thu May 05 16:01:09 2011 +0200 +++ b/contrib/zsh_completion Fri May 06 14:29:53 2011 +0200 @@ -360,8 +360,8 @@ '(--help -h)'{-h,--help}'[display help and exit]' '--debug[debug mode]' '--debugger[start debugger]' - '--encoding[set the charset encoding (default: UTF8)]' - '--encodingmode[set the charset encoding mode (default: strict)]' + '--encoding[set the charset encoding]' + '--encodingmode[set the charset encoding mode]' '--lsprof[print improved command execution profile]' '--traceback[print traceback on exception]' '--time[time how long the command takes]' diff -r 7b627923739f -r 680c3c6fcb48 doc/hgignore.5.txt --- a/doc/hgignore.5.txt Thu May 05 16:01:09 2011 +0200 +++ b/doc/hgignore.5.txt Fri May 06 14:29:53 2011 +0200 @@ -11,85 +11,7 @@ :Manual section: 5 :Manual group: Mercurial Manual -Synopsis --------- - -The Mercurial system uses a file called ``.hgignore`` in the root -directory of a repository to control its behavior when it searches -for files that it is not currently tracking. - -Description ------------ - -The working directory of a Mercurial repository will often contain -files that should not be tracked by Mercurial. These include backup -files created by editors and build products created by compilers. -These files can be ignored by listing them in a ``.hgignore`` file in -the root of the working directory. The ``.hgignore`` file must be -created manually. It is typically put under version control, so that -the settings will propagate to other repositories with push and pull. - -An untracked file is ignored if its path relative to the repository -root directory, or any prefix path of that path, is matched against -any pattern in ``.hgignore``. - -For example, say we have an untracked file, ``file.c``, at -``a/b/file.c`` inside our repository. Mercurial will ignore ``file.c`` -if any pattern in ``.hgignore`` matches ``a/b/file.c``, ``a/b`` or ``a``. - -In addition, a Mercurial configuration file can reference a set of -per-user or global ignore files. See the |hgrc(5)|_ man page for details -of how to configure these files. Look for the "ignore" entry in the -"ui" section. - -To control Mercurial's handling of files that it manages, see the -|hg(1)|_ man page. Look for the ``-I`` and ``-X`` options. - -Syntax ------- - -An ignore file is a plain text file consisting of a list of patterns, -with one pattern per line. Empty lines are skipped. The ``#`` -character is treated as a comment character, and the ``\`` character -is treated as an escape character. - -Mercurial supports several pattern syntaxes. The default syntax used -is Python/Perl-style regular expressions. - -To change the syntax used, use a line of the following form:: - - syntax: NAME - -where ``NAME`` is one of the following: - -``regexp`` - Regular expression, Python/Perl syntax. -``glob`` - Shell-style glob. - -The chosen syntax stays in effect when parsing all patterns that -follow, until another syntax is selected. - -Neither glob nor regexp patterns are rooted. A glob-syntax pattern of -the form ``*.c`` will match a file ending in ``.c`` in any directory, -and a regexp pattern of the form ``\.c$`` will do the same. To root a -regexp pattern, start it with ``^``. - -Example -------- - -Here is an example ignore file. :: - - # use glob syntax. - syntax: glob - - *.elc - *.pyc - *~ - - # switch to regexp syntax. - syntax: regexp - ^\.pc/ +.. include:: ../mercurial/help/hgignore.txt Author ------ @@ -109,3 +31,4 @@ Public License version 2 or any later version. .. include:: common.txt + diff -r 7b627923739f -r 680c3c6fcb48 doc/hgrc.5.txt --- a/doc/hgrc.5.txt Thu May 05 16:01:09 2011 +0200 +++ b/doc/hgrc.5.txt Fri May 06 14:29:53 2011 +0200 @@ -634,6 +634,10 @@ Run before starting a local commit. Exit status 0 allows the commit to proceed. Non-zero status will cause the commit to fail. Parent changeset IDs are in ``$HG_PARENT1`` and ``$HG_PARENT2``. +``prelistkeys`` + Run before listing pushkeys (like bookmarks) in the + repository. Non-zero status will cause failure. The key namespace is + in ``$HG_NAMESPACE``. ``preoutgoing`` Run before collecting changes to send from the local repository to another. Non-zero status will cause failure. This lets you prevent @@ -643,6 +647,12 @@ ``$HG_SOURCE``. If "serve", operation is happening on behalf of remote SSH or HTTP repository. If "push", "pull" or "bundle", operation is happening on behalf of repository on same system. +``prepushkey`` + Run before a pushkey (like a bookmark) is added to the + repository. Non-zero status will cause the key to be rejected. The + key namespace is in ``$HG_NAMESPACE``, the key is in ``$HG_KEY``, + the old value (if any) is in ``$HG_OLD``, and the new value is in + ``$HG_NEW``. ``pretag`` Run before creating a tag. Exit status 0 allows the tag to be created. Non-zero status will cause the tag to fail. ID of @@ -669,6 +679,15 @@ the update to proceed. Non-zero status will prevent the update. Changeset ID of first new parent is in ``$HG_PARENT1``. If merge, ID of second new parent is in ``$HG_PARENT2``. +``listkeys`` + Run after listing pushkeys (like bookmarks) in the repository. The + key namespace is in ``$HG_NAMESPACE``. ``$HG_VALUES`` is a + dictionary containing the keys and values. +``pushkey`` + Run after a pushkey (like a bookmark) is added to the + repository. The key namespace is in ``$HG_NAMESPACE``, the key is in + ``$HG_KEY``, the old value (if any) is in ``$HG_OLD``, and the new + value is in ``$HG_NEW``. ``tag`` Run after a tag is created. ID of tagged changeset is in ``$HG_NODE``. Name of tag is in ``$HG_TAG``. Tag is local if ``$HG_LOCAL=1``, in @@ -910,9 +929,17 @@ The conflict resolution program to use during a manual merge. For more information on merge tools see :hg:`help merge-tools`. For configuring merge tools see the merge-tools_ section. -``patch`` - command to use to apply patches. Look for ``gpatch`` or ``patch`` in - PATH if unset. +``portablefilenames`` + Check for portable filenames. Can be ``warn``, ``ignore`` or ``abort``. + Default is ``warn``. + If set to ``warn`` (or ``true``), a warning message is printed on POSIX + platforms, if a file with a non-portable filename is added (e.g. a file + with a name that can't be created on Windows because it contains reserved + parts like ``AUX``, reserved characters like ``:``, or would cause a case + collision with an existing file). + If set to ``ignore`` (or ``false``), no warning is printed. + If set to ``abort``, the command is aborted. + On Windows, this configuration option is ignored and the command aborted. ``quiet`` Reduce the amount of output printed. True or False. Default is False. ``remotecmd`` @@ -1045,6 +1072,8 @@ To disable SSL verification temporarily, specify ``--insecure`` from command line. +``cache`` + Whether to support caching in hgweb. Defaults to True. ``contact`` Name or email address of the person in charge of the repository. Defaults to ui.username or ``$EMAIL`` or "unknown" if unset or empty. @@ -1085,6 +1114,9 @@ Default is False. ``ipv6`` Whether to use IPv6. Default is False. +``logourl`` + Base URL to use for logos. If unset, ``http://mercurial.selenic.com/`` + will be used. ``name`` Repository name to use in the web interface. Default is current working directory. diff -r 7b627923739f -r 680c3c6fcb48 hgext/bugzilla.py --- a/hgext/bugzilla.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/bugzilla.py Fri May 06 14:29:53 2011 +0200 @@ -1,6 +1,7 @@ # bugzilla.py - bugzilla integration for mercurial # # Copyright 2006 Vadim Gelfer +# Copyright 2011 Jim Hague # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. @@ -8,97 +9,161 @@ '''hooks for integrating with the Bugzilla bug tracker This hook extension adds comments on bugs in Bugzilla when changesets -that refer to bugs by Bugzilla ID are seen. The hook does not change -bug status. +that refer to bugs by Bugzilla ID are seen. The comment is formatted using +the Mercurial template mechanism. + +The hook does not change bug status. + +Three basic modes of access to Bugzilla are provided: + +1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later. + +2. Check data via the Bugzilla XMLRPC interface and submit bug change + via email to Bugzilla email interface. Requires Bugzilla 3.4 or later. + +3. Writing directly to the Bugzilla database. Only Bugzilla installations + using MySQL are supported. Requires Python MySQLdb. -The hook updates the Bugzilla database directly. Only Bugzilla -installations using MySQL are supported. +Writing directly to the database is susceptible to schema changes, and +relies on a Bugzilla contrib script to send out bug change +notification emails. This script runs as the user running Mercurial, +must be run on the host with the Bugzilla install, and requires +permission to read Bugzilla configuration details and the necessary +MySQL user and password to have full access rights to the Bugzilla +database. For these reasons this access mode is now considered +deprecated, and will not be updated for new Bugzilla versions going +forward. + +Access via XMLRPC needs a Bugzilla username and password to be specified +in the configuration. Comments are added under that username. Since the +configuration must be readable by all Mercurial users, it is recommended +that the rights of that user are restricted in Bugzilla to the minimum +necessary to add comments. -The hook relies on a Bugzilla script to send bug change notification -emails. That script changes between Bugzilla versions; the -'processmail' script used prior to 2.18 is replaced in 2.18 and -subsequent versions by 'config/sendbugmail.pl'. Note that these will -be run by Mercurial as the user pushing the change; you will need to -ensure the Bugzilla install file permissions are set appropriately. +Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends +email to the Bugzilla email interface to submit comments to bugs. +The From: address in the email is set to the email address of the Mercurial +user, so the comment appears to come from the Mercurial user. In the event +that the Mercurial user email is not recognised by Bugzilla as a Bugzilla +user, the email associated with the Bugzilla username used to log into +Bugzilla is used instead as the source of the comment. + +Configuration items common to all access modes: + +bugzilla.version + This access type to use. Values recognised are: + + :``xmlrpc``: Bugzilla XMLRPC interface. + :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces. + :``3.0``: MySQL access, Bugzilla 3.0 and later. + :``2.18``: MySQL access, Bugzilla 2.18 and up to but not + including 3.0. + :``2.16``: MySQL access, Bugzilla 2.16 and up to but not + including 2.18. + +bugzilla.regexp + Regular expression to match bug IDs in changeset commit message. + Must contain one "()" group. The default expression matches ``Bug + 1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``, + ``Bug 1234 and 5678`` and variations thereof. Matching is case + insensitive. + +bugzilla.style + The style file to use when formatting comments. + +bugzilla.template + Template to use when formatting comments. Overrides style if + specified. In addition to the usual Mercurial keywords, the + extension specifies: -The extension is configured through three different configuration -sections. These keys are recognized in the [bugzilla] section: + :``{bug}``: The Bugzilla bug ID. + :``{root}``: The full pathname of the Mercurial repository. + :``{webroot}``: Stripped pathname of the Mercurial repository. + :``{hgweb}``: Base URL for browsing Mercurial repositories. + + Default ``changeset {node|short} in repo {root} refers to bug + {bug}.\\ndetails:\\n\\t{desc|tabindent}`` + +bugzilla.strip + The number of path separator characters to strip from the front of + the Mercurial repository path (``{root}`` in templates) to produce + ``{webroot}``. For example, a repository with ``{root}`` + ``/var/local/my-project`` with a strip of 2 gives a value for + ``{webroot}`` of ``my-project``. Default 0. -host - Hostname of the MySQL server holding the Bugzilla database. +web.baseurl + Base URL for browsing Mercurial repositories. Referenced from + templates as ``{hgweb}``. + +Configuration items common to XMLRPC+email and MySQL access modes: + +bugzilla.usermap + Path of file containing Mercurial committer email to Bugzilla user email + mappings. If specified, the file should contain one mapping per + line:: + + committer = Bugzilla user + + See also the ``[usermap]`` section. + +The ``[usermap]`` section is used to specify mappings of Mercurial +committer email to Bugzilla user email. See also ``bugzilla.usermap``. +Contains entries of the form ``committer = Bugzilla user``. -db - Name of the Bugzilla database in MySQL. Default 'bugs'. +XMLRPC access mode configuration: + +bugzilla.bzurl + The base URL for the Bugzilla installation. + Default ``http://localhost/bugzilla``. + +bugzilla.user + The username to use to log into Bugzilla via XMLRPC. Default + ``bugs``. + +bugzilla.password + The password for Bugzilla login. + +XMLRPC+email access mode uses the XMLRPC access mode configuration items, +and also: -user - Username to use to access MySQL server. Default 'bugs'. +bugzilla.bzemail + The Bugzilla email address. + +In addition, the Mercurial email settings must be configured. See the +documentation in hgrc(5), sections ``[email]`` and ``[smtp]``. + +MySQL access mode configuration: -password +bugzilla.host + Hostname of the MySQL server holding the Bugzilla database. + Default ``localhost``. + +bugzilla.db + Name of the Bugzilla database in MySQL. Default ``bugs``. + +bugzilla.user + Username to use to access MySQL server. Default ``bugs``. + +bugzilla.password Password to use to access MySQL server. -timeout +bugzilla.timeout Database connection timeout (seconds). Default 5. -version - Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later, - '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior - to 2.18. - -bzuser +bugzilla.bzuser Fallback Bugzilla user name to record comments with, if changeset committer cannot be found as a Bugzilla user. -bzdir +bugzilla.bzdir Bugzilla install directory. Used by default notify. Default - '/var/www/html/bugzilla'. - -notify - The command to run to get Bugzilla to send bug change notification - emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id) - and 'user' (committer bugzilla email). Default depends on version; - from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl - %(id)s %(user)s". - -regexp - Regular expression to match bug IDs in changeset commit message. - Must contain one "()" group. The default expression matches 'Bug - 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug - 1234 and 5678' and variations thereof. Matching is case insensitive. - -style - The style file to use when formatting comments. - -template - Template to use when formatting comments. Overrides style if - specified. In addition to the usual Mercurial keywords, the - extension specifies:: + ``/var/www/html/bugzilla``. - {bug} The Bugzilla bug ID. - {root} The full pathname of the Mercurial repository. - {webroot} Stripped pathname of the Mercurial repository. - {hgweb} Base URL for browsing Mercurial repositories. - - Default 'changeset {node|short} in repo {root} refers ' - 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}' - -strip - The number of slashes to strip from the front of {root} to produce - {webroot}. Default 0. - -usermap - Path of file containing Mercurial committer ID to Bugzilla user ID - mappings. If specified, the file should contain one mapping per - line, "committer"="Bugzilla user". See also the [usermap] section. - -The [usermap] section is used to specify mappings of Mercurial -committer ID to Bugzilla user ID. See also [bugzilla].usermap. -"committer"="Bugzilla user" - -Finally, the [web] section supports one entry: - -baseurl - Base URL for browsing Mercurial repositories. Reference from - templates as {hgweb}. +bugzilla.notify + The command to run to get Bugzilla to send bug change notification + emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug + id) and ``user`` (committer bugzilla email). Default depends on + version; from 2.18 it is "cd %(bzdir)s && perl -T + contrib/sendbugmail.pl %(id)s %(user)s". Activating the extension:: @@ -109,11 +174,58 @@ # run bugzilla hook on every change pulled or pushed in here incoming.bugzilla = python:hgext.bugzilla.hook -Example configuration: +Example configurations: + +XMLRPC example configuration. This uses the Bugzilla at +``http://my-project.org/bugzilla``, logging in as user +``bugmail@my-project.org`` with password ``plugh``. It is used with a +collection of Mercurial repositories in ``/var/local/hg/repos/``, +with a web interface at ``http://my-project.org/hg``. :: + + [bugzilla] + bzurl=http://my-project.org/bugzilla + user=bugmail@my-project.org + password=plugh + version=xmlrpc + template=Changeset {node|short} in {root|basename}. + {hgweb}/{webroot}/rev/{node|short}\\n + {desc}\\n + strip=5 + + [web] + baseurl=http://my-project.org/hg -This example configuration is for a collection of Mercurial -repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2 -installation in /opt/bugzilla-3.2. :: +XMLRPC+email example configuration. This uses the Bugzilla at +``http://my-project.org/bugzilla``, logging in as user +``bugmail@my-project.org`` wityh password ``plugh``. It is used with a +collection of Mercurial repositories in ``/var/local/hg/repos/``, +with a web interface at ``http://my-project.org/hg``. Bug comments +are sent to the Bugzilla email address +``bugzilla@my-project.org``. :: + + [bugzilla] + bzurl=http://my-project.org/bugzilla + user=bugmail@my-project.org + password=plugh + version=xmlrpc + bzemail=bugzilla@my-project.org + template=Changeset {node|short} in {root|basename}. + {hgweb}/{webroot}/rev/{node|short}\\n + {desc}\\n + strip=5 + + [web] + baseurl=http://my-project.org/hg + + [usermap] + user@emaildomain.com=user.name@bugzilladomain.com + +MySQL example configuration. This has a local Bugzilla 3.2 installation +in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``, +the Bugzilla database name is ``bugs`` and MySQL is +accessed with MySQL username ``bugs`` password ``XYZZY``. It is used +with a collection of Mercurial repositories in ``/var/local/hg/repos/``, +with a web interface at ``http://my-project.org/hg``. :: [bugzilla] host=localhost @@ -127,46 +239,98 @@ strip=5 [web] - baseurl=http://dev.domain.com/hg + baseurl=http://my-project.org/hg [usermap] user@emaildomain.com=user.name@bugzilladomain.com -Commits add a comment to the Bugzilla bug record of the form:: +All the above add a comment to the Bugzilla bug record of the form:: Changeset 3b16791d6642 in repository-name. - http://dev.domain.com/hg/repository-name/rev/3b16791d6642 + http://my-project.org/hg/repository-name/rev/3b16791d6642 Changeset commit comment. Bug 1234. ''' from mercurial.i18n import _ from mercurial.node import short -from mercurial import cmdutil, templater, util -import re, time - -MySQLdb = None +from mercurial import cmdutil, mail, templater, util +import re, time, xmlrpclib -def buglist(ids): - return '(' + ','.join(map(str, ids)) + ')' - -class bugzilla_2_16(object): - '''support for bugzilla version 2.16.''' +class bzaccess(object): + '''Base class for access to Bugzilla.''' def __init__(self, ui): self.ui = ui + usermap = self.ui.config('bugzilla', 'usermap') + if usermap: + self.ui.readconfig(usermap, sections=['usermap']) + + def map_committer(self, user): + '''map name of committer to Bugzilla user name.''' + for committer, bzuser in self.ui.configitems('usermap'): + if committer.lower() == user.lower(): + return bzuser + return user + + # Methods to be implemented by access classes. + def filter_real_bug_ids(self, ids): + '''remove bug IDs that do not exist in Bugzilla from set.''' + pass + + def filter_cset_known_bug_ids(self, node, ids): + '''remove bug IDs where node occurs in comment text from set.''' + pass + + def add_comment(self, bugid, text, committer): + '''add comment to bug. + + If possible add the comment as being from the committer of + the changeset. Otherwise use the default Bugzilla user. + ''' + pass + + def notify(self, ids, committer): + '''Force sending of Bugzilla notification emails.''' + pass + +# Bugzilla via direct access to MySQL database. +class bzmysql(bzaccess): + '''Support for direct MySQL access to Bugzilla. + + The earliest Bugzilla version this is tested with is version 2.16. + + If your Bugzilla is version 3.2 or above, you are strongly + recommended to use the XMLRPC access method instead. + ''' + + @staticmethod + def sql_buglist(ids): + '''return SQL-friendly list of bug ids''' + return '(' + ','.join(map(str, ids)) + ')' + + _MySQLdb = None + + def __init__(self, ui): + try: + import MySQLdb as mysql + bzmysql._MySQLdb = mysql + except ImportError, err: + raise util.Abort(_('python mysql support not available: %s') % err) + + bzaccess.__init__(self, ui) + host = self.ui.config('bugzilla', 'host', 'localhost') user = self.ui.config('bugzilla', 'user', 'bugs') passwd = self.ui.config('bugzilla', 'password') db = self.ui.config('bugzilla', 'db', 'bugs') timeout = int(self.ui.config('bugzilla', 'timeout', 5)) - usermap = self.ui.config('bugzilla', 'usermap') - if usermap: - self.ui.readconfig(usermap, sections=['usermap']) self.ui.note(_('connecting to %s:%s as %s, password %s\n') % (host, db, user, '*' * len(passwd))) - self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd, - db=db, connect_timeout=timeout) + self.conn = bzmysql._MySQLdb.connect(host=host, + user=user, passwd=passwd, + db=db, + connect_timeout=timeout) self.cursor = self.conn.cursor() self.longdesc_id = self.get_longdesc_id() self.user_ids = {} @@ -177,7 +341,7 @@ self.ui.note(_('query: %s %s\n') % (args, kwargs)) try: self.cursor.execute(*args, **kwargs) - except MySQLdb.MySQLError: + except bzmysql._MySQLdb.MySQLError: self.ui.note(_('failed query: %s %s\n') % (args, kwargs)) raise @@ -190,22 +354,22 @@ return ids[0][0] def filter_real_bug_ids(self, ids): - '''filter not-existing bug ids from list.''' - self.run('select bug_id from bugs where bug_id in %s' % buglist(ids)) - return sorted([c[0] for c in self.cursor.fetchall()]) + '''filter not-existing bug ids from set.''' + self.run('select bug_id from bugs where bug_id in %s' % + bzmysql.sql_buglist(ids)) + return set([c[0] for c in self.cursor.fetchall()]) - def filter_unknown_bug_ids(self, node, ids): - '''filter bug ids from list that already refer to this changeset.''' + def filter_cset_known_bug_ids(self, node, ids): + '''filter bug ids that already refer to this changeset from set.''' self.run('''select bug_id from longdescs where bug_id in %s and thetext like "%%%s%%"''' % - (buglist(ids), short(node))) - unknown = set(ids) + (bzmysql.sql_buglist(ids), short(node))) for (id,) in self.cursor.fetchall(): self.ui.status(_('bug %d already knows about changeset %s\n') % (id, short(node))) - unknown.discard(id) - return sorted(unknown) + ids.discard(id) + return ids def notify(self, ids, committer): '''tell bugzilla to send mail.''' @@ -251,15 +415,8 @@ self.user_ids[user] = userid return userid - def map_committer(self, user): - '''map name of committer to bugzilla user name.''' - for committer, bzuser in self.ui.configitems('usermap'): - if committer.lower() == user.lower(): - return bzuser - return user - def get_bugzilla_user(self, committer): - '''see if committer is a registered bugzilla user. Return + '''See if committer is a registered bugzilla user. Return bugzilla username and userid if so. If not, return default bugzilla username and userid.''' user = self.map_committer(committer) @@ -292,19 +449,19 @@ (bugid, userid, now, self.longdesc_id)) self.conn.commit() -class bugzilla_2_18(bugzilla_2_16): +class bzmysql_2_18(bzmysql): '''support for bugzilla 2.18 series.''' def __init__(self, ui): - bugzilla_2_16.__init__(self, ui) + bzmysql.__init__(self, ui) self.default_notify = \ "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s" -class bugzilla_3_0(bugzilla_2_18): +class bzmysql_3_0(bzmysql_2_18): '''support for bugzilla 3.0 series.''' def __init__(self, ui): - bugzilla_2_18.__init__(self, ui) + bzmysql_2_18.__init__(self, ui) def get_longdesc_id(self): '''get identity of longdesc field''' @@ -314,13 +471,176 @@ raise util.Abort(_('unknown database schema')) return ids[0][0] +# Buzgilla via XMLRPC interface. + +class CookieSafeTransport(xmlrpclib.SafeTransport): + """A SafeTransport that retains cookies over its lifetime. + + The regular xmlrpclib transports ignore cookies. Which causes + a bit of a problem when you need a cookie-based login, as with + the Bugzilla XMLRPC interface. + + So this is a SafeTransport which looks for cookies being set + in responses and saves them to add to all future requests. + It appears a SafeTransport can do both HTTP and HTTPS sessions, + which saves us having to do a CookieTransport too. + """ + + # Inspiration drawn from + # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html + # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/ + + cookies = [] + def send_cookies(self, connection): + if self.cookies: + for cookie in self.cookies: + connection.putheader("Cookie", cookie) + + def request(self, host, handler, request_body, verbose=0): + self.verbose = verbose + + # issue XML-RPC request + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_cookies(h) + self.send_user_agent(h) + self.send_content(h, request_body) + + # Deal with differences between Python 2.4-2.6 and 2.7. + # In the former h is a HTTP(S). In the latter it's a + # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of + # HTTP(S) has an underlying HTTP(S)Connection, so extract + # that and use it. + try: + response = h.getresponse() + except AttributeError: + response = h._conn.getresponse() + + # Add any cookie definitions to our list. + for header in response.msg.getallmatchingheaders("Set-Cookie"): + val = header.split(": ", 1)[1] + cookie = val.split(";", 1)[0] + self.cookies.append(cookie) + + if response.status != 200: + raise xmlrpclib.ProtocolError(host + handler, response.status, + response.reason, response.msg.headers) + + payload = response.read() + parser, unmarshaller = self.getparser() + parser.feed(payload) + parser.close() + + return unmarshaller.close() + +class bzxmlrpc(bzaccess): + """Support for access to Bugzilla via the Bugzilla XMLRPC API. + + Requires a minimum Bugzilla version 3.4. + """ + + def __init__(self, ui): + bzaccess.__init__(self, ui) + + bzweb = self.ui.config('bugzilla', 'bzurl', + 'http://localhost/bugzilla/') + bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi" + + user = self.ui.config('bugzilla', 'user', 'bugs') + passwd = self.ui.config('bugzilla', 'password') + + self.bzproxy = xmlrpclib.ServerProxy(bzweb, CookieSafeTransport()) + self.bzproxy.User.login(dict(login=user, password=passwd)) + + def get_bug_comments(self, id): + """Return a string with all comment text for a bug.""" + c = self.bzproxy.Bug.comments(dict(ids=[id])) + return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']]) + + def filter_real_bug_ids(self, ids): + res = set() + bugs = self.bzproxy.Bug.get(dict(ids=sorted(ids), permissive=True)) + for bug in bugs['bugs']: + res.add(bug['id']) + return res + + def filter_cset_known_bug_ids(self, node, ids): + for id in sorted(ids): + if self.get_bug_comments(id).find(short(node)) != -1: + self.ui.status(_('bug %d already knows about changeset %s\n') % + (id, short(node))) + ids.discard(id) + return ids + + def add_comment(self, bugid, text, committer): + self.bzproxy.Bug.add_comment(dict(id=bugid, comment=text)) + +class bzxmlrpcemail(bzxmlrpc): + """Read data from Bugzilla via XMLRPC, send updates via email. + + Advantages of sending updates via email: + 1. Comments can be added as any user, not just logged in user. + 2. Bug statuses and other fields not accessible via XMLRPC can + be updated. This is not currently used. + """ + + def __init__(self, ui): + bzxmlrpc.__init__(self, ui) + + self.bzemail = self.ui.config('bugzilla', 'bzemail') + if not self.bzemail: + raise util.Abort(_("configuration 'bzemail' missing")) + mail.validateconfig(self.ui) + + def send_bug_modify_email(self, bugid, commands, comment, committer): + '''send modification message to Bugzilla bug via email. + + The message format is documented in the Bugzilla email_in.pl + specification. commands is a list of command lines, comment is the + comment text. + + To stop users from crafting commit comments with + Bugzilla commands, specify the bug ID via the message body, rather + than the subject line, and leave a blank line after it. + ''' + user = self.map_committer(committer) + matches = self.bzproxy.User.get(dict(match=[user])) + if not matches['users']: + user = self.ui.config('bugzilla', 'user', 'bugs') + matches = self.bzproxy.User.get(dict(match=[user])) + if not matches['users']: + raise util.Abort(_("default bugzilla user %s email not found") % + user) + user = matches['users'][0]['email'] + + text = "\n".join(commands) + "\n@bug_id = %d\n\n" % bugid + comment + + _charsets = mail._charsets(self.ui) + user = mail.addressencode(self.ui, user, _charsets) + bzemail = mail.addressencode(self.ui, self.bzemail, _charsets) + msg = mail.mimeencode(self.ui, text, _charsets) + msg['From'] = user + msg['To'] = bzemail + msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets) + sendmail = mail.connect(self.ui) + sendmail(user, bzemail, msg.as_string()) + + def add_comment(self, bugid, text, committer): + self.send_bug_modify_email(bugid, [], text, committer) + class bugzilla(object): # supported versions of bugzilla. different versions have # different schemas. _versions = { - '2.16': bugzilla_2_16, - '2.18': bugzilla_2_18, - '3.0': bugzilla_3_0 + '2.16': bzmysql, + '2.18': bzmysql_2_18, + '3.0': bzmysql_3_0, + 'xmlrpc': bzxmlrpc, + 'xmlrpc+email': bzxmlrpcemail } _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*' @@ -353,10 +673,12 @@ _split_re = None def find_bug_ids(self, ctx): - '''find valid bug ids that are referred to in changeset - comments and that do not already have references to this - changeset.''' + '''return set of integer bug IDs from commit comment. + Extract bug IDs from changeset comments. Filter out any that are + not known to Bugzilla, and any that already have a reference to + the given changeset in their comments. + ''' if bugzilla._bug_re is None: bugzilla._bug_re = re.compile( self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re), @@ -376,7 +698,7 @@ if ids: ids = self.filter_real_bug_ids(ids) if ids: - ids = self.filter_unknown_bug_ids(ctx.node(), ids) + ids = self.filter_cset_known_bug_ids(ctx.node(), ids) return ids def update(self, bugid, ctx): @@ -418,13 +740,6 @@ '''add comment to bugzilla for each changeset that refers to a bugzilla bug id. only add a comment once per bug, so same change seen multiple times does not fill bug with duplicate data.''' - try: - import MySQLdb as mysql - global MySQLdb - MySQLdb = mysql - except ImportError, err: - raise util.Abort(_('python mysql support not available: %s') % err) - if node is None: raise util.Abort(_('hook type %s does not pass a changeset id') % hooktype) @@ -436,6 +751,6 @@ for id in ids: bz.update(id, ctx) bz.notify(ids, util.email(ctx.user())) - except MySQLdb.MySQLError, err: - raise util.Abort(_('database error: %s') % err.args[1]) + except Exception, e: + raise util.Abort(_('Bugzilla error: %s') % e) diff -r 7b627923739f -r 680c3c6fcb48 hgext/churn.py --- a/hgext/churn.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/churn.py Fri May 06 14:29:53 2011 +0200 @@ -60,9 +60,8 @@ if df and not df(ctx.date()[0]): # doesn't match date format return - key = getkey(ctx) + key = getkey(ctx).strip() key = amap.get(key, key) # alias remap - key = key.strip() # ignore leading and trailing spaces if opts.get('changesets'): rate[key] = (rate.get(key, (0,))[0] + 1, 0) else: diff -r 7b627923739f -r 680c3c6fcb48 hgext/color.py --- a/hgext/color.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/color.py Fri May 06 14:29:53 2011 +0200 @@ -18,14 +18,16 @@ '''colorize output from some commands -This extension modifies the status and resolve commands to add color to their -output to reflect file status, the qseries command to add color to reflect -patch status (applied, unapplied, missing), and to diff-related -commands to highlight additions, removals, diff headers, and trailing -whitespace. +This extension modifies the status and resolve commands to add color +to their output to reflect file status, the qseries command to add +color to reflect patch status (applied, unapplied, missing), and to +diff-related commands to highlight additions, removals, diff headers, +and trailing whitespace. Other effects in addition to color, like bold and underlined text, are -also available. Effects are rendered with the ECMA-48 SGR control +also available. By default, the terminfo database is used to find the +terminal codes used to change color and effect. If terminfo is not +available, then effects are rendered with the ECMA-48 SGR control function (aka ANSI escape codes). Default effects may be overridden from your configuration file:: @@ -66,13 +68,35 @@ branches.current = green branches.inactive = none -The color extension will try to detect whether to use ANSI codes or -Win32 console APIs, unless it is made explicit:: +The available effects in terminfo mode are 'blink', 'bold', 'dim', +'inverse', 'invisible', 'italic', 'standout', and 'underline'; in +ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and +'underline'. How each is rendered depends on the terminal emulator. +Some may not be available for a given terminal type, and will be +silently ignored. + +Because there are only eight standard colors, this module allows you +to define color names for other color slots which might be available +for your terminal type, assuming terminfo mode. For instance:: + + color.brightblue = 12 + color.pink = 207 + color.orange = 202 + +to set 'brightblue' to color slot 12 (useful for 16 color terminals +that have brighter colors defined in the upper eight) and, 'pink' and +'orange' to colors in 256-color xterm's default color cube. These +defined colors may then be used as any of the pre-defined eight, +including appending '_background' to set the background to that color. + +The color extension will try to detect whether to use terminfo, ANSI +codes or Win32 console APIs, unless it is made explicit; e.g.:: [color] mode = ansi -Any value other than 'ansi', 'win32', or 'auto' will disable color. +Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will +disable color. ''' @@ -90,6 +114,66 @@ 'blue_background': 44, 'purple_background': 45, 'cyan_background': 46, 'white_background': 47} +def _terminfosetup(ui): + '''Initialize terminfo data and the terminal if we're in terminfo mode.''' + + global _terminfo_params + # If we failed to load curses, we go ahead and return. + if not _terminfo_params: + return + # Otherwise, see what the config file says. + mode = ui.config('color', 'mode', 'auto') + if mode not in ('auto', 'terminfo'): + return + + _terminfo_params.update((key[6:], (False, int(val))) + for key, val in ui.configitems('color') + if key.startswith('color.')) + + try: + curses.setupterm() + except curses.error, e: + _terminfo_params = {} + return + + for key, (b, e) in _terminfo_params.items(): + if not b: + continue + if not curses.tigetstr(e): + # Most terminals don't support dim, invis, etc, so don't be + # noisy and use ui.debug(). + ui.debug("no terminfo entry for %s\n" % e) + del _terminfo_params[key] + if not curses.tigetstr('setaf') or not curses.tigetstr('setab'): + ui.warn(_("no terminfo entry for setab/setaf: reverting to " + "ECMA-48 color\n")) + _terminfo_params = {} + +try: + import curses + # Mapping from effect name to terminfo attribute name or color number. + # This will also force-load the curses module. + _terminfo_params = {'none': (True, 'sgr0'), + 'standout': (True, 'smso'), + 'underline': (True, 'smul'), + 'reverse': (True, 'rev'), + 'inverse': (True, 'rev'), + 'blink': (True, 'blink'), + 'dim': (True, 'dim'), + 'bold': (True, 'bold'), + 'invisible': (True, 'invis'), + 'italic': (True, 'sitm'), + 'black': (False, curses.COLOR_BLACK), + 'red': (False, curses.COLOR_RED), + 'green': (False, curses.COLOR_GREEN), + 'yellow': (False, curses.COLOR_YELLOW), + 'blue': (False, curses.COLOR_BLUE), + 'magenta': (False, curses.COLOR_MAGENTA), + 'cyan': (False, curses.COLOR_CYAN), + 'white': (False, curses.COLOR_WHITE)} +except ImportError: + _terminfo_params = False + _styles = {'grep.match': 'red bold', 'bookmarks.current': 'green', 'branches.active': 'none', @@ -107,6 +191,7 @@ 'diff.trailingwhitespace': 'bold red_background', 'diffstat.deleted': 'red', 'diffstat.inserted': 'green', + 'ui.prompt': 'yellow', 'log.changeset': 'yellow', 'resolve.resolved': 'green bold', 'resolve.unresolved': 'red bold', @@ -120,13 +205,33 @@ 'status.unknown': 'magenta bold underline'} +def _effect_str(effect): + '''Helper function for render_effects().''' + + bg = False + if effect.endswith('_background'): + bg = True + effect = effect[:-11] + attr, val = _terminfo_params[effect] + if attr: + return curses.tigetstr(val) + elif bg: + return curses.tparm(curses.tigetstr('setab'), val) + else: + return curses.tparm(curses.tigetstr('setaf'), val) + def render_effects(text, effects): 'Wrap text in commands to turn on each effect.' if not text: return text - start = [str(_effects[e]) for e in ['none'] + effects.split()] - start = '\033[' + ';'.join(start) + 'm' - stop = '\033[' + str(_effects['none']) + 'm' + if not _terminfo_params: + start = [str(_effects[e]) for e in ['none'] + effects.split()] + start = '\033[' + ';'.join(start) + 'm' + stop = '\033[' + str(_effects['none']) + 'm' + else: + start = ''.join(_effect_str(effect) + for effect in ['none'] + effects.split()) + stop = _effect_str('none') return ''.join([start, text, stop]) def extstyles(): @@ -135,13 +240,15 @@ def configstyles(ui): for status, cfgeffects in ui.configitems('color'): - if '.' not in status: + if '.' not in status or status.startswith('color.'): continue cfgeffects = ui.configlist('color', status) if cfgeffects: good = [] for e in cfgeffects: - if e in _effects: + if not _terminfo_params and e in _effects: + good.append(e) + elif e in _terminfo_params or e[:-11] in _terminfo_params: good.append(e) else: ui.warn(_("ignoring unknown color/effect %r " @@ -183,7 +290,7 @@ s = _styles.get(l, '') if s: effects.append(s) - effects = ''.join(effects) + effects = ' '.join(effects) if effects: return '\n'.join([render_effects(s, effects) for s in msg.split('\n')]) @@ -191,30 +298,43 @@ def uisetup(ui): + global _terminfo_params if ui.plain(): return + + formatted = (os.environ.get('TERM') != 'dumb' and ui.formatted()) mode = ui.config('color', 'mode', 'auto') if mode == 'auto': if os.name == 'nt' and 'TERM' not in os.environ: # looks line a cmd.exe console, use win32 API or nothing mode = w32effects and 'win32' or 'none' else: - mode = 'ansi' + if not formatted: + _terminfo_params = False + else: + _terminfosetup(ui) + if not _terminfo_params: + mode = 'ansi' + else: + mode = 'terminfo' if mode == 'win32': if w32effects is None: # only warn if color.mode is explicitly set to win32 ui.warn(_('warning: failed to set color mode to %s\n') % mode) return _effects.update(w32effects) - elif mode != 'ansi': + elif mode == 'ansi': + _terminfo_params = {} + elif mode == 'terminfo': + _terminfosetup(ui) + else: return def colorcmd(orig, ui_, opts, cmd, cmdfunc): coloropt = opts['color'] auto = coloropt == 'auto' always = util.parsebool(coloropt) if (always or - (always is None and - (auto and (os.environ.get('TERM') != 'dumb' and ui_.formatted())))): + (always is None and auto and formatted)): colorui._colormode = mode colorui.__bases__ = (ui_.__class__,) ui_.__class__ = colorui @@ -348,13 +468,15 @@ # Look for ANSI-like codes embedded in text m = re.match(ansire, text) - while m: - for sattr in m.group(1).split(';'): - if sattr: - attr = mapcolor(int(sattr), attr) - _kernel32.SetConsoleTextAttribute(stdout, attr) - orig(m.group(2), **opts) - m = re.match(ansire, m.group(3)) - # Explicity reset original attributes - _kernel32.SetConsoleTextAttribute(stdout, origattr) + try: + while m: + for sattr in m.group(1).split(';'): + if sattr: + attr = mapcolor(int(sattr), attr) + _kernel32.SetConsoleTextAttribute(stdout, attr) + orig(m.group(2), **opts) + m = re.match(ansire, m.group(3)) + finally: + # Explicity reset original attributes + _kernel32.SetConsoleTextAttribute(stdout, origattr) diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/__init__.py --- a/hgext/convert/__init__.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/__init__.py Fri May 06 14:29:53 2011 +0200 @@ -10,7 +10,7 @@ import convcmd import cvsps import subversion -from mercurial import commands +from mercurial import commands, templatekw from mercurial.i18n import _ # Commands definition was moved elsewhere to ease demandload job. @@ -334,3 +334,34 @@ ], _('hg debugcvsps [OPTION]... [PATH]...')), } + +def kwconverted(ctx, name): + rev = ctx.extra().get('convert_revision', '') + if rev.startswith('svn:'): + if name == 'svnrev': + return str(subversion.revsplit(rev)[2]) + elif name == 'svnpath': + return subversion.revsplit(rev)[1] + elif name == 'svnuuid': + return subversion.revsplit(rev)[0] + return rev + +def kwsvnrev(repo, ctx, **args): + """:svnrev: String. Converted subversion revision number.""" + return kwconverted(ctx, 'svnrev') + +def kwsvnpath(repo, ctx, **args): + """:svnpath: String. Converted subversion revision project path.""" + return kwconverted(ctx, 'svnpath') + +def kwsvnuuid(repo, ctx, **args): + """:svnuuid: String. Converted subversion revision repository identifier.""" + return kwconverted(ctx, 'svnuuid') + +def extsetup(ui): + templatekw.keywords['svnrev'] = kwsvnrev + templatekw.keywords['svnpath'] = kwsvnpath + templatekw.keywords['svnuuid'] = kwsvnuuid + +# tell hggettext to extract docstrings from these functions: +i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid] diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/common.py --- a/hgext/convert/common.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/common.py Fri May 06 14:29:53 2011 +0200 @@ -151,6 +151,13 @@ """ return None + def getbookmarks(self): + """Return the bookmarks as a dictionary of name: revision + + Bookmark names are to be UTF-8 strings. + """ + return {} + class converter_sink(object): """Conversion sink (target) interface""" @@ -228,6 +235,13 @@ def after(self): pass + def putbookmarks(self, bookmarks): + """Put bookmarks into sink. + + bookmarks: {bookmarkname: sink_rev_id, ...} + where bookmarkname is an UTF-8 string. + """ + pass class commandline(object): def __init__(self, ui, command): @@ -240,7 +254,7 @@ def postrun(self): pass - def _cmdline(self, cmd, *args, **kwargs): + def _cmdline(self, cmd, closestdin, *args, **kwargs): cmdline = [self.command, cmd] + list(args) for k, v in kwargs.iteritems(): if len(k) == 1: @@ -257,16 +271,23 @@ cmdline = [util.shellquote(arg) for arg in cmdline] if not self.ui.debugflag: cmdline += ['2>', util.nulldev] - cmdline += ['<', util.nulldev] + if closestdin: + cmdline += ['<', util.nulldev] cmdline = ' '.join(cmdline) return cmdline def _run(self, cmd, *args, **kwargs): - cmdline = self._cmdline(cmd, *args, **kwargs) + return self._dorun(util.popen, cmd, True, *args, **kwargs) + + def _run2(self, cmd, *args, **kwargs): + return self._dorun(util.popen2, cmd, False, *args, **kwargs) + + def _dorun(self, openfunc, cmd, closestdin, *args, **kwargs): + cmdline = self._cmdline(cmd, closestdin, *args, **kwargs) self.ui.debug('running: %s\n' % (cmdline,)) self.prerun() try: - return util.popen(cmdline) + return openfunc(cmdline) finally: self.postrun() @@ -322,8 +343,9 @@ self._argmax = self._argmax / 2 - 1 return self._argmax - def limit_arglist(self, arglist, cmd, *args, **kwargs): - limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs)) + def limit_arglist(self, arglist, cmd, closestdin, *args, **kwargs): + cmdlen = len(self._cmdline(cmd, closestdin, *args, **kwargs)) + limit = self.getargmax() - cmdlen bytes = 0 fl = [] for fn in arglist: @@ -339,7 +361,7 @@ yield fl def xargs(self, arglist, cmd, *args, **kwargs): - for l in self.limit_arglist(arglist, cmd, *args, **kwargs): + for l in self.limit_arglist(arglist, cmd, True, *args, **kwargs): self.run0(cmd, *(list(args) + l), **kwargs) class mapfile(dict): diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/convcmd.py --- a/hgext/convert/convcmd.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/convcmd.py Fri May 06 14:29:53 2011 +0200 @@ -378,6 +378,16 @@ if tagsparents: self.map[tagsparents[0][0]] = nrev + bookmarks = self.source.getbookmarks() + cbookmarks = {} + for k in bookmarks: + v = bookmarks[k] + if self.map.get(v, SKIPREV) != SKIPREV: + cbookmarks[k] = self.map[v] + + if c and cbookmarks: + self.dest.putbookmarks(cbookmarks) + self.writeauthormap() finally: self.cleanup() diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/darcs.py --- a/hgext/convert/darcs.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/darcs.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,7 @@ from common import NoRepo, checktool, commandline, commit, converter_source from mercurial.i18n import _ -from mercurial import encoding, util +from mercurial import util import os, shutil, tempfile, re # The naming drift of ElementTree is fun! @@ -191,7 +191,7 @@ if rev != self.lastrev: raise util.Abort(_('internal calling inconsistency')) path = os.path.join(self.tmppath, name) - data = open(path, 'rb').read() + data = util.readfile(path) mode = os.lstat(path).st_mode mode = (mode & 0111) and 'x' or '' return data, mode diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/git.py --- a/hgext/convert/git.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/git.py Fri May 06 14:29:53 2011 +0200 @@ -17,19 +17,27 @@ # cannot remove environment variable. Just assume none have # both issues. if hasattr(os, 'unsetenv'): - def gitopen(self, s): + def gitopen(self, s, noerr=False): prevgitdir = os.environ.get('GIT_DIR') os.environ['GIT_DIR'] = self.path try: - return util.popen(s, 'rb') + if noerr: + (stdin, stdout, stderr) = util.popen3(s) + return stdout + else: + return util.popen(s, 'rb') finally: if prevgitdir is None: del os.environ['GIT_DIR'] else: os.environ['GIT_DIR'] = prevgitdir else: - def gitopen(self, s): - return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') + def gitopen(self, s, noerr=False): + if noerr: + (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s)) + return so + else: + util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') def gitread(self, s): fh = self.gitopen(s) @@ -168,3 +176,30 @@ raise util.Abort(_('cannot read changes in %s') % version) return changes + + def getbookmarks(self): + bookmarks = {} + + # Interesting references in git are prefixed + prefix = 'refs/heads/' + prefixlen = len(prefix) + + # factor two commands + gitcmd = { 'remote/': 'git ls-remote --heads origin', + '': 'git show-ref'} + + # Origin heads + for reftype in gitcmd: + try: + fh = self.gitopen(gitcmd[reftype], noerr=True) + for line in fh: + line = line.strip() + rev, name = line.split(None, 1) + if not name.startswith(prefix): + continue + name = '%s%s' % (reftype, name[prefixlen:]) + bookmarks[name] = rev + except: + pass + + return bookmarks diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/hg.py --- a/hgext/convert/hg.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/hg.py Fri May 06 14:29:53 2011 +0200 @@ -21,7 +21,7 @@ import os, time, cStringIO from mercurial.i18n import _ from mercurial.node import bin, hex, nullid -from mercurial import hg, util, context, error +from mercurial import hg, util, context, bookmarks, error from common import NoRepo, commit, converter_source, converter_sink @@ -214,6 +214,16 @@ def setfilemapmode(self, active): self.filemapmode = active + def putbookmarks(self, updatedbookmark): + if not len(updatedbookmark): + return + + self.ui.status(_("updating bookmarks\n")) + for bookmark in updatedbookmark: + self.repo._bookmarks[bookmark] = bin(updatedbookmark[bookmark]) + bookmarks.write(self.repo) + + class mercurial_source(converter_source): def __init__(self, ui, path, rev=None): converter_source.__init__(self, ui, path, rev) @@ -277,10 +287,9 @@ parents = self.parents(ctx) if not parents: files = sorted(ctx.manifest()) - if self.ignoreerrors: - # calling getcopies() is a simple way to detect missing - # revlogs and populate self.ignored - self.getcopies(ctx, parents, files) + # getcopies() is not needed for roots, but it is a simple way to + # detect missing revlogs and abort on errors or populate self.ignored + self.getcopies(ctx, parents, files) return [(f, rev) for f in files if f not in self.ignored], {} if self._changescache and self._changescache[0] == rev: m, a, r = self._changescache[1] @@ -374,3 +383,6 @@ return hex(self.repo.lookup(rev)) except error.RepoError: return None + + def getbookmarks(self): + return bookmarks.listbookmarks(self.repo) diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/monotone.py --- a/hgext/convert/monotone.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/monotone.py Fri May 06 14:29:53 2011 +0200 @@ -19,13 +19,17 @@ self.ui = ui self.path = path + self.automatestdio = False + self.rev = rev norepo = NoRepo(_("%s does not look like a monotone repository") % path) if not os.path.exists(os.path.join(path, '_MTN')): # Could be a monotone repository (SQLite db file) try: - header = file(path, 'rb').read(16) + f = file(path, 'rb') + header = f.read(16) + f.close() except: header = '' if header != 'SQLite format 3\x00': @@ -64,18 +68,103 @@ checktool('mtn', abort=False) - # test if there are any revisions - self.rev = None - try: - self.getheads() - except: - raise norepo - self.rev = rev + def mtnrun(self, *args, **kwargs): + if self.automatestdio: + return self.mtnrunstdio(*args, **kwargs) + else: + return self.mtnrunsingle(*args, **kwargs) - def mtnrun(self, *args, **kwargs): + def mtnrunsingle(self, *args, **kwargs): kwargs['d'] = self.path return self.run0('automate', *args, **kwargs) + def mtnrunstdio(self, *args, **kwargs): + # Prepare the command in automate stdio format + command = [] + for k, v in kwargs.iteritems(): + command.append("%s:%s" % (len(k), k)) + if v: + command.append("%s:%s" % (len(v), v)) + if command: + command.insert(0, 'o') + command.append('e') + + command.append('l') + for arg in args: + command += "%s:%s" % (len(arg), arg) + command.append('e') + command = ''.join(command) + + self.ui.debug("mtn: sending '%s'\n" % command) + self.mtnwritefp.write(command) + self.mtnwritefp.flush() + + return self.mtnstdioreadcommandoutput(command) + + def mtnstdioreadpacket(self): + read = None + commandnbr = '' + while read != ':': + read = self.mtnreadfp.read(1) + if not read: + raise util.Abort(_('bad mtn packet - no end of commandnbr')) + commandnbr += read + commandnbr = commandnbr[:-1] + + stream = self.mtnreadfp.read(1) + if stream not in 'mewptl': + raise util.Abort(_('bad mtn packet - bad stream type %s' % stream)) + + read = self.mtnreadfp.read(1) + if read != ':': + raise util.Abort(_('bad mtn packet - no divider before size')) + + read = None + lengthstr = '' + while read != ':': + read = self.mtnreadfp.read(1) + if not read: + raise util.Abort(_('bad mtn packet - no end of packet size')) + lengthstr += read + try: + length = long(lengthstr[:-1]) + except TypeError: + raise util.Abort(_('bad mtn packet - bad packet size %s') + % lengthstr) + + read = self.mtnreadfp.read(length) + if len(read) != length: + raise util.Abort(_("bad mtn packet - unable to read full packet " + "read %s of %s") % (len(read), length)) + + return (commandnbr, stream, length, read) + + def mtnstdioreadcommandoutput(self, command): + retval = [] + while True: + commandnbr, stream, length, output = self.mtnstdioreadpacket() + self.ui.debug('mtn: read packet %s:%s:%s\n' % + (commandnbr, stream, length)) + + if stream == 'l': + # End of command + if output != '0': + raise util.Abort(_("mtn command '%s' returned %s") % + (command, output)) + break + elif stream in 'ew': + # Error, warning output + self.ui.warn(_('%s error:\n') % self.command) + self.ui.warn(output) + elif stream == 'p': + # Progress messages + self.ui.debug('mtn: ' + output) + elif stream == 'm': + # Main stream - command output + retval.append(output) + + return ''.join(retval) + def mtnloadmanifest(self, rev): if self.manifest_rev == rev: return @@ -204,14 +293,18 @@ return data, attr def getcommit(self, rev): - certs = self.mtngetcerts(rev) + extra = {} + certs = self.mtngetcerts(rev) + if certs.get('suspend') == certs["branch"]: + extra['close'] = '1' return commit( author=certs["author"], date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")), desc=certs["changelog"], rev=rev, parents=self.mtnrun("parents", rev).splitlines(), - branch=certs["branch"]) + branch=certs["branch"], + extra=extra) def gettags(self): tags = {} @@ -225,3 +318,43 @@ # This function is only needed to support --filemap # ... and we don't support that raise NotImplementedError() + + def before(self): + # Check if we have a new enough version to use automate stdio + version = 0.0 + try: + versionstr = self.mtnrunsingle("interface_version") + version = float(versionstr) + except Exception: + raise util.Abort(_("unable to determine mtn automate interface " + "version")) + + if version >= 12.0: + self.automatestdio = True + self.ui.debug("mtn automate version %s - using automate stdio\n" % + version) + + # launch the long-running automate stdio process + self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio', + '-d', self.path) + # read the headers + read = self.mtnreadfp.readline() + if read != 'format-version: 2\n': + raise util.Abort(_('mtn automate stdio header unexpected: %s') + % read) + while read != '\n': + read = self.mtnreadfp.readline() + if not read: + raise util.Abort(_("failed to reach end of mtn automate " + "stdio headers")) + else: + self.ui.debug("mtn automate version %s - not using automate stdio " + "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version) + + def after(self): + if self.automatestdio: + self.mtnwritefp.close() + self.mtnwritefp = None + self.mtnreadfp.close() + self.mtnreadfp = None + diff -r 7b627923739f -r 680c3c6fcb48 hgext/convert/subversion.py --- a/hgext/convert/subversion.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/convert/subversion.py Fri May 06 14:29:53 2011 +0200 @@ -10,7 +10,7 @@ import urllib import urllib2 -from mercurial import strutil, util, encoding +from mercurial import strutil, scmutil, util, encoding from mercurial.i18n import _ # Subversion stuff. Works best with very recent Python SVN bindings @@ -41,6 +41,15 @@ class SvnPathNotFound(Exception): pass +def revsplit(rev): + """Parse a revision string and return (uuid, path, revnum).""" + url, revnum = rev.rsplit('@', 1) + parts = url.split('/', 1) + mod = '' + if len(parts) > 1: + mod = '/' + parts[1] + return parts[0][4:], mod, int(revnum) + def geturl(path): try: return svn.client.url_from_path(svn.core.svn_path_canonicalize(path)) @@ -259,6 +268,7 @@ except ValueError: raise util.Abort(_('svn: revision %s is not an integer') % rev) + self.trunkname = self.ui.config('convert', 'svn.trunk', 'trunk').strip('/') self.startrev = self.ui.config('convert', 'svn.startrev', default=0) try: self.startrev = int(self.startrev) @@ -268,7 +278,10 @@ raise util.Abort(_('svn: start revision %s is not an integer') % self.startrev) - self.head = self.latest(self.module, latest) + try: + self.head = self.latest(self.module, latest) + except SvnPathNotFound: + self.head = None if not self.head: raise util.Abort(_('no revision found in module %s') % self.module) @@ -285,7 +298,7 @@ def setrevmap(self, revmap): lastrevs = {} for revid in revmap.iterkeys(): - uuid, module, revnum = self.revsplit(revid) + uuid, module, revnum = revsplit(revid) lastrevnum = lastrevs.setdefault(module, revnum) if revnum > lastrevnum: lastrevs[module] = revnum @@ -380,7 +393,7 @@ files, self.removed, copies = self.expandpaths(rev, paths, parents) else: # Perform a full checkout on roots - uuid, module, revnum = self.revsplit(rev) + uuid, module, revnum = revsplit(rev) entries = svn.client.ls(self.baseurl + urllib.quote(module), optrev(revnum), True, self.ctx) files = [n for n, e in entries.iteritems() @@ -402,7 +415,7 @@ def getcommit(self, rev): if rev not in self.commits: - uuid, module, revnum = self.revsplit(rev) + uuid, module, revnum = revsplit(rev) self.module = module self.reparent(module) # We assume that: @@ -529,16 +542,6 @@ def revnum(self, rev): return int(rev.split('@')[-1]) - def revsplit(self, rev): - url, revnum = rev.rsplit('@', 1) - revnum = int(revnum) - parts = url.split('/', 1) - uuid = parts.pop(0)[4:] - mod = '' - if parts: - mod = '/' + parts[0] - return uuid, mod, revnum - def latest(self, path, stop=0): """Find the latest revid affecting path, up to stop. It may return a revision in a different module, since a branch may be moved without @@ -605,7 +608,7 @@ changed, removed = set(), set() copies = {} - new_module, revnum = self.revsplit(rev)[1:] + new_module, revnum = revsplit(rev)[1:] if new_module != self.module: self.module = new_module self.reparent(self.module) @@ -622,7 +625,7 @@ continue # Copy sources not in parent revisions cannot be # represented, ignore their origin for now - pmodule, prevnum = self.revsplit(parents[0])[1:] + pmodule, prevnum = revsplit(parents[0])[1:] if ent.copyfrom_rev < prevnum: continue copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule) @@ -633,7 +636,7 @@ copies[self.recode(entrypath)] = self.recode(copyfrom_path) elif kind == 0: # gone, but had better be a deleted *file* self.ui.debug("gone from %s\n" % ent.copyfrom_rev) - pmodule, prevnum = self.revsplit(parents[0])[1:] + pmodule, prevnum = revsplit(parents[0])[1:] parentpath = pmodule + "/" + entrypath fromkind = self._checkpath(entrypath, prevnum, pmodule) @@ -659,7 +662,7 @@ if ent.action == 'R' and parents: # If a directory is replacing a file, mark the previous # file as deleted - pmodule, prevnum = self.revsplit(parents[0])[1:] + pmodule, prevnum = revsplit(parents[0])[1:] pkind = self._checkpath(entrypath, prevnum, pmodule) if pkind == svn.core.svn_node_file: removed.add(self.recode(entrypath)) @@ -681,7 +684,7 @@ continue # Copy sources not in parent revisions cannot be # represented, ignore their origin for now - pmodule, prevnum = self.revsplit(parents[0])[1:] + pmodule, prevnum = revsplit(parents[0])[1:] if ent.copyfrom_rev < prevnum: continue copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule) @@ -736,7 +739,7 @@ # ent.copyfrom_rev may not be the actual last revision previd = self.latest(newpath, ent.copyfrom_rev) if previd is not None: - prevmodule, prevnum = self.revsplit(previd)[1:] + prevmodule, prevnum = revsplit(previd)[1:] if prevnum >= self.startrev: parents = [previd] self.ui.note( @@ -761,9 +764,8 @@ author = author and self.recode(author) or '' try: branch = self.module.split("/")[-1] - trunkname = self.ui.config('convert', 'svn.trunk', 'trunk') - if branch == trunkname.strip('/'): - branch = '' + if branch == self.trunkname: + branch = None except IndexError: branch = None @@ -834,7 +836,7 @@ raise IOError() mode = '' try: - new_module, revnum = self.revsplit(rev)[1:] + new_module, revnum = revsplit(rev)[1:] if self.module != new_module: self.module = new_module self.reparent(self.module) @@ -944,6 +946,7 @@ class svn_sink(converter_sink, commandline): commit_re = re.compile(r'Committed revision (\d+).', re.M) + uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M) def prerun(self): if self.wc: @@ -964,8 +967,6 @@ def __init__(self, ui, path): - if svn is None: - raise MissingTool(_('Could not load Subversion python bindings')) converter_sink.__init__(self, ui, path) commandline.__init__(self, ui, 'svn') self.delete = [] @@ -1000,8 +1001,8 @@ self.run0('checkout', path, wcpath) self.wc = wcpath - self.opener = util.opener(self.wc) - self.wopener = util.opener(self.wc) + 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.is_exec or None @@ -1012,8 +1013,8 @@ fp.close() util.set_flags(hook, False, True) - xport = transport.SvnRaTransport(url=geturl(path)) - self.uuid = svn.ra.get_uuid(xport.ra) + output = self.run0('info') + self.uuid = self.uuid_re.search(output).group(1).strip() def wjoin(self, *names): return os.path.join(self.wc, *names) @@ -1027,7 +1028,7 @@ os.unlink(filename) except OSError: pass - self.wopener(filename, 'w').write(data) + self.wopener.write(filename, data) if self.is_exec: was_exec = self.is_exec(self.wjoin(filename)) diff -r 7b627923739f -r 680c3c6fcb48 hgext/eol.py --- a/hgext/eol.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/eol.py Fri May 06 14:29:53 2011 +0200 @@ -73,11 +73,13 @@ only need to these filters until you have prepared a ``.hgeol`` file. The ``win32text.forbid*`` hooks provided by the win32text extension -have been unified into a single hook named ``eol.hook``. The hook will -lookup the expected line endings from the ``.hgeol`` file, which means -you must migrate to a ``.hgeol`` file first before using the hook. -Remember to enable the eol extension in the repository where you -install the hook. +have been unified into a single hook named ``eol.checkheadshook``. The +hook will lookup the expected line endings from the ``.hgeol`` file, +which means you must migrate to a ``.hgeol`` file first before using +the hook. ``eol.checkheadshook`` only checks heads, intermediate +invalid revisions will be pushed. To forbid them completely, use the +``eol.checkallhook`` hook. These hooks are best used as +``pretxnchangegroup`` hooks. See :hg:`help patterns` for more information about the glob patterns used. @@ -127,36 +129,119 @@ 'cleverdecode:': tocrlf } +class eolfile(object): + def __init__(self, ui, root, data): + self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'} + self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'} -def hook(ui, repo, node, hooktype, **kwargs): - """verify that files have expected EOLs""" + self.cfg = config.config() + # Our files should not be touched. The pattern must be + # inserted first override a '** = native' pattern. + self.cfg.set('patterns', '.hg*', 'BIN') + # We can then parse the user's patterns. + self.cfg.parse('.hgeol', data) + + isrepolf = self.cfg.get('repository', 'native') != 'CRLF' + self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf' + iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n') + self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf' + + include = [] + exclude = [] + for pattern, style in self.cfg.items('patterns'): + key = style.upper() + if key == 'BIN': + exclude.append(pattern) + else: + include.append(pattern) + # This will match the files for which we need to care + # about inconsistent newlines. + self.match = match.match(root, '', [], include, exclude) + + def setfilters(self, ui): + for pattern, style in self.cfg.items('patterns'): + key = style.upper() + try: + ui.setconfig('decode', pattern, self._decode[key]) + ui.setconfig('encode', pattern, self._encode[key]) + except KeyError: + ui.warn(_("ignoring unknown EOL style '%s' from %s\n") + % (style, self.cfg.source('patterns', pattern))) + + def checkrev(self, repo, ctx, files): + failed = [] + for f in (files or ctx.files()): + if f not in ctx: + continue + for pattern, style in self.cfg.items('patterns'): + if not match.match(repo.root, '', [pattern])(f): + continue + target = self._encode[style.upper()] + data = ctx[f].data() + if (target == "to-lf" and "\r\n" in data + or target == "to-crlf" and singlelf.search(data)): + failed.append((str(ctx), target, f)) + break + return failed + +def parseeol(ui, repo, nodes): + try: + for node in nodes: + try: + if node is None: + # Cannot use workingctx.data() since it would load + # and cache the filters before we configure them. + data = repo.wfile('.hgeol').read() + else: + data = repo[node]['.hgeol'].data() + return eolfile(ui, repo.root, data) + except (IOError, LookupError): + pass + except error.ParseError, inst: + ui.warn(_("warning: ignoring .hgeol file due to parse error " + "at %s: %s\n") % (inst.args[1], inst.args[0])) + return None + +def _checkhook(ui, repo, node, headsonly): + # Get revisions to check and touched files at the same time files = set() + revs = set() for rev in xrange(repo[node].rev(), len(repo)): - files.update(repo[rev].files()) - tip = repo['tip'] - for f in files: - if f not in tip: - continue - for pattern, target in ui.configitems('encode'): - if match.match(repo.root, '', [pattern])(f): - data = tip[f].data() - if target == "to-lf" and "\r\n" in data: - raise util.Abort(_("%s should not have CRLF line endings") - % f) - elif target == "to-crlf" and singlelf.search(data): - raise util.Abort(_("%s should not have LF line endings") - % f) - # Ignore other rules for this file - break + revs.add(rev) + if headsonly: + ctx = repo[rev] + files.update(ctx.files()) + for pctx in ctx.parents(): + revs.discard(pctx.rev()) + failed = [] + for rev in revs: + ctx = repo[rev] + eol = parseeol(ui, repo, [ctx.node()]) + if eol: + failed.extend(eol.checkrev(repo, ctx, files)) + if failed: + eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'} + msgs = [] + for node, target, f in failed: + msgs.append(_(" %s in %s should not have %s line endings") % + (f, node, eols[target])) + raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs)) + +def checkallhook(ui, repo, node, hooktype, **kwargs): + """verify that files have expected EOLs""" + _checkhook(ui, repo, node, False) + +def checkheadshook(ui, repo, node, hooktype, **kwargs): + """verify that files have expected EOLs""" + _checkhook(ui, repo, node, True) + +# "checkheadshook" used to be called "hook" +hook = checkheadshook def preupdate(ui, repo, hooktype, parent1, parent2): #print "preupdate for %s: %s -> %s" % (repo.root, parent1, parent2) - try: - repo.readhgeol(parent1) - except error.ParseError, inst: - ui.warn(_("warning: ignoring .hgeol file due to parse error " - "at %s: %s\n") % (inst.args[1], inst.args[0])) + repo.loadeol([parent1]) return False def uisetup(ui): @@ -184,66 +269,15 @@ class eolrepo(repo.__class__): - _decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'} - _encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'} - - def readhgeol(self, node=None, data=None): - if data is None: - try: - if node is None: - data = self.wfile('.hgeol').read() - else: - data = self[node]['.hgeol'].data() - except (IOError, LookupError): - return None - - if self.ui.config('eol', 'native', os.linesep) in ('LF', '\n'): - self._decode['NATIVE'] = 'to-lf' - else: - self._decode['NATIVE'] = 'to-crlf' - - eol = config.config() - # Our files should not be touched. The pattern must be - # inserted first override a '** = native' pattern. - eol.set('patterns', '.hg*', 'BIN') - # We can then parse the user's patterns. - eol.parse('.hgeol', data) - - if eol.get('repository', 'native') == 'CRLF': - self._encode['NATIVE'] = 'to-crlf' - else: - self._encode['NATIVE'] = 'to-lf' - - for pattern, style in eol.items('patterns'): - key = style.upper() - try: - self.ui.setconfig('decode', pattern, self._decode[key]) - self.ui.setconfig('encode', pattern, self._encode[key]) - except KeyError: - self.ui.warn(_("ignoring unknown EOL style '%s' from %s\n") - % (style, eol.source('patterns', pattern))) - - include = [] - exclude = [] - for pattern, style in eol.items('patterns'): - key = style.upper() - if key == 'BIN': - exclude.append(pattern) - else: - include.append(pattern) - - # This will match the files for which we need to care - # about inconsistent newlines. - return match.match(self.root, '', [], include, exclude) + def loadeol(self, nodes): + eol = parseeol(self.ui, self, nodes) + if eol is None: + return None + eol.setfilters(self.ui) + return eol.match def _hgcleardirstate(self): - try: - self._eolfile = self.readhgeol() or self.readhgeol('tip') - except error.ParseError, inst: - ui.warn(_("warning: ignoring .hgeol file due to parse error " - "at %s: %s\n") % (inst.args[1], inst.args[0])) - self._eolfile = None - + self._eolfile = self.loadeol([None, 'tip']) if not self._eolfile: self._eolfile = util.never return diff -r 7b627923739f -r 680c3c6fcb48 hgext/extdiff.py --- a/hgext/extdiff.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/extdiff.py Fri May 06 14:29:53 2011 +0200 @@ -40,6 +40,7 @@ $parent1, $plabel1 - filename, descriptive label of first parent $child, $clabel - filename, descriptive label of child revision $parent2, $plabel2 - filename, descriptive label of second parent + $root - repository root $parent is an alias for $parent1. The extdiff extension will look in your [diff-tools] and [merge-tools] @@ -61,7 +62,7 @@ from mercurial.i18n import _ from mercurial.node import short, nullid -from mercurial import cmdutil, util, commands, encoding +from mercurial import cmdutil, scmutil, util, commands, encoding import os, shlex, shutil, tempfile, re def snapshot(ui, repo, files, node, tmproot): @@ -81,7 +82,7 @@ else: ui.note(_('making snapshot of %d files from working directory\n') % (len(files))) - wopener = util.opener(base) + wopener = scmutil.opener(base) fns_and_mtime = [] ctx = repo[node] for fn in files: @@ -96,7 +97,7 @@ if 'l' in fctx.flags(): wopener.symlink(data, wfn) else: - wopener(wfn, 'w').write(data) + wopener.write(wfn, data) if 'x' in fctx.flags(): util.set_flags(dest, False, True) if node is None: @@ -122,12 +123,12 @@ msg = _('cannot specify --rev and --change at the same time') raise util.Abort(msg) elif change: - node2 = repo.lookup(change) + node2 = cmdutil.revsingle(repo, change, None).node() node1a, node1b = repo.changelog.parents(node2) else: node1a, node2 = cmdutil.revpair(repo, revs) if not revs: - node1b = repo.dirstate.parents()[1] + node1b = repo.dirstate.p2() else: node1b = nullid @@ -188,14 +189,14 @@ # Handle bogus modifies correctly by checking if the files exist if len(common) == 1: common_file = util.localpath(common.pop()) - dir1a = os.path.join(dir1a, common_file) + dir1a = os.path.join(tmproot, dir1a, common_file) label1a = common_file + rev1a - if not os.path.isfile(os.path.join(tmproot, dir1a)): + if not os.path.isfile(dir1a): dir1a = os.devnull if do3way: - dir1b = os.path.join(dir1b, common_file) + dir1b = os.path.join(tmproot, dir1b, common_file) label1b = common_file + rev1b - if not os.path.isfile(os.path.join(tmproot, dir1b)): + if not os.path.isfile(dir1b): dir1b = os.devnull dir2 = os.path.join(dir2root, dir2, common_file) label2 = common_file + rev2 @@ -205,7 +206,8 @@ # returned for parent2 replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b, plabel1=label1a, plabel2=label1b, - clabel=label2, child=dir2) + clabel=label2, child=dir2, + root=repo.root) def quote(match): key = match.group()[1:] if not do3way and key == 'parent2': @@ -213,7 +215,7 @@ return util.shellquote(replace[key]) # Match parent2 first, so 'parent1?' will match both parent1 and parent - regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel)' + regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)' if not do3way and not re.search(regex, args): args += ' $parent1 $child' args = re.sub(regex, quote, args) diff -r 7b627923739f -r 680c3c6fcb48 hgext/fetch.py --- a/hgext/fetch.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/fetch.py Fri May 06 14:29:53 2011 +0200 @@ -9,7 +9,7 @@ from mercurial.i18n import _ from mercurial.node import nullid, short -from mercurial import commands, cmdutil, hg, util, url, error +from mercurial import commands, cmdutil, hg, util, error from mercurial.lock import release def fetch(ui, repo, source='default', **opts): @@ -66,7 +66,7 @@ other = hg.repository(hg.remoteui(repo, opts), ui.expandpath(source)) ui.status(_('pulling from %s\n') % - url.hidepassword(ui.expandpath(source))) + util.hidepassword(ui.expandpath(source))) revs = None if opts['rev']: try: @@ -125,7 +125,7 @@ # we don't translate commit messages message = (cmdutil.logmessage(opts) or ('Automated merge with %s' % - url.removeauth(other.url()))) + util.removeauth(other.url()))) editor = cmdutil.commiteditor if opts.get('force_editor') or opts.get('edit'): editor = cmdutil.commitforceeditor diff -r 7b627923739f -r 680c3c6fcb48 hgext/gpg.py --- a/hgext/gpg.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/gpg.py Fri May 06 14:29:53 2011 +0200 @@ -234,7 +234,7 @@ # write it if opts['local']: - repo.opener("localsigs", "ab").write(sigmessage) + repo.opener.append("localsigs", sigmessage) return msigs = match.exact(repo.root, '', ['.hgsigs']) diff -r 7b627923739f -r 680c3c6fcb48 hgext/graphlog.py --- a/hgext/graphlog.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/graphlog.py Fri May 06 14:29:53 2011 +0200 @@ -12,7 +12,6 @@ revision graph is also shown. ''' -import os from mercurial.cmdutil import revrange, show_changeset from mercurial.commands import templateopts from mercurial.i18n import _ @@ -22,7 +21,7 @@ ASCIIDATA = 'ASC' -def asciiedges(seen, rev, parents): +def asciiedges(type, char, lines, seen, rev, parents): """adds edge info to changelog DAG walk suitable for ascii()""" if rev not in seen: seen.append(rev) @@ -37,16 +36,33 @@ newparents.append(parent) ncols = len(seen) - seen[nodeidx:nodeidx + 1] = newparents - edges = [(nodeidx, seen.index(p)) for p in knownparents] + nextseen = seen[:] + nextseen[nodeidx:nodeidx + 1] = newparents + edges = [(nodeidx, nextseen.index(p)) for p in knownparents] + + while len(newparents) > 2: + # ascii() only knows how to add or remove a single column between two + # calls. Nodes with more than two parents break this constraint so we + # introduce intermediate expansion lines to grow the active node list + # slowly. + edges.append((nodeidx, nodeidx)) + edges.append((nodeidx, nodeidx + 1)) + nmorecols = 1 + yield (type, char, lines, (nodeidx, edges, ncols, nmorecols)) + char = '\\' + lines = [] + nodeidx += 1 + ncols += 1 + edges = [] + del newparents[0] if len(newparents) > 0: edges.append((nodeidx, nodeidx)) if len(newparents) > 1: edges.append((nodeidx, nodeidx + 1)) - - nmorecols = len(seen) - ncols - return nodeidx, edges, ncols, nmorecols + nmorecols = len(nextseen) - ncols + seen[:] = nextseen + yield (type, char, lines, (nodeidx, edges, ncols, nmorecols)) def fix_long_right_edges(edges): for (i, (start, end)) in enumerate(edges): @@ -215,13 +231,64 @@ else: return (len(repo) - 1, 0) -def check_unsupported_flags(opts): - for op in ["follow", "follow_first", "date", "copies", "keyword", "remove", - "only_merges", "user", "branch", "only_branch", "prune", - "newest_first", "no_merges", "include", "exclude"]: +def check_unsupported_flags(pats, opts): + for op in ["follow_first", "copies", "newest_first"]: if op in opts and opts[op]: - raise util.Abort(_("--graph option is incompatible with --%s") + raise util.Abort(_("-G/--graph option is incompatible with --%s") % op.replace("_", "-")) + if pats and opts.get('follow'): + raise util.Abort(_("-G/--graph option is incompatible with --follow " + "with file argument")) + +def revset(pats, opts): + """Return revset str built of revisions, log options and file patterns. + """ + opt2revset = { + 'follow': (0, 'follow()'), + 'no_merges': (0, 'not merge()'), + 'only_merges': (0, 'merge()'), + 'removed': (0, 'removes("*")'), + 'date': (1, 'date($)'), + 'branch': (2, 'branch($)'), + 'exclude': (2, 'not file($)'), + 'include': (2, 'file($)'), + 'keyword': (2, 'keyword($)'), + 'only_branch': (2, 'branch($)'), + 'prune': (2, 'not ($ or ancestors($))'), + 'user': (2, 'user($)'), + } + optrevset = [] + revset = [] + for op, val in opts.iteritems(): + if not val: + continue + if op == 'rev': + # Already a revset + revset.extend(val) + if op not in opt2revset: + continue + arity, revop = opt2revset[op] + revop = revop.replace('$', '%(val)r') + if arity == 0: + optrevset.append(revop) + elif arity == 1: + optrevset.append(revop % {'val': val}) + else: + for f in val: + optrevset.append(revop % {'val': f}) + + for path in pats: + optrevset.append('file(%r)' % path) + + if revset or optrevset: + if revset: + revset = ['(' + ' or '.join(revset) + ')'] + if optrevset: + revset.append('(' + ' and '.join(optrevset) + ')') + revset = ' and '.join(revset) + else: + revset = 'all()' + return revset def generate(ui, dag, displayer, showparents, edgefn): seen, state = [], asciistate() @@ -230,10 +297,12 @@ displayer.show(ctx) lines = displayer.hunk.pop(rev).split('\n')[:-1] displayer.flush(rev) - ascii(ui, state, type, char, lines, edgefn(seen, rev, parents)) + edges = edgefn(type, char, lines, seen, rev, parents) + for type, char, lines, coldata in edges: + ascii(ui, state, type, char, lines, coldata) displayer.close() -def graphlog(ui, repo, path=None, **opts): +def graphlog(ui, repo, *pats, **opts): """show revision history alongside an ASCII revision graph Print a revision history alongside a revision graph drawn with @@ -243,20 +312,13 @@ directory. """ - check_unsupported_flags(opts) - limit = cmdutil.loglimit(opts) - start, stop = get_revs(repo, opts["rev"]) - if start == nullrev: - return + check_unsupported_flags(pats, opts) - if path: - path = util.canonpath(repo.root, os.getcwd(), path) - if path: # could be reset in canonpath - revdag = graphmod.filerevs(repo, path, start, stop, limit) - else: - if limit is not None: - stop = max(stop, start - limit + 1) - revdag = graphmod.revisions(repo, start, stop) + revs = sorted(revrange(repo, [revset(pats, opts)]), reverse=1) + limit = cmdutil.loglimit(opts) + if limit is not None: + revs = revs[:limit] + revdag = graphmod.dagwalker(repo, revs) displayer = show_changeset(ui, repo, opts, buffered=True) showparents = [ctx.node() for ctx in repo[None].parents()] @@ -279,7 +341,7 @@ directory. """ - check_unsupported_flags(opts) + check_unsupported_flags([], opts) o = hg._outgoing(ui, repo, dest, opts) if o is None: return @@ -301,7 +363,7 @@ def subreporecurse(): return 1 - check_unsupported_flags(opts) + check_unsupported_flags([], opts) def display(other, chlist, displayer): revdag = graphrevs(other, chlist, opts) showparents = [ctx.node() for ctx in repo[None].parents()] @@ -319,11 +381,7 @@ '''wrap the command''' def graph(orig, *args, **kwargs): if kwargs['graph']: - try: - return wrapfn(*args, **kwargs) - except TypeError, e: - if len(args) > wrapfn.func_code.co_argcount: - raise util.Abort(_('--graph option allows at most one file')) + return wrapfn(*args, **kwargs) return orig(*args, **kwargs) entry = extensions.wrapcommand(table, cmd, graph) entry[1].append(('G', 'graph', None, _("show the revision DAG"))) diff -r 7b627923739f -r 680c3c6fcb48 hgext/hgcia.py --- a/hgext/hgcia.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/hgcia.py Fri May 06 14:29:53 2011 +0200 @@ -75,7 +75,7 @@ def fileelems(self): n = self.ctx.node() - f = self.cia.repo.status(self.ctx.parents()[0].node(), n) + f = self.cia.repo.status(self.ctx.p1().node(), n) url = self.url or '' elems = [] for path in f[0]: diff -r 7b627923739f -r 680c3c6fcb48 hgext/inotify/linuxserver.py --- a/hgext/inotify/linuxserver.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/inotify/linuxserver.py Fri May 06 14:29:53 2011 +0200 @@ -44,7 +44,7 @@ def _explain_watch_limit(ui, dirstate, rootabs): path = '/proc/sys/fs/inotify/max_user_watches' try: - limit = int(file(path).read()) + limit = int(util.readfile(path)) except IOError, err: if err.errno != errno.ENOENT: raise diff -r 7b627923739f -r 680c3c6fcb48 hgext/keyword.py --- a/hgext/keyword.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/keyword.py Fri May 06 14:29:53 2011 +0200 @@ -60,11 +60,11 @@ control run :hg:`kwdemo`. See :hg:`help templates` for a list of available templates and filters. -Three additional date template filters are provided:: +Three additional date template filters are provided: - utcdate "2006/09/18 15:13:13" - svnutcdate "2006-09-18 15:13:13Z" - svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" +:``utcdate``: "2006/09/18 15:13:13" +:``svnutcdate``: "2006-09-18 15:13:13Z" +:``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" The default template mappings (view with :hg:`kwdemo -d`) can be replaced with customized keywords and templates. Again, run @@ -83,6 +83,7 @@ from mercurial import commands, context, cmdutil, dispatch, filelog, extensions from mercurial import localrepo, match, patch, templatefilters, templater, util +from mercurial import scmutil from mercurial.hgweb import webcommands from mercurial.i18n import _ import os, re, shutil, tempfile @@ -109,11 +110,26 @@ } # date like in cvs' $Date -utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S') +def utcdate(text): + ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13". + ''' + return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S') # date like in svn's $Date -svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') +def svnisodate(text): + ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13 + +0200 (Tue, 18 Aug 2009)". + ''' + return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') # date like in svn's $Id -svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ') +def svnutcdate(text): + ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18 + 11:00:13Z". + ''' + return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ') + +templatefilters.filters.update({'utcdate': utcdate, + 'svnisodate': svnisodate, + 'svnutcdate': svnutcdate}) # make keyword tools accessible kwtools = {'templater': None, 'hgcmd': ''} @@ -176,9 +192,6 @@ for k, v in kwmaps) else: self.templates = _defaultkwmaps(self.ui) - templatefilters.filters.update({'utcdate': utcdate, - 'svnisodate': svnisodate, - 'svnutcdate': svnutcdate}) @util.propertycache def escape(self): @@ -400,7 +413,7 @@ demoitems('keywordset', ui.configitems('keywordset')) demoitems('keywordmaps', kwmaps.iteritems()) keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n' - repo.wopener(fn, 'w').write(keywords) + repo.wopener.write(fn, keywords) repo[None].add([fn]) ui.note(_('\nkeywords written to %s:\n') % fn) ui.note(keywords) @@ -607,8 +620,8 @@ expansion. ''' source = repo.dirstate.copied(dest) if 'l' in wctx.flags(source): - source = util.canonpath(repo.root, cwd, - os.path.realpath(source)) + source = scmutil.canonpath(repo.root, cwd, + os.path.realpath(source)) return kwt.match(source) candidates = [f for f in repo.dirstate.copies() if diff -r 7b627923739f -r 680c3c6fcb48 hgext/mq.py --- a/hgext/mq.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/mq.py Fri May 06 14:29:53 2011 +0200 @@ -45,7 +45,7 @@ from mercurial.i18n import _ from mercurial.node import bin, hex, short, nullid, nullrev from mercurial.lock import release -from mercurial import commands, cmdutil, hg, patch, util +from mercurial import commands, cmdutil, hg, patch, scmutil, util from mercurial import repair, extensions, url, error import os, sys, re, errno, shutil @@ -259,7 +259,7 @@ except IOError: curpath = os.path.join(path, 'patches') self.path = patchdir or curpath - self.opener = util.opener(self.path) + self.opener = scmutil.opener(self.path) self.ui = ui self.applied_dirty = 0 self.series_dirty = 0 @@ -291,14 +291,14 @@ elif l.strip(): self.ui.warn(_('malformated mq status line: %s\n') % entry) # else we ignore empty lines - lines = self.opener(self.status_path).read().splitlines() + lines = self.opener.read(self.status_path).splitlines() return list(parselines(lines)) return [] @util.propertycache def full_series(self): if os.path.exists(self.join(self.series_path)): - return self.opener(self.series_path).read().splitlines() + return self.opener.read(self.series_path).splitlines() return [] @util.propertycache @@ -412,7 +412,7 @@ if self.active_guards is None: self.active_guards = [] try: - guards = self.opener(self.guards_path).read().split() + guards = self.opener.read(self.guards_path).split() except IOError, err: if err.errno != errno.ENOENT: raise @@ -737,11 +737,29 @@ os.unlink(self.join(p)) if numrevs: + qfinished = self.applied[:numrevs] del self.applied[:numrevs] self.applied_dirty = 1 - for i in sorted([self.find_series(p) for p in patches], reverse=True): - del self.full_series[i] + unknown = [] + + for (i, p) in sorted([(self.find_series(p), p) for p in patches], + reverse=True): + if i is not None: + del self.full_series[i] + else: + unknown.append(p) + + if unknown: + if numrevs: + rev = dict((entry.name, entry.node) for entry in qfinished) + for p in unknown: + msg = _('revision %s refers to unknown patches: %s\n') + self.ui.warn(msg % (short(rev[p]), p)) + else: + msg = _('unknown patches: %s\n') + raise util.Abort(''.join(msg % p for p in unknown)) + self.parse_series() self.series_dirty = 1 @@ -836,10 +854,18 @@ _reserved = ('series', 'status', 'guards', '.', '..') def check_reserved_name(self, name): - if (name in self._reserved or name.startswith('.hg') - or name.startswith('.mq') or '#' in name or ':' in name): + if name in self._reserved: raise util.Abort(_('"%s" cannot be used as the name of a patch') % name) + for prefix in ('.hg', '.mq'): + if name.startswith(prefix): + raise util.Abort(_('patch name cannot begin with "%s"') + % prefix) + for c in ('#', ':'): + if c in name: + raise util.Abort(_('"%s" cannot be used in the name of a patch') + % c) + def new(self, repo, patchfn, *pats, **opts): """options: @@ -899,7 +925,7 @@ else: p.write("# HG changeset patch\n") p.write("# Parent " - + hex(repo[None].parents()[0].node()) + "\n") + + hex(repo[None].p1().node()) + "\n") if user: p.write("# User " + user + "\n") if date: @@ -1054,7 +1080,7 @@ heads += ls if not heads: heads = [nullid] - if repo.dirstate.parents()[0] not in heads and not exact: + if repo.dirstate.p1() not in heads and not exact: self.ui.status(_("(working directory not at a head)\n")) if not self.series: @@ -1101,7 +1127,7 @@ self.ui.warn(_('patch series already fully applied\n')) return 1 if not force: - self.check_localchanges(repo) + self.check_localchanges(repo, refresh=self.applied) if exact: if move: @@ -1148,7 +1174,7 @@ ret = self.apply(repo, s, list, all_files=all_files) except: self.ui.warn(_('cleaning up working directory...')) - node = repo.dirstate.parents()[0] + node = repo.dirstate.p1() hg.revert(repo, node, None) # only remove unknown files that we know we touched or # created while patching @@ -1899,7 +1925,7 @@ With -g/--git, patches imported with --rev will use the git diff format. See the diffs help topic for information on why this is important for preserving rename/copy information and permission - changes. + changes. Use :hg:`qfinish` to remove changesets from mq control. To import a patch from standard input, pass - as the patch file. When importing from standard input, a patch name must be specified @@ -2537,8 +2563,9 @@ """strip changesets and all their descendants from the repository The strip command removes the specified changesets and all their - descendants. If the working directory has uncommitted changes, - the operation is aborted unless the --force flag is supplied. + descendants. If the working directory has uncommitted changes, the + operation is aborted unless the --force flag is supplied, in which + case changes will be discarded. If a parent of the working directory is stripped, then the working directory will automatically be updated to the most recent @@ -2960,7 +2987,7 @@ mqtags = [(patch.node, patch.name) for patch in q.applied] try: - r = self.changelog.rev(mqtags[-1][0]) + self.changelog.rev(mqtags[-1][0]) except error.RepoLookupError: self.ui.warn(_('mq status file refers to unknown node %s\n') % short(mqtags[-1][0])) @@ -3234,8 +3261,8 @@ _('hg qseries [-ms]')), "strip": (strip, - [('f', 'force', None, _('force removal of changesets even if the ' - 'working directory has uncommitted changes')), + [('f', 'force', None, _('force removal of changesets, discard ' + 'uncommitted changes (no backup)')), ('b', 'backup', None, _('bundle only changesets with local revision' ' number greater than REV which are not' ' descendants of REV (DEPRECATED)')), diff -r 7b627923739f -r 680c3c6fcb48 hgext/notify.py --- a/hgext/notify.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/notify.py Fri May 06 14:29:53 2011 +0200 @@ -249,7 +249,7 @@ def diff(self, ctx, ref=None): maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) - prev = ctx.parents()[0].node() + prev = ctx.p1().node() ref = ref and ref.node() or ctx.node() chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui)) difflines = ''.join(chunks).splitlines() diff -r 7b627923739f -r 680c3c6fcb48 hgext/parentrevspec.py --- a/hgext/parentrevspec.py Thu May 05 16:01:09 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +0,0 @@ -# Mercurial extension to make it easy to refer to the parent of a revision -# -# Copyright (C) 2007 Alexis S. L. Carvalho -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -'''interpret suffixes to refer to ancestor revisions - -This extension allows you to use git-style suffixes to refer to the -ancestors of a specific revision. - -For example, if you can refer to a revision as "foo", then:: - - foo^N = Nth parent of foo - foo^0 = foo - foo^1 = first parent of foo - foo^2 = second parent of foo - foo^ = foo^1 - - foo~N = Nth first grandparent of foo - foo~0 = foo - foo~1 = foo^1 = foo^ = first parent of foo - foo~2 = foo^1^1 = foo^^ = first parent of first parent of foo -''' -from mercurial import error - -def reposetup(ui, repo): - if not repo.local(): - return - - class parentrevspecrepo(repo.__class__): - def lookup(self, key): - try: - _super = super(parentrevspecrepo, self) - return _super.lookup(key) - except error.RepoError: - pass - - circ = key.find('^') - tilde = key.find('~') - if circ < 0 and tilde < 0: - raise - elif circ >= 0 and tilde >= 0: - end = min(circ, tilde) - else: - end = max(circ, tilde) - - cl = self.changelog - base = key[:end] - try: - node = _super.lookup(base) - except error.RepoError: - # eek - reraise the first error - return _super.lookup(key) - - rev = cl.rev(node) - suffix = key[end:] - i = 0 - while i < len(suffix): - # foo^N => Nth parent of foo - # foo^0 == foo - # foo^1 == foo^ == 1st parent of foo - # foo^2 == 2nd parent of foo - if suffix[i] == '^': - j = i + 1 - p = cl.parentrevs(rev) - if j < len(suffix) and suffix[j].isdigit(): - j += 1 - n = int(suffix[i + 1:j]) - if n > 2 or n == 2 and p[1] == -1: - raise - else: - n = 1 - if n: - rev = p[n - 1] - i = j - # foo~N => Nth first grandparent of foo - # foo~0 = foo - # foo~1 = foo^1 == foo^ == 1st parent of foo - # foo~2 = foo^1^1 == foo^^ == 1st parent of 1st parent of foo - elif suffix[i] == '~': - j = i + 1 - while j < len(suffix) and suffix[j].isdigit(): - j += 1 - if j == i + 1: - raise - n = int(suffix[i + 1:j]) - for k in xrange(n): - rev = cl.parentrevs(rev)[0] - i = j - else: - raise - return cl.node(rev) - - repo.__class__ = parentrevspecrepo diff -r 7b627923739f -r 680c3c6fcb48 hgext/patchbomb.py --- a/hgext/patchbomb.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/patchbomb.py Fri May 06 14:29:53 2011 +0200 @@ -48,7 +48,7 @@ import os, errno, socket, tempfile, cStringIO, time import email.MIMEMultipart, email.MIMEBase import email.Utils, email.Encoders, email.Generator -from mercurial import cmdutil, commands, hg, mail, patch, util, discovery, url +from mercurial import cmdutil, commands, hg, mail, patch, util, discovery from mercurial.i18n import _ from mercurial.node import bin @@ -238,15 +238,14 @@ dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest) revs, checkout = hg.addbranchrevs(repo, repo, branches, revs) - if revs: - revs = [repo.lookup(rev) for rev in revs] other = hg.repository(hg.remoteui(repo, opts), dest) - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) - o = discovery.findoutgoing(repo, other) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) + common, _anyinc, _heads = discovery.findcommonincoming(repo, other) + nodes = revs and map(repo.lookup, revs) or revs + o = repo.changelog.findmissing(common, heads=nodes) if not o: ui.status(_("no changes found\n")) return [] - o = repo.changelog.nodesbetween(o, revs)[0] return [str(repo.changelog.rev(r)) for r in o] def getpatches(revs): diff -r 7b627923739f -r 680c3c6fcb48 hgext/progress.py --- a/hgext/progress.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/progress.py Fri May 06 14:29:53 2011 +0200 @@ -47,7 +47,6 @@ import time from mercurial.i18n import _ -from mercurial import util def spacejoin(*args): return ' '.join(s for s in args if s) @@ -239,7 +238,7 @@ self.topicstates[topic] = pos, item, unit, total if now - self.lastprint >= self.refresh and self.topics: self.lastprint = now - current = self.topics[-1] + self.topics[-1] self.show(now, topic, *self.topicstates[topic]) def uisetup(ui): diff -r 7b627923739f -r 680c3c6fcb48 hgext/rebase.py --- a/hgext/rebase.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/rebase.py Fri May 06 14:29:53 2011 +0200 @@ -15,7 +15,7 @@ ''' from mercurial import hg, util, repair, merge, cmdutil, commands -from mercurial import extensions, ancestor, copies, patch +from mercurial import extensions, copies, patch from mercurial.commands import templateopts from mercurial.node import nullrev from mercurial.lock import release @@ -90,7 +90,8 @@ contf = opts.get('continue') abortf = opts.get('abort') collapsef = opts.get('collapse', False) - extrafn = opts.get('extrafn') + collapsemsg = cmdutil.logmessage(opts) + extrafn = opts.get('extrafn') # internal, used by e.g. hgsubversion keepf = opts.get('keep', False) keepbranchesf = opts.get('keepbranches', False) detachf = opts.get('detach', False) @@ -98,6 +99,10 @@ # other extensions keepopen = opts.get('keepopen', False) + if collapsemsg and not collapsef: + raise util.Abort( + _('message can only be specified with collapse')) + if contf or abortf: if contf and abortf: raise util.Abort(_('cannot use both abort and continue')) @@ -109,6 +114,8 @@ if srcf or basef or destf: raise util.Abort( _('abort and continue do not allow specifying revisions')) + if opts.get('tool', False): + ui.warn(_('tool option will be ignored\n')) (originalwd, target, state, skipped, collapsef, keepf, keepbranchesf, external) = restorestatus(repo) @@ -138,8 +145,7 @@ external = checkexternal(repo, state, targetancestors) if keepbranchesf: - if extrafn: - raise util.Abort(_('cannot use both keepbranches and extrafn')) + assert not extrafn, 'cannot use both keepbranches and extrafn' def extrafn(ctx, extra): extra['branch'] = ctx.branch() @@ -163,10 +169,14 @@ if len(repo.parents()) == 2: repo.ui.debug('resuming interrupted rebase\n') else: - stats = rebasenode(repo, rev, p1, p2, state) - if stats and stats[3] > 0: - raise util.Abort(_('unresolved conflicts (see hg ' - 'resolve, then hg rebase --continue)')) + try: + ui.setconfig('ui', 'forcemerge', opts.get('tool', '')) + stats = rebasenode(repo, rev, p1, state) + if stats and stats[3] > 0: + raise util.Abort(_('unresolved conflicts (see hg ' + 'resolve, then hg rebase --continue)')) + finally: + ui.setconfig('ui', 'forcemerge', '') updatedirstate(repo, rev, target, p2) if not collapsef: newrev = concludenode(repo, rev, p1, p2, extrafn=extrafn) @@ -190,11 +200,14 @@ if collapsef and not keepopen: p1, p2 = defineparents(repo, min(state), target, state, targetancestors) - commitmsg = 'Collapsed revision' - for rebased in state: - if rebased not in skipped and state[rebased] != nullmerge: - commitmsg += '\n* %s' % repo[rebased].description() - commitmsg = ui.edit(commitmsg, repo.ui.username()) + if collapsemsg: + commitmsg = collapsemsg + else: + commitmsg = 'Collapsed revision' + for rebased in state: + if rebased not in skipped and state[rebased] != nullmerge: + commitmsg += '\n* %s' % repo[rebased].description() + commitmsg = ui.edit(commitmsg, repo.ui.username()) newrev = concludenode(repo, rev, p1, external, commitmsg=commitmsg, extrafn=extrafn) @@ -221,25 +234,6 @@ finally: release(lock, wlock) -def rebasemerge(repo, rev, first=False): - 'return the correct ancestor' - oldancestor = ancestor.ancestor - - def newancestor(a, b, pfunc): - if b == rev: - return repo[rev].parents()[0].rev() - return oldancestor(a, b, pfunc) - - if not first: - ancestor.ancestor = newancestor - else: - repo.ui.debug("first revision, do not change ancestor\n") - try: - stats = merge.update(repo, rev, True, True, False) - return stats - finally: - ancestor.ancestor = oldancestor - def checkexternal(repo, state, targetancestors): """Check whether one or more external revisions need to be taken in consideration. In the latter case, abort. @@ -293,7 +287,7 @@ repo.dirstate.invalidate() raise -def rebasenode(repo, rev, p1, p2, state): +def rebasenode(repo, rev, p1, state): 'Rebase a single revision' # Merge phase # Update to target and merge it with local @@ -304,9 +298,10 @@ repo.ui.debug(" already in target\n") repo.dirstate.write() repo.ui.debug(" merge against %d:%s\n" % (repo[rev].rev(), repo[rev])) - first = repo[rev].rev() == repo[min(state)].rev() - stats = rebasemerge(repo, rev, first) - return stats + base = None + if repo[rev].rev() != repo[min(state)].rev(): + base = repo[rev].p1().node() + return merge.update(repo, rev, True, True, False, base) def defineparents(repo, rev, target, state, targetancestors): 'Return the new parent relationship of the revision that will be rebased' @@ -354,6 +349,8 @@ 'Update rebased mq patches - finalize and then import them' mqrebase = {} mq = repo.mq + original_series = mq.full_series[:] + for p in mq.applied: rev = repo[p.node].rev() if rev in state: @@ -371,6 +368,15 @@ repo.ui.debug('import mq patch %d (%s)\n' % (state[rev], name)) mq.qimport(repo, (), patchname=name, git=isgit, rev=[str(state[rev])]) + + # Restore missing guards + for s in original_series: + pname = mq.guard_re.split(s, 1)[0] + if pname in mq.full_series: + repo.ui.debug('restoring guard for patch %s' % (pname)) + mq.full_series.remove(pname) + mq.full_series.append(s) + mq.series_dirty = True mq.save_dirty() def storestatus(repo, originalwd, target, state, collapse, keep, keepbranches, @@ -475,9 +481,10 @@ if src: commonbase = repo[src].ancestor(repo[dest]) + samebranch = repo[src].branch() == repo[dest].branch() if commonbase == repo[src]: raise util.Abort(_('source is ancestor of destination')) - if commonbase == repo[dest]: + if samebranch and commonbase == repo[dest]: raise util.Abort(_('source is descendant of destination')) source = repo[src].rev() if detach: @@ -565,10 +572,15 @@ ('d', 'dest', '', _('rebase onto the specified changeset'), _('REV')), ('', 'collapse', False, _('collapse the rebased changesets')), + ('m', 'message', '', + _('use text as collapse commit message'), _('TEXT')), + ('l', 'logfile', '', + _('read collapse commit message from file'), _('FILE')), ('', 'keep', False, _('keep original changesets')), ('', 'keepbranches', False, _('keep original branch names')), ('', 'detach', False, _('force detaching of source from its original ' 'branch')), + ('t', 'tool', '', _('specify merge tool')), ('c', 'continue', False, _('continue an interrupted rebase')), ('a', 'abort', False, _('abort an interrupted rebase'))] + templateopts, diff -r 7b627923739f -r 680c3c6fcb48 hgext/record.py --- a/hgext/record.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/record.py Fri May 06 14:29:53 2011 +0200 @@ -324,10 +324,12 @@ for i, chunk in enumerate(h.hunks): if skipfile is None and skipall is None: chunk.pretty(ui) - msg = (total == 1 - and (_('record this change to %r?') % chunk.filename()) - or (_('record change %d/%d to %r?') % - (pos - len(h.hunks) + i, total, chunk.filename()))) + if total == 1: + msg = _('record this change to %r?') % chunk.filename() + else: + idx = pos - len(h.hunks) + i + msg = _('record change %d/%d to %r?') % (idx, total, + chunk.filename()) r, skipfile, skipall = prompt(skipfile, skipall, msg) if r: if fixoffset: @@ -467,7 +469,7 @@ # 3a. apply filtered patch to clean repo (clean) if backups: - hg.revert(repo, repo.dirstate.parents()[0], + hg.revert(repo, repo.dirstate.p1(), lambda key: key in backups) # 3b. (apply) @@ -533,6 +535,9 @@ "record": (record, commands.table['^commit|ci'][1], # same options as commit _('hg record [OPTION]... [FILE]...')), + "qrecord": + (qrecord, {}, # placeholder until mq is available + _('hg qrecord [OPTION]... PATCH [FILE]...')), } diff -r 7b627923739f -r 680c3c6fcb48 hgext/relink.py --- a/hgext/relink.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/relink.py Fri May 06 14:29:53 2011 +0200 @@ -38,9 +38,9 @@ """ if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'): raise util.Abort(_('hardlinks are not supported on this system')) - src = hg.repository( - hg.remoteui(repo, opts), - ui.expandpath(origin or 'default-relink', origin or 'default')) + src = hg.repository(hg.remoteui(repo, opts), + ui.expandpath(origin or 'default-relink', + origin or 'default')) if not src.local(): raise util.Abort(_('must specify local origin repository')) ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path)) @@ -172,8 +172,8 @@ ui.progress(_('relinking'), None) - ui.status(_('relinked %d files (%d bytes reclaimed)\n') % - (relinked, savedbytes)) + ui.status(_('relinked %d files (%s reclaimed)\n') % + (relinked, util.bytecount(savedbytes))) cmdtable = { 'relink': ( diff -r 7b627923739f -r 680c3c6fcb48 hgext/schemes.py --- a/hgext/schemes.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/schemes.py Fri May 06 14:29:53 2011 +0200 @@ -40,8 +40,9 @@ same name. """ -import re -from mercurial import hg, templater +import os, re +from mercurial import extensions, hg, templater, util +from mercurial.i18n import _ class ShortRepository(object): @@ -58,6 +59,7 @@ return '' % self.scheme def instance(self, ui, url, create): + # Should this use urlmod.url(), or is manual parsing better? url = url.split('://', 1)[1] parts = url.split('/', self.parts) if len(parts) > self.parts: @@ -69,6 +71,12 @@ url = ''.join(self.templater.process(self.url, context)) + tail return hg._lookup(url).instance(ui, url, create) +def hasdriveletter(orig, path): + for scheme in schemes: + if path.startswith(scheme + ':'): + return False + return orig(path) + schemes = { 'py': 'http://hg.python.org/', 'bb': 'https://bitbucket.org/', @@ -81,4 +89,10 @@ schemes.update(dict(ui.configitems('schemes'))) t = templater.engine(lambda x: x) for scheme, url in schemes.items(): + if (os.name == 'nt' and len(scheme) == 1 and scheme.isalpha() + and os.path.exists('%s:\\' % scheme)): + raise util.Abort(_('custom scheme %s:// conflicts with drive ' + 'letter %s:\\\n') % (scheme, scheme.upper())) hg.schemes[scheme] = ShortRepository(url, scheme, t) + + extensions.wrapfunction(util, 'hasdriveletter', hasdriveletter) diff -r 7b627923739f -r 680c3c6fcb48 hgext/transplant.py --- a/hgext/transplant.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/transplant.py Fri May 06 14:29:53 2011 +0200 @@ -16,8 +16,8 @@ from mercurial.i18n import _ import os, tempfile from mercurial import bundlerepo, cmdutil, hg, merge, match -from mercurial import patch, revlog, util, error -from mercurial import revset +from mercurial import patch, revlog, scmutil, util, error +from mercurial import revset, templatekw class transplantentry(object): def __init__(self, lnode, rnode): @@ -31,7 +31,7 @@ self.opener = opener if not opener: - self.opener = util.opener(self.path) + self.opener = scmutil.opener(self.path) self.transplants = {} self.dirty = False self.read() @@ -39,7 +39,7 @@ def read(self): abspath = os.path.join(self.path, self.transplantfile) if self.transplantfile and os.path.exists(abspath): - for line in self.opener(self.transplantfile).read().splitlines(): + for line in self.opener.read(self.transplantfile).splitlines(): lnode, rnode = map(revlog.bin, line.split(':')) list = self.transplants.setdefault(rnode, []) list.append(transplantentry(lnode, rnode)) @@ -74,7 +74,7 @@ def __init__(self, ui, repo): self.ui = ui self.path = repo.join('transplant') - self.opener = util.opener(self.path) + self.opener = scmutil.opener(self.path) self.transplants = transplants(self.path, 'transplants', opener=self.opener) @@ -177,12 +177,11 @@ lock.release() wlock.release() - def filter(self, filter, changelog, patchfile): + def filter(self, filter, node, changelog, patchfile): '''arbitrarily rewrite changeset before applying it''' self.ui.status(_('filtering %s\n') % patchfile) user, date, msg = (changelog[1], changelog[2], changelog[4]) - fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-') fp = os.fdopen(fd, 'w') fp.write("# HG changeset patch\n") @@ -194,7 +193,9 @@ try: util.system('%s %s %s' % (filter, util.shellquote(headerfile), util.shellquote(patchfile)), - environ={'HGUSER': changelog[1]}, + environ={'HGUSER': changelog[1], + 'HGREVISION': revlog.hex(node), + }, onerr=util.Abort, errprefix=_('filter failed')) user, date, msg = self.parselog(file(headerfile))[1:4] finally: @@ -209,7 +210,7 @@ date = "%d %d" % (time, timezone) extra = {'transplant_source': node} if filter: - (user, date, message) = self.filter(filter, cl, patchfile) + (user, date, message) = self.filter(filter, node, cl, patchfile) if log: # we don't translate messages inserted into commits @@ -236,7 +237,7 @@ seriespath = os.path.join(self.path, 'series') if os.path.exists(seriespath): os.unlink(seriespath) - p1 = repo.dirstate.parents()[0] + p1 = repo.dirstate.p1() p2 = node self.log(user, date, message, p1, p2, merge=merge) self.ui.write(str(inst) + '\n') @@ -317,7 +318,7 @@ nodes = [] merges = [] cur = nodes - for line in self.opener('series').read().splitlines(): + for line in self.opener.read('series').splitlines(): if line.startswith('# Merges'): cur = merges continue @@ -345,6 +346,8 @@ message = [] node = revlog.nullid inmsg = False + user = None + date = None for line in fp.read().splitlines(): if inmsg: message.append(line) @@ -359,6 +362,8 @@ elif not line.startswith('# '): inmsg = True message.append(line) + if None in (user, date): + raise util.Abort(_("filter corrupted changeset (no user or date)")) return (node, user, date, '\n'.join(message), parents) def log(self, user, date, message, p1, p2, merge=False): @@ -489,10 +494,8 @@ and then resume where you left off by calling :hg:`transplant --continue/-c`. ''' - def incwalk(repo, incoming, branches, match=util.always): - if not branches: - branches = None - for node in repo.changelog.nodesbetween(incoming, branches)[0]: + def incwalk(repo, csets, match=util.always): + for node in csets: if match(node): yield node @@ -542,15 +545,16 @@ if m or a or r or d: raise util.Abort(_('outstanding local changes')) - bundle = None - source = opts.get('source') - if source: - sourcerepo = ui.expandpath(source) - source = hg.repository(ui, sourcerepo) - source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source, - force=True) + sourcerepo = opts.get('source') + if sourcerepo: + source = hg.repository(ui, ui.expandpath(sourcerepo)) + branches = map(source.lookup, opts.get('branch', ())) + source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, source, + onlyheads=branches, force=True) else: source = repo + branches = map(source.lookup, opts.get('branch', ())) + cleanupfn = None try: if opts.get('continue'): @@ -564,7 +568,6 @@ matchfn = lambda x: tf(x) and x not in prune else: matchfn = tf - branches = map(source.lookup, opts.get('branch', ())) merges = map(source.lookup, opts.get('merge', ())) revmap = {} if revs: @@ -572,8 +575,7 @@ revmap[int(r)] = source.lookup(r) elif opts.get('all') or not merges: if source != repo: - alltransplants = incwalk(source, incoming, branches, - match=matchfn) + alltransplants = incwalk(source, csets, match=matchfn) else: alltransplants = transplantwalk(source, p1, branches, match=matchfn) @@ -589,9 +591,8 @@ tp.apply(repo, source, revmap, merges, opts) finally: - if bundle: - source.close() - os.unlink(bundle) + if cleanupfn: + cleanupfn() def revsettransplanted(repo, subset, x): """``transplanted(set)`` @@ -607,8 +608,15 @@ cs.add(r) return [r for r in s if r in cs] +def kwtransplanted(repo, ctx, **args): + """:transplanted: String. The node identifier of the transplanted + changeset if any.""" + n = ctx.extra().get('transplant_source') + return n and revlog.hex(n) or '' + def extsetup(ui): revset.symbols['transplanted'] = revsettransplanted + templatekw.keywords['transplanted'] = kwtransplanted cmdtable = { "transplant": @@ -632,4 +640,4 @@ } # tell hggettext to extract docstrings from these functions: -i18nfunctions = [revsettransplanted] +i18nfunctions = [revsettransplanted, kwtransplanted] diff -r 7b627923739f -r 680c3c6fcb48 hgext/zeroconf/Zeroconf.py --- a/hgext/zeroconf/Zeroconf.py Thu May 05 16:01:09 2011 +0200 +++ b/hgext/zeroconf/Zeroconf.py Fri May 06 14:29:53 2011 +0200 @@ -237,14 +237,14 @@ """Class accessor""" try: return _CLASSES[clazz] - except: + except KeyError: return "?(%s)" % (clazz) def getType(self, type): """Type accessor""" try: return _TYPES[type] - except: + except KeyError: return "?(%s)" % (type) def toString(self, hdr, other): @@ -360,7 +360,7 @@ """String representation""" try: return socket.inet_ntoa(self.address) - except: + except Exception: return self.address class DNSHinfo(DNSRecord): @@ -790,7 +790,7 @@ """Adds an entry""" try: list = self.cache[entry.key] - except: + except KeyError: list = self.cache[entry.key] = [] list.append(entry) @@ -799,7 +799,7 @@ try: list = self.cache[entry.key] list.remove(entry) - except: + except KeyError: pass def get(self, entry): @@ -808,7 +808,7 @@ try: list = self.cache[entry.key] return list[list.index(entry)] - except: + except (KeyError, ValueError): return None def getByDetails(self, name, type, clazz): @@ -821,7 +821,7 @@ """Returns a list of entries whose key matches the name.""" try: return self.cache[name] - except: + except KeyError: return [] def entries(self): @@ -829,7 +829,7 @@ def add(x, y): return x+y try: return reduce(add, self.cache.values()) - except: + except Exception: return [] @@ -869,10 +869,10 @@ for socket in rr: try: self.readers[socket].handle_read() - except: + except Exception: if not globals()['_GLOBAL_DONE']: traceback.print_exc() - except: + except Exception: pass def getReaders(self): @@ -988,7 +988,7 @@ callback = lambda x: self.listener.removeService(x, self.type, record.alias) self.list.append(callback) return - except: + except Exception: if not expired: self.services[record.alias.lower()] = record callback = lambda x: self.listener.addService(x, self.type, record.alias) @@ -1117,7 +1117,7 @@ result[key] = value self.properties = result - except: + except Exception: traceback.print_exc() self.properties = None @@ -1255,7 +1255,7 @@ try: self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: + except Exception: # SO_REUSEADDR should be equivalent to SO_REUSEPORT for # multicast UDP sockets (p 731, "TCP/IP Illustrated, # Volume 2"), but some BSD-derived systems require @@ -1270,7 +1270,7 @@ self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1) try: self.socket.bind(self.group) - except: + except Exception: # Some versions of linux raise an exception even though # the SO_REUSE* options have been set, so ignore it # @@ -1370,7 +1370,7 @@ self.servicetypes[info.type]-=1 else: del self.servicetypes[info.type] - except: + except KeyError: pass now = currentTimeMillis() nextTime = now @@ -1455,7 +1455,7 @@ try: self.listeners.remove(listener) self.notifyAll() - except: + except Exception: pass def updateRecord(self, now, rec): @@ -1528,7 +1528,7 @@ out.addAnswer(msg, DNSText(question.name, _TYPE_TXT, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.text)) if question.type == _TYPE_SRV: out.addAdditionalAnswer(DNSAddress(service.server, _TYPE_A, _CLASS_IN | _CLASS_UNIQUE, _DNS_TTL, service.address)) - except: + except Exception: traceback.print_exc() if out is not None and out.answers: @@ -1541,7 +1541,7 @@ #temp = DNSIncoming(out.packet()) try: self.socket.sendto(out.packet(), 0, (addr, port)) - except: + except Exception: # Ignore this, it may be a temporary loss of network connection pass diff -r 7b627923739f -r 680c3c6fcb48 i18n/da.po --- a/i18n/da.po Thu May 05 16:01:09 2011 +0200 +++ b/i18n/da.po Fri May 06 14:29:53 2011 +0200 @@ -17,8 +17,8 @@ msgstr "" "Project-Id-Version: Mercurial\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2011-03-16 17:41+0100\n" -"PO-Revision-Date: 2011-03-16 17:42+0100\n" +"POT-Creation-Date: 2011-04-19 09:32+0200\n" +"PO-Revision-Date: 2011-04-19 09:45+0200\n" "Last-Translator: \n" "Language-Team: Danish\n" "Language: Danish\n" @@ -294,140 +294,231 @@ msgid "" "This hook extension adds comments on bugs in Bugzilla when changesets\n" -"that refer to bugs by Bugzilla ID are seen. The hook does not change\n" -"bug status." -msgstr "" - -msgid "" -"The hook updates the Bugzilla database directly. Only Bugzilla\n" -"installations using MySQL are supported." -msgstr "" - -msgid "" -"The hook relies on a Bugzilla script to send bug change notification\n" -"emails. That script changes between Bugzilla versions; the\n" -"'processmail' script used prior to 2.18 is replaced in 2.18 and\n" -"subsequent versions by 'config/sendbugmail.pl'. Note that these will\n" -"be run by Mercurial as the user pushing the change; you will need to\n" -"ensure the Bugzilla install file permissions are set appropriately." -msgstr "" - -msgid "" -"The extension is configured through three different configuration\n" -"sections. These keys are recognized in the [bugzilla] section:" -msgstr "" - -msgid "" -"host\n" -" Hostname of the MySQL server holding the Bugzilla database." -msgstr "" - -msgid "" -"db\n" -" Name of the Bugzilla database in MySQL. Default 'bugs'." -msgstr "" - -msgid "" -"user\n" -" Username to use to access MySQL server. Default 'bugs'." -msgstr "" - -msgid "" -"password\n" +"that refer to bugs by Bugzilla ID are seen. The comment is formatted using\n" +"the Mercurial template mechanism." +msgstr "" + +msgid "The hook does not change bug status." +msgstr "" + +msgid "Three basic modes of access to Bugzilla are provided:" +msgstr "" + +msgid "" +"1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later." +msgstr "" + +msgid "" +"2. Check data via the Bugzilla XMLRPC interface and submit bug change\n" +" via email to Bugzilla email interface. Requires Bugzilla 3.4 or later." +msgstr "" + +msgid "" +"3. Writing directly to the Bugzilla database. Only Bugzilla installations\n" +" using MySQL are supported. Requires Python MySQLdb." +msgstr "" + +msgid "" +"Writing directly to the database is susceptible to schema changes, and\n" +"relies on a Bugzilla contrib script to send out bug change\n" +"notification emails. This script runs as the user running Mercurial,\n" +"must be run on the host with the Bugzilla install, and requires\n" +"permission to read Bugzilla configuration details and the necessary\n" +"MySQL user and password to have full access rights to the Bugzilla\n" +"database. For these reasons this access mode is now considered\n" +"deprecated, and will not be updated for new Bugzilla versions going\n" +"forward." +msgstr "" + +msgid "" +"Access via XMLRPC needs a Bugzilla username and password to be specified\n" +"in the configuration. Comments are added under that username. Since the\n" +"configuration must be readable by all Mercurial users, it is recommended\n" +"that the rights of that user are restricted in Bugzilla to the minimum\n" +"necessary to add comments." +msgstr "" + +msgid "" +"Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends\n" +"email to the Bugzilla email interface to submit comments to bugs.\n" +"The From: address in the email is set to the email address of the Mercurial\n" +"user, so the comment appears to come from the Mercurial user. In the event\n" +"that the Mercurial user email is not recognised by Bugzilla as a Bugzilla\n" +"user, the email associated with the Bugzilla username used to log into\n" +"Bugzilla is used instead as the source of the comment." +msgstr "" + +msgid "Configuration items common to all access modes:" +msgstr "" + +msgid "" +"bugzilla.version\n" +" This access type to use. Values recognised are:" +msgstr "" + +msgid "" +" :``xmlrpc``: Bugzilla XMLRPC interface.\n" +" :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.\n" +" :``3.0``: MySQL access, Bugzilla 3.0 and later.\n" +" :``2.18``: MySQL access, Bugzilla 2.18 and up to but not\n" +" including 3.0.\n" +" :``2.16``: MySQL access, Bugzilla 2.16 and up to but not\n" +" including 2.18." +msgstr "" + +msgid "" +"bugzilla.regexp\n" +" Regular expression to match bug IDs in changeset commit message.\n" +" Must contain one \"()\" group. The default expression matches ``Bug\n" +" 1234``, ``Bug no. 1234``, ``Bug number 1234``, ``Bugs 1234,5678``,\n" +" ``Bug 1234 and 5678`` and variations thereof. Matching is case\n" +" insensitive." +msgstr "" + +msgid "" +"bugzilla.style\n" +" The style file to use when formatting comments." +msgstr "" + +msgid "" +"bugzilla.template\n" +" Template to use when formatting comments. Overrides style if\n" +" specified. In addition to the usual Mercurial keywords, the\n" +" extension specifies:" +msgstr "" + +msgid "" +" :``{bug}``: The Bugzilla bug ID.\n" +" :``{root}``: The full pathname of the Mercurial repository.\n" +" :``{webroot}``: Stripped pathname of the Mercurial repository.\n" +" :``{hgweb}``: Base URL for browsing Mercurial repositories." +msgstr "" + +msgid "" +" Default ``changeset {node|short} in repo {root} refers to bug\n" +" {bug}.\\ndetails:\\n\\t{desc|tabindent}``" +msgstr "" + +msgid "" +"bugzilla.strip\n" +" The number of path separator characters to strip from the front of\n" +" the Mercurial repository path (``{root}`` in templates) to produce\n" +" ``{webroot}``. For example, a repository with ``{root}``\n" +" ``/var/local/my-project`` with a strip of 2 gives a value for\n" +" ``{webroot}`` of ``my-project``. Default 0." +msgstr "" + +msgid "" +"web.baseurl\n" +" Base URL for browsing Mercurial repositories. Referenced from\n" +" templates as ``{hgweb}``." +msgstr "" + +msgid "Configuration items common to XMLRPC+email and MySQL access modes:" +msgstr "" + +msgid "" +"bugzilla.usermap\n" +" Path of file containing Mercurial committer email to Bugzilla user email\n" +" mappings. If specified, the file should contain one mapping per\n" +" line::" +msgstr "" + +msgid " committer = Bugzilla user" +msgstr "" + +msgid " See also the ``[usermap]`` section." +msgstr "" + +msgid "" +"The ``[usermap]`` section is used to specify mappings of Mercurial\n" +"committer email to Bugzilla user email. See also ``bugzilla.usermap``.\n" +"Contains entries of the form ``committer = Bugzilla user``." +msgstr "" + +msgid "XMLRPC access mode configuration:" +msgstr "" + +msgid "" +"bugzilla.bzurl\n" +" The base URL for the Bugzilla installation.\n" +" Default ``http://localhost/bugzilla``." +msgstr "" + +msgid "" +"bugzilla.user\n" +" The username to use to log into Bugzilla via XMLRPC. Default\n" +" ``bugs``." +msgstr "" + +msgid "" +"bugzilla.password\n" +" The password for Bugzilla login." +msgstr "" + +msgid "" +"XMLRPC+email access mode uses the XMLRPC access mode configuration items,\n" +"and also:" +msgstr "" + +msgid "" +"bugzilla.bzemail\n" +" The Bugzilla email address." +msgstr "" + +msgid "" +"In addition, the Mercurial email settings must be configured. See the\n" +"documentation in hgrc(5), sections ``[email]`` and ``[smtp]``." +msgstr "" + +msgid "MySQL access mode configuration:" +msgstr "" + +msgid "" +"bugzilla.host\n" +" Hostname of the MySQL server holding the Bugzilla database.\n" +" Default ``localhost``." +msgstr "" + +msgid "" +"bugzilla.db\n" +" Name of the Bugzilla database in MySQL. Default ``bugs``." +msgstr "" + +msgid "" +"bugzilla.user\n" +" Username to use to access MySQL server. Default ``bugs``." +msgstr "" + +msgid "" +"bugzilla.password\n" " Password to use to access MySQL server." msgstr "" msgid "" -"timeout\n" +"bugzilla.timeout\n" " Database connection timeout (seconds). Default 5." msgstr "" msgid "" -"version\n" -" Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,\n" -" '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior\n" -" to 2.18." -msgstr "" - -msgid "" -"bzuser\n" +"bugzilla.bzuser\n" " Fallback Bugzilla user name to record comments with, if changeset\n" " committer cannot be found as a Bugzilla user." msgstr "" msgid "" -"bzdir\n" +"bugzilla.bzdir\n" " Bugzilla install directory. Used by default notify. Default\n" -" '/var/www/html/bugzilla'." -msgstr "" - -msgid "" -"notify\n" +" ``/var/www/html/bugzilla``." +msgstr "" + +msgid "" +"bugzilla.notify\n" " The command to run to get Bugzilla to send bug change notification\n" -" emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)\n" -" and 'user' (committer bugzilla email). Default depends on version;\n" -" from 2.18 it is \"cd %(bzdir)s && perl -T contrib/sendbugmail.pl\n" -" %(id)s %(user)s\"." -msgstr "" - -msgid "" -"regexp\n" -" Regular expression to match bug IDs in changeset commit message.\n" -" Must contain one \"()\" group. The default expression matches 'Bug\n" -" 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug\n" -" 1234 and 5678' and variations thereof. Matching is case insensitive." -msgstr "" - -msgid "" -"style\n" -" The style file to use when formatting comments." -msgstr "" - -msgid "" -"template\n" -" Template to use when formatting comments. Overrides style if\n" -" specified. In addition to the usual Mercurial keywords, the\n" -" extension specifies::" -msgstr "" - -msgid "" -" {bug} The Bugzilla bug ID.\n" -" {root} The full pathname of the Mercurial repository.\n" -" {webroot} Stripped pathname of the Mercurial repository.\n" -" {hgweb} Base URL for browsing Mercurial repositories." -msgstr "" - -msgid "" -" Default 'changeset {node|short} in repo {root} refers '\n" -" 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'" -msgstr "" - -msgid "" -"strip\n" -" The number of slashes to strip from the front of {root} to produce\n" -" {webroot}. Default 0." -msgstr "" - -msgid "" -"usermap\n" -" Path of file containing Mercurial committer ID to Bugzilla user ID\n" -" mappings. If specified, the file should contain one mapping per\n" -" line, \"committer\"=\"Bugzilla user\". See also the [usermap] section." -msgstr "" - -msgid "" -"The [usermap] section is used to specify mappings of Mercurial\n" -"committer ID to Bugzilla user ID. See also [bugzilla].usermap.\n" -"\"committer\"=\"Bugzilla user\"" -msgstr "" - -msgid "Finally, the [web] section supports one entry:" -msgstr "" - -msgid "" -"baseurl\n" -" Base URL for browsing Mercurial repositories. Reference from\n" -" templates as {hgweb}." +" emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug\n" +" id) and ``user`` (committer bugzilla email). Default depends on\n" +" version; from 2.18 it is \"cd %(bzdir)s && perl -T\n" +" contrib/sendbugmail.pl %(id)s %(user)s\"." msgstr "" msgid "Activating the extension::" @@ -446,13 +537,92 @@ " incoming.bugzilla = python:hgext.bugzilla.hook" msgstr "" -msgid "Example configuration:" -msgstr "" - -msgid "" -"This example configuration is for a collection of Mercurial\n" -"repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2\n" -"installation in /opt/bugzilla-3.2. ::" +msgid "Example configurations:" +msgstr "Eksempelkonfigurationer:" + +msgid "" +"XMLRPC example configuration. This uses the Bugzilla at\n" +"``http://my-project.org/bugzilla``, logging in as user\n" +"``bugmail@my-project.org`` with password ``plugh``. It is used with a\n" +"collection of Mercurial repositories in ``/var/local/hg/repos/``,\n" +"with a web interface at ``http://my-project.org/hg``. ::" +msgstr "" + +msgid "" +" [bugzilla]\n" +" bzurl=http://my-project.org/bugzilla\n" +" user=bugmail@my-project.org\n" +" password=plugh\n" +" version=xmlrpc\n" +" template=Changeset {node|short} in {root|basename}.\n" +" {hgweb}/{webroot}/rev/{node|short}\\n\n" +" {desc}\\n\n" +" strip=5" +msgstr "" +" [bugzilla]\n" +" bzurl=http://my-project.org/bugzilla\n" +" user=bugmail@my-project.org\n" +" password=plugh\n" +" version=xmlrpc\n" +" template=Changeset {node|short} in {root|basename}.\n" +" {hgweb}/{webroot}/rev/{node|short}\\n\n" +" {desc}\\n\n" +" strip=5" + +msgid "" +" [web]\n" +" baseurl=http://my-project.org/hg" +msgstr "" +" [web]\n" +" baseurl=http://my-project.org/hg" + +msgid "" +"XMLRPC+email example configuration. This uses the Bugzilla at\n" +"``http://my-project.org/bugzilla``, logging in as user\n" +"``bugmail@my-project.org`` wityh password ``plugh``. It is used with a\n" +"collection of Mercurial repositories in ``/var/local/hg/repos/``,\n" +"with a web interface at ``http://my-project.org/hg``. Bug comments\n" +"are sent to the Bugzilla email address\n" +"``bugzilla@my-project.org``. ::" +msgstr "" + +msgid "" +" [bugzilla]\n" +" bzurl=http://my-project.org/bugzilla\n" +" user=bugmail@my-project.org\n" +" password=plugh\n" +" version=xmlrpc\n" +" bzemail=bugzilla@my-project.org\n" +" template=Changeset {node|short} in {root|basename}.\n" +" {hgweb}/{webroot}/rev/{node|short}\\n\n" +" {desc}\\n\n" +" strip=5" +msgstr "" +" [bugzilla]\n" +" bzurl=http://my-project.org/bugzilla\n" +" user=bugmail@my-project.org\n" +" password=plugh\n" +" version=xmlrpc\n" +" bzemail=bugzilla@my-project.org\n" +" template=Changeset {node|short} in {root|basename}.\n" +" {hgweb}/{webroot}/rev/{node|short}\\n\n" +" {desc}\\n\n" +" strip=5" + +msgid "" +" [usermap]\n" +" user@emaildomain.com=user.name@bugzilladomain.com" +msgstr "" +" [usermap]\n" +" user@emaildomain.com=user.name@bugzilladomain.com" + +msgid "" +"MySQL example configuration. This has a local Bugzilla 3.2 installation\n" +"in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,\n" +"the Bugzilla database name is ``bugs`` and MySQL is\n" +"accessed with MySQL username ``bugs`` password ``XYZZY``. It is used\n" +"with a collection of Mercurial repositories in ``/var/local/hg/repos/``,\n" +"with a web interface at ``http://my-project.org/hg``. ::" msgstr "" msgid "" @@ -478,32 +648,22 @@ " {desc}\\n\n" " strip=5" -msgid "" -" [web]\n" -" baseurl=http://dev.domain.com/hg" -msgstr "" -" [web]\n" -" baseurl=http://dev.domain.com/hg" - -msgid "" -" [usermap]\n" -" user@emaildomain.com=user.name@bugzilladomain.com" -msgstr "" -" [usermap]\n" -" user@emaildomain.com=user.name@bugzilladomain.com" - -msgid "Commits add a comment to the Bugzilla bug record of the form::" +msgid "All the above add a comment to the Bugzilla bug record of the form::" msgstr "" msgid "" " Changeset 3b16791d6642 in repository-name.\n" -" http://dev.domain.com/hg/repository-name/rev/3b16791d6642" +" http://my-project.org/hg/repository-name/rev/3b16791d6642" msgstr "" msgid " Changeset commit comment. Bug 1234.\n" msgstr "" #, python-format +msgid "python mysql support not available: %s" +msgstr "python mysql-understøttelse ikke tilgængelig: %s" + +#, python-format msgid "connecting to %s:%s as %s, password %s\n" msgstr "forbinder til %s:%s som %s, kodeord %s\n" @@ -552,6 +712,13 @@ msgid "cannot find bugzilla user id for %s or %s" msgstr "kan ikke finde bugzilla bruger-id for %s eller %s" +msgid "configuration 'bzemail' missing" +msgstr "konfigurationen af 'bzemail' mangler" + +#, python-format +msgid "default bugzilla user %s email not found" +msgstr "" + #, python-format msgid "bugzilla version %s not supported" msgstr "bugzilla version %s ikke understøttet" @@ -563,16 +730,12 @@ msgstr "" #, python-format -msgid "python mysql support not available: %s" -msgstr "python mysql-understøttelse ikke tilgængelig: %s" - -#, python-format msgid "hook type %s does not pass a changeset id" msgstr "" #, python-format -msgid "database error: %s" -msgstr "databasefejl: %s" +msgid "Bugzilla error: %s" +msgstr "Bugzilla fejl: %s" msgid "command to display child changesets" msgstr "kommando til at vise børne-ændringer" @@ -736,12 +899,11 @@ msgstr "farvelæg output for nogle kommandoer" msgid "" -"This extension modifies the status and resolve commands to add color to " -"their\n" -"output to reflect file status, the qseries command to add color to reflect\n" -"patch status (applied, unapplied, missing), and to diff-related\n" -"commands to highlight additions, removals, diff headers, and trailing\n" -"whitespace." +"This extension modifies the status and resolve commands to add color\n" +"to their output to reflect file status, the qseries command to add\n" +"color to reflect patch status (applied, unapplied, missing), and to\n" +"diff-related commands to highlight additions, removals, diff headers,\n" +"and trailing whitespace." msgstr "" "Denne udvidelse ændrer status- og resolve-kommandoerne så de tilføjer\n" "farve til deres output for at afspejle filstatus, qseries-kommandoen\n" @@ -1332,6 +1494,15 @@ msgid "hg debugcvsps [OPTION]... [PATH]..." msgstr "hg debugcvsps [TILVALG]... [STI]..." +msgid ":svnrev: String. Converted subversion revision number." +msgstr "" + +msgid ":svnpath: String. Converted subversion revision project path." +msgstr "" + +msgid ":svnuuid: String. Converted subversion revision repository identifier." +msgstr "" + #, python-format msgid "%s does not look like a Bazaar repository" msgstr "%s ser ikke ud som et Bazaar depot" @@ -1664,6 +1835,9 @@ msgid "updating tags\n" msgstr "opdaterer mærkater\n" +msgid "updating bookmarks\n" +msgstr "opdaterer bogmærker\n" + #, python-format msgid "%s is not a valid start revision" msgstr "%s er ikke en gyldig startrevision" @@ -1676,10 +1850,45 @@ msgid "%s does not look like a monotone repository" msgstr "%s ser ikke ud som et monotone depot" +msgid "bad mtn packet - no end of commandnbr" +msgstr "" + +#, python-format +msgid "bad mtn packet - bad stream type %s" +msgstr "" + +msgid "bad mtn packet - no divider before size" +msgstr "" + +msgid "bad mtn packet - no end of packet size" +msgstr "" + +#, python-format +msgid "bad mtn packet - bad packet size %s" +msgstr "" + +#, python-format +msgid "bad mtn packet - unable to read full packet read %s of %s" +msgstr "" + +#, python-format +msgid "mtn command '%s' returned %s" +msgstr "mtn kommando '%s' returnerede %s" + #, python-format msgid "copying file in renamed directory from '%s' to '%s'" msgstr "kopierer fil i omdøbt katalog fra '%s' til '%s'" +msgid "unable to determine mtn automate interface version" +msgstr "" + +#, python-format +msgid "mtn automate stdio header unexpected: %s" +msgstr "" + +msgid "failed to reach end of mtn automate stdio headers" +msgstr "" + #, python-format msgid "%s does not look like a P4 repository" msgstr "%s ser ikke ud som et P4 depot" @@ -1935,11 +2144,13 @@ msgid "" "The ``win32text.forbid*`` hooks provided by the win32text extension\n" -"have been unified into a single hook named ``eol.hook``. The hook will\n" -"lookup the expected line endings from the ``.hgeol`` file, which means\n" -"you must migrate to a ``.hgeol`` file first before using the hook.\n" -"Remember to enable the eol extension in the repository where you\n" -"install the hook." +"have been unified into a single hook named ``eol.checkheadshook``. The\n" +"hook will lookup the expected line endings from the ``.hgeol`` file,\n" +"which means you must migrate to a ``.hgeol`` file first before using\n" +"the hook. ``eol.checkheadshook`` only checks heads, intermediate\n" +"invalid revisions will be pushed. To forbid them completely, use the\n" +"``eol.checkallhook`` hook. These hooks are best used as\n" +"``pretxnchangegroup`` hooks." msgstr "" msgid "" @@ -1950,25 +2161,24 @@ "glob-mønstre.\n" #, python-format -msgid "%s should not have CRLF line endings" -msgstr "%s skulle ikke have CRLF linieskift" - -#, python-format -msgid "%s should not have LF line endings" -msgstr "%s skulle ikke have LF linieskift" +msgid "ignoring unknown EOL style '%s' from %s\n" +msgstr "ignorerer ukendt linieskifttype '%s' fra %s\n" #, python-format msgid "warning: ignoring .hgeol file due to parse error at %s: %s\n" msgstr "" +#, python-format +msgid " %s in %s should not have %s line endings" +msgstr " %s i %s skulle ikke have %s linieskift" + +msgid "end-of-line check failed:\n" +msgstr "" + msgid "the eol extension is incompatible with the win32text extension\n" msgstr "eol-udvidelsen er inkompatibel med win32text-udvidelsen\n" #, python-format -msgid "ignoring unknown EOL style '%s' from %s\n" -msgstr "ignorerer ukendt linieskifttype '%s' fra %s\n" - -#, python-format msgid "inconsistent newline style in %s\n" msgstr "inkonsistent linieskift i %s\n" @@ -2873,17 +3083,17 @@ "available templates and filters." msgstr "" -msgid "Three additional date template filters are provided::" -msgstr "" - -msgid "" -" utcdate \"2006/09/18 15:13:13\"\n" -" svnutcdate \"2006-09-18 15:13:13Z\"\n" -" svnisodate \"2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)\"" -msgstr "" -" utcdate \"2006/09/18 15:13:13\"\n" -" svnutcdate \"2006-09-18 15:13:13Z\"\n" -" svnisodate \"2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)\"" +msgid "Three additional date template filters are provided:" +msgstr "" + +msgid "" +":``utcdate``: \"2006/09/18 15:13:13\"\n" +":``svnutcdate``: \"2006-09-18 15:13:13Z\"\n" +":``svnisodate``: \"2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)\"" +msgstr "" +":``utcdate``: \"2006/09/18 15:13:13\"\n" +":``svnutcdate``: \"2006-09-18 15:13:13Z\"\n" +":``svnisodate``: \"2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)\"" msgid "" "The default template mappings (view with :hg:`kwdemo -d`) can be\n" @@ -3609,14 +3819,15 @@ " With -g/--git, patches imported with --rev will use the git diff\n" " format. See the diffs help topic for information on why this is\n" " important for preserving rename/copy information and permission\n" -" changes." -msgstr "" -" En eksisterende ændrin kan blive sat under mq kontrol med -r/--rev\n" +" changes. Use :hg:`qfinish` to remove changesets from mq control." +msgstr "" +" En eksisterende ændring kan blive sat under mq kontrol med -r/--rev\n" " (e.g. qimport --rev tip -n patch vil sætte tip under mq kontrol).\n" " Med -g/--git vil patches importeret med --rev bruge git diff\n" " formatet. Se 'hg help diffs' for mere information om hvorfor dette\n" " er vigtigt for at bevare omdøbnings/kopierings-information og\n" -" ændriner i rettigheder." +" ændriner i rettigheder. Brug :hg:`qfinish` for at fjerne ændringer\n" +" fra mq." msgid "" " To import a patch from standard input, pass - as the patch file.\n" @@ -3929,13 +4140,6 @@ " Når -f/--force er angivet, så vil alle lokale ændringer i de\n" " rettede filer gå tabt." -msgid "" -" Return 0 on succces.\n" -" " -msgstr "" -" Returnerer 0 ved succes.\n" -" " - msgid "no saved queues found, please use -n\n" msgstr "fandt ingen gemte køer, brug venligst -r\n" @@ -4053,12 +4257,12 @@ " eksempel::" msgid "" -" qguard foo.patch -stable (negative guard)\n" -" qguard bar.patch +stable (positive guard)\n" +" qguard foo.patch -- -stable (negative guard)\n" +" qguard bar.patch +stable (positive guard)\n" " qselect stable" msgstr "" -" qguard foo.patch -stable (negativt filter)\n" -" qguard bar.patch +stable (positivt filter)\n" +" qguard foo.patch -- -stable (negativt filter)\n" +" qguard bar.patch +stable (positivt filter)\n" " qselect stable" msgid "" @@ -4805,8 +5009,8 @@ msgstr "" msgid "" -"To configure other defaults, add a section like this to your hgrc\n" -"file::" +"To configure other defaults, add a section like this to your\n" +"configuration file::" msgstr "" msgid "" @@ -5311,6 +5515,9 @@ " " msgstr "" +msgid "message can only be specified with collapse" +msgstr "" + msgid "cannot use both abort and continue" msgstr "abort og continue kan ikke angives samtidig" @@ -5323,6 +5530,9 @@ msgid "abort and continue do not allow specifying revisions" msgstr "abort og continue tillader ikke at der angives revisioner" +msgid "tool option will be ignored\n" +msgstr "" + msgid "cannot specify both a revision and a base" msgstr "man kan ikke angive både en revision og en basis" @@ -5335,9 +5545,6 @@ msgid "nothing to rebase\n" msgstr "" -msgid "cannot use both keepbranches and extrafn" -msgstr "man kan ikke bruge både keepbranches og extrafn" - msgid "rebasing" msgstr "" @@ -5407,6 +5614,12 @@ msgid "collapse the rebased changesets" msgstr "" +msgid "use text as collapse commit message" +msgstr "brug tekst som deponeringsbesked" + +msgid "read collapse commit message from file" +msgstr "læs deponeringsbeskeden fra fil" + msgid "keep original changesets" msgstr "behold de originale ændringer" @@ -5416,6 +5629,9 @@ msgid "force detaching of source from its original branch" msgstr "" +msgid "specify merge tool" +msgstr "angiv sammenføjningsværktøj" + msgid "continue an interrupted rebase" msgstr "" @@ -5536,6 +5752,17 @@ msgid " This command is not available when committing a merge." msgstr "" +msgid "interactively record a new patch" +msgstr "vælg ændringer interaktivt til deponering" + +msgid "" +" See :hg:`help qnew` & :hg:`help record` for more information and\n" +" usage.\n" +" " +msgstr "" +" Se :hg:`help qnew` & :hg:`help record` for mere information.\n" +" " + msgid "'mq' extension not loaded" msgstr "'mq' udvidelsen er ikke indlæst" @@ -5641,7 +5868,7 @@ msgstr "sammenkæder" #, python-format -msgid "relinked %d files (%d bytes reclaimed)\n" +msgid "relinked %d files (%s reclaimed)\n" msgstr "" msgid "[ORIGIN]" @@ -5711,6 +5938,10 @@ "same name.\n" msgstr "" +#, python-format +msgid "custom scheme %s:// conflicts with drive letter %s:\\\n" +msgstr "" + msgid "share a common history between several working directories" msgstr "del en fælles historie mellem flere arbejdsbiblioteker" @@ -5800,6 +6031,9 @@ msgid "commit failed" msgstr "deponering fejlede" +msgid "filter corrupted changeset (no user or date)" +msgstr "filtrer ødelagte ændringer (ingen bruger eller dato)" + msgid "" "y: transplant this changeset\n" "n: skip this changeset\n" @@ -5901,6 +6135,11 @@ " Transplanted changesets in set." msgstr "" +msgid "" +":transplanted: String. The node identifier of the transplanted\n" +" changeset if any." +msgstr "" + msgid "pull patches from REPO" msgstr "hiv rettelser fra DEPOT" @@ -6161,6 +6400,18 @@ msgid "bookmark '%s' contains illegal character" msgstr "" +#, python-format +msgid "branch %s not found" +msgstr "gren %s blev ikke fundet" + +#, python-format +msgid "updating bookmark %s\n" +msgstr "opdaterer bogmærke %s\n" + +#, python-format +msgid "not updating divergent bookmark %s\n" +msgstr "opdaterer ikke divergerende bogmærke %s\n" + msgid "searching for changed bookmarks\n" msgstr "leder efter ændrede bogmærker\n" @@ -6699,7 +6950,8 @@ #, python-format msgid "" "Not all ancestors of this changeset have been checked.\n" -"To check the other ancestors, start from the common ancestor, %s.\n" +"Use bisect --extend to continue the bisection from\n" +"the common ancestor, %s.\n" msgstr "" msgid "Due to skipped revisions, the first good revision could be any of:\n" @@ -6737,6 +6989,13 @@ msgstr "Ændring %d:%s: %s\n" #, python-format +msgid "Extending search to changeset %d:%s\n" +msgstr "Udvider søgning til ændring %d:%s\n" + +msgid "nothing to extend" +msgstr "kan ikke udvidde" + +#, python-format msgid "Testing changeset %d:%s (%d changesets remaining, ~%d tests)\n" msgstr "Tester ændring %d:%s (%d ændringer tilbage, ~%d test)\n" @@ -6781,11 +7040,13 @@ " bookmark-udviddelsen skal være slået til.\n" " " -msgid "a bookmark of this name does not exist" -msgstr "et bogmærke med dette navn findes ikke" - -msgid "a bookmark of the same name already exists" -msgstr "et bogmærke med samme navn findes allerede" +#, python-format +msgid "bookmark '%s' does not exist" +msgstr "mærkaten '%s' eksisterer ikke" + +#, python-format +msgid "bookmark '%s' already exists (use -f to force)" +msgstr "mærkaten '%s' eksisterer allerede (brug -f for at gennemtvinge)" msgid "new bookmark name required" msgstr "nyt bogmærkenavn påkrævet" @@ -7144,6 +7405,10 @@ msgid "can only close branch heads" msgstr "kan kun lukke grenhoveder" +#, python-format +msgid "nothing changed (%d missing files, see 'hg status')\n" +msgstr "" + msgid "nothing changed\n" msgstr "ingen ændringer\n" @@ -7314,6 +7579,29 @@ msgid "only one config item permitted" msgstr "" +msgid "test whether node ids are known to a repo" +msgstr "" + +msgid "" +" Every ID must be a full-length hex node id string. Returns a list of 0s " +"and 1s\n" +" indicating unknown/known.\n" +" " +msgstr "" + +msgid "lists the contents of a bundle" +msgstr "vis indholdet af et bundt" + +msgid "retrieves a bundle from a repo" +msgstr "" + +msgid "" +" Every ID must be a full-length hex node id string. Saves the bundle to " +"the\n" +" given file.\n" +" " +msgstr "" + msgid "access the pushkey key/value protocol" msgstr "" @@ -7412,28 +7700,6 @@ msgid " (templates seem to have been installed incorrectly)\n" msgstr "" -msgid "Checking patch...\n" -msgstr "" - -msgid " patch call failed:\n" -msgstr "" - -msgid " unexpected patch output!\n" -msgstr "" - -msgid " patch test failed!\n" -msgstr "" - -msgid "" -" (Current patch tool may be incompatible with patch, or misconfigured. " -"Please check your configuration file)\n" -msgstr "" - -msgid "" -" Internal patcher failure, please report this error to http://mercurial." -"selenic.com/wiki/BugTracker\n" -msgstr "" - msgid "Checking commit editor...\n" msgstr "" @@ -7742,6 +8008,10 @@ "detaljer" #, python-format +msgid "use \"hg help %s\" to show the full help text" +msgstr "brug \"hg help %s\" for at se udførlig hjælp" + +#, python-format msgid "use \"hg -v help%s\" to show builtin aliases and global options" msgstr "" "brug \"hg -v help%s\" for at vise indbyggede aliaser og globale " @@ -7840,7 +8110,14 @@ msgstr "" msgid "" -" With no revision, print a summary of the current state of the\n" +" Print a summary identifiying the repository state at REV\n" +" using one or two parent hash identifiers, followed by a\n" +" \"+\" if there are uncommitted changes in the working directory,\n" +" the branch name (omitted if default) and a list of tags, bookmarks." +msgstr "" + +msgid "" +" When REV is not given, print a summary of the current state of the\n" " repository." msgstr "" @@ -7849,14 +8126,7 @@ " cause lookup to operate on that repository/bundle." msgstr "" -msgid "" -" This summary identifies the repository state using one or two\n" -" parent hash identifiers, followed by a \"+\" if there are\n" -" uncommitted changes in the working directory, a list of tags for\n" -" this revision and a branch name for non-default branches." -msgstr "" - -msgid "can't query remote revision number, branch, tags, or bookmarks" +msgid "can't query remote revision number, branch, or tags" msgstr "" msgid "import an ordered set of patches" @@ -8099,7 +8369,7 @@ msgid "" " ``--tool`` can be used to specify the merge tool used for file\n" " merges. It overrides the HGMERGE environment variable and your\n" -" configuration files." +" configuration files. See :hg:`help merge-tools` for options." msgstr "" msgid "" @@ -8239,6 +8509,12 @@ msgid "(run 'hg heads' to see heads, 'hg merge' to merge)\n" msgstr "(kør 'hg heads' for at se hoveder, 'hg merge' for at sammenføje)\n" +msgid "(run 'hg heads .' to see heads, 'hg merge' to merge)\n" +msgstr "(kør 'hg heads .' for at se hoveder, 'hg merge' for at sammenføje)\n" + +msgid "(run 'hg heads' to see heads)\n" +msgstr "(kør 'hg heads' for at se hoveder)\n" + msgid "(run 'hg update' to get a working copy)\n" msgstr "(kør 'hg update' for at få en arbejdskopi)\n" @@ -8582,8 +8858,9 @@ msgstr "" msgid "" -" If a file has been deleted, it is restored. If the executable mode\n" -" of a file was changed, it is reset." +" If a file has been deleted, it is restored. Files scheduled for\n" +" addition are just unscheduled and left as they are. If the\n" +" executable mode of a file was changed, it is reset." msgstr "" msgid "" @@ -9333,9 +9610,6 @@ msgid "parent to choose when backing out merge" msgstr "" -msgid "specify merge tool" -msgstr "angiv sammenføjningsværktøj" - msgid "revision to backout" msgstr "revision som skal bakkes ud" @@ -9354,6 +9628,9 @@ msgid "skip testing changeset" msgstr "spring testen af denne ændring over" +msgid "extend the bisect range" +msgstr "udvid halveringsområdet" + msgid "use command to check changeset state" msgstr "brug kommando for at kontrollere tilstanden af ændringen" @@ -9474,6 +9751,9 @@ msgid "[OPTION]... TEXT" msgstr "[TILVALG]... TEKST" +msgid "show all details" +msgstr "vis alle detaljer" + msgid "[COMMAND]" msgstr "[KOMMANDO]" @@ -9510,9 +9790,21 @@ msgid "[PATH]" msgstr "[STI]" +msgid "id of head node" +msgstr "" + +msgid "id of common node" +msgstr "id på fælles knude" + +msgid "REPO FILE [-H|-C ID]..." +msgstr "" + msgid "revlog format" msgstr "" +msgid "REPO ID..." +msgstr "" + msgid "REPO NAMESPACE [KEY OLD NEW]" msgstr "" @@ -9534,12 +9826,18 @@ msgid "do not display the saved mtime" msgstr "vis ikke den gemte mtime" +msgid "sort by saved mtime" +msgstr "sorter efter den gemte mtime" + msgid "[OPTION]..." msgstr "[TILVALG]..." msgid "revision to check" msgstr "revision som skal undersøges" +msgid "REPO [OPTIONS]... [ONE [TWO]]" +msgstr "DEPOT [TILVALG]... [EN [TO]]" + msgid "[OPTION]... ([-c REV] | [-r REV1 [-r REV2]]) [FILE]..." msgstr "[TILVALG]... ([-c REV] | [-r REV1 [-r REV2]]) [FIL]..." @@ -10014,10 +10312,6 @@ msgstr "arbejdskatalogtilstand virker beskadiget!" #, python-format -msgid "'\\n' and '\\r' disallowed in filenames: %r" -msgstr "'\\n' og '\\r' må ikke forekomme i filnavne: %r" - -#, python-format msgid "directory %r already in dirstate" msgstr "katalog %r er allerede i dirstate" @@ -10566,29 +10860,29 @@ msgstr "- ``1165432709 0`` (Ons 6. dec. 13:18:29 2006 UTC)" msgid "" -"This is the internal representation format for dates. unixtime is the\n" -"number of seconds since the epoch (1970-01-01 00:00 UTC). offset is\n" -"the offset of the local timezone, in seconds west of UTC (negative if\n" -"the timezone is east of UTC)." -msgstr "" -"Dette er den interne repræsentation af datoer. unixtime er\n" +"This is the internal representation format for dates. The first number\n" +"is the number of seconds since the epoch (1970-01-01 00:00 UTC). The\n" +"second is the offset of the local timezone, in seconds west of UTC\n" +"(negative if the timezone is east of UTC)." +msgstr "" +"Dette er den interne repræsentation af datoer. Det første tal er\n" "antallet af sekunder siden begyndelsen af epoken (1970-01-01 00:00\n" -"UTC). offset er den lokale tidszone, angivet i antal sekunder vest\n" -"for UTC (negativ hvis tidszonen er øst for UTC)." +"UTC). Det andet tal er den lokale tidszone, angivet i antal sekunder\n" +"vest for UTC (negativ hvis tidszonen er øst for UTC)." msgid "The log command also accepts date ranges:" msgstr "Kommandoen log accepterer også datointervaller:" msgid "" -"- ``<{datetime}`` - at or before a given date/time\n" -"- ``>{datetime}`` - on or after a given date/time\n" -"- ``{datetime} to {datetime}`` - a date range, inclusive\n" -"- ``-{days}`` - within a given number of days of today\n" -msgstr "" -"- ``<{date}`` - på eller før den angivne dato/tidspunkt\n" -"- ``>{date}`` - på eller efter den angivne dato/tidspunkt\n" -"- ``{date} to {date}`` - et datointerval, inklusiv endepunkterne\n" -"- ``-{days}`` - indenfor et angivet antal dage, fra dags dato\n" +"- ``DATE`` - on or after a given date/time\n" +"- ``DATE to DATE`` - a date range, inclusive\n" +"- ``-DAYS`` - within a given number of days of today\n" +msgstr "" +"- ``DATO`` - på eller efter den angivne dato/tidspunkt\n" +"- ``DATO to DATO`` - et datointerval, inklusiv endepunkterne\n" +"- ``-DAGE`` - indenfor et angivet antal dage, fra dags dato\n" msgid "" "Mercurial's default format for showing changes between two versions of\n" @@ -10714,6 +11008,18 @@ msgstr "" msgid "" +"HGPLAINEXCEPT\n" +" This is a comma-separated list of features to preserve when\n" +" HGPLAIN is enabled. Currently the only value supported is \"i18n\",\n" +" which preserves internationalization in plain mode." +msgstr "" + +msgid "" +" Setting HGPLAINEXCEPT to anything (even an empty string) will\n" +" enable plain mode." +msgstr "" + +msgid "" "HGUSER\n" " This is the string used as the author of a commit. If not set,\n" " available values will be considered in this order:" @@ -11367,8 +11673,8 @@ msgstr "" msgid "" -"This file uses the same syntax as hgrc configuration files, but only\n" -"the following sections are recognized:" +"This file uses the same syntax as other Mercurial configuration files,\n" +"but only the following sections are recognized:" msgstr "" msgid "" @@ -11382,7 +11688,8 @@ msgid "" "The ``web`` section can specify all the settings described in the web\n" -"section of the hgrc documentation." +"section of the hgrc(5) documentation. See :hg:`help config` for\n" +"information on where to find the manual page." msgstr "" msgid "" @@ -11889,7 +12196,7 @@ msgid " hg log -r \"1.3::1.5 and keyword(bug) and file('hgext/*')\"" msgstr " hg log -r \"1.3::1.5 and keyword(bug) and file(\"hgext/*\")\"" -msgid "- Changesets in committed May 2008, sorted by user::" +msgid "- Changesets committed in May 2008, sorted by user::" msgstr "" msgid " hg log -r \"sort(date('May 2008'), user)\"" @@ -12134,78 +12441,7 @@ "keywords are usually available for templating a log-like command:" msgstr "" -msgid ":author: String. The unmodified author of the changeset." -msgstr "" - -msgid "" -":branch: String. The name of the branch on which the changeset was\n" -" committed." -msgstr "" - -msgid "" -":branches: List of strings. The name of the branch on which the\n" -" changeset was committed. Will be empty if the branch name was\n" -" default." -msgstr "" - -msgid ":children: List of strings. The children of the changeset." -msgstr "" - -msgid ":date: Date information. The date when the changeset was committed." -msgstr "" - -msgid ":desc: String. The text of the changeset description." -msgstr "" - -msgid "" -":diffstat: String. Statistics of changes with the following format:\n" -" \"modified files: +added/-removed lines\"" -msgstr "" - -msgid "" -":files: List of strings. All files modified, added, or removed by this\n" -" changeset." -msgstr "" - -msgid ":file_adds: List of strings. Files added by this changeset." -msgstr "" - -msgid "" -":file_copies: List of strings. Files copied in this changeset with\n" -" their sources." -msgstr "" - -msgid "" -":file_copies_switch: List of strings. Like \"file_copies\" but displayed\n" -" only if the --copied switch is set." -msgstr "" - -msgid ":file_mods: List of strings. Files modified by this changeset." -msgstr "" - -msgid ":file_dels: List of strings. Files removed by this changeset." -msgstr "" - -msgid "" -":node: String. The changeset identification hash, as a 40 hexadecimal\n" -" digit string." -msgstr "" - -msgid ":parents: List of strings. The parents of the changeset." -msgstr "" - -msgid ":rev: Integer. The repository-local changeset revision number." -msgstr "" - -msgid ":tags: List of strings. Any tags associated with the changeset." -msgstr "" - -msgid "" -":latesttag: String. Most recent global tag in the ancestors of this\n" -" changeset." -msgstr "" - -msgid ":latesttagdistance: Integer. Longest path to the latest tag." +msgid ".. keywordsmarker" msgstr "" msgid "" @@ -12227,131 +12463,7 @@ msgid "List of filters:" msgstr "" -msgid "" -":addbreaks: Any text. Add an XHTML \"
\" tag before the end of\n" -" every line except the last." -msgstr "" - -msgid "" -":age: Date. Returns a human-readable date/time difference between the\n" -" given date/time and the current date/time." -msgstr "" - -msgid "" -":basename: Any text. Treats the text as a path, and returns the last\n" -" component of the path after splitting by the path separator\n" -" (ignoring trailing separators). For example, \"foo/bar/baz\" becomes\n" -" \"baz\" and \"foo/bar//\" becomes \"bar\"." -msgstr "" - -msgid "" -":stripdir: Treat the text as path and strip a directory level, if\n" -" possible. For example, \"foo\" and \"foo/bar\" becomes \"foo\"." -msgstr "" - -msgid "" -":date: Date. Returns a date in a Unix date format, including the\n" -" timezone: \"Mon Sep 04 15:13:13 2006 0700\"." -msgstr "" - -msgid "" -":domain: Any text. Finds the first string that looks like an email\n" -" address, and extracts just the domain component. Example: ``User\n" -" `` becomes ``example.com``." -msgstr "" - -msgid "" -":email: Any text. Extracts the first string that looks like an email\n" -" address. Example: ``User `` becomes\n" -" ``user@example.com``." -msgstr "" - -msgid "" -":escape: Any text. Replaces the special XML/XHTML characters \"&\", \"<\"\n" -" and \">\" with XML entities." -msgstr "" - -msgid "" -":hex: Any text. Convert a binary Mercurial node identifier into\n" -" its long hexadecimal representation." -msgstr "" - -msgid ":fill68: Any text. Wraps the text to fit in 68 columns." -msgstr "" - -msgid ":fill76: Any text. Wraps the text to fit in 76 columns." -msgstr "" - -msgid ":firstline: Any text. Returns the first line of text." -msgstr "" - -msgid ":nonempty: Any text. Returns '(none)' if the string is empty." -msgstr "" - -msgid "" -":hgdate: Date. Returns the date as a pair of numbers: \"1157407993\n" -" 25200\" (Unix timestamp, timezone offset)." -msgstr "" - -msgid "" -":isodate: Date. Returns the date in ISO 8601 format: \"2009-08-18 13:00\n" -" +0200\"." -msgstr "" - -msgid "" -":isodatesec: Date. Returns the date in ISO 8601 format, including\n" -" seconds: \"2009-08-18 13:00:13 +0200\". See also the rfc3339date\n" -" filter." -msgstr "" - -msgid ":localdate: Date. Converts a date to local date." -msgstr "" - -msgid "" -":obfuscate: Any text. Returns the input text rendered as a sequence of\n" -" XML entities." -msgstr "" - -msgid ":person: Any text. Returns the text before an email address." -msgstr "" - -msgid "" -":rfc822date: Date. Returns a date using the same format used in email\n" -" headers: \"Tue, 18 Aug 2009 13:00:13 +0200\"." -msgstr "" - -msgid "" -":rfc3339date: Date. Returns a date using the Internet date format\n" -" specified in RFC 3339: \"2009-08-18T13:00:13+02:00\"." -msgstr "" - -msgid "" -":short: Changeset hash. Returns the short form of a changeset hash,\n" -" i.e. a 12 hexadecimal digit string." -msgstr "" - -msgid ":shortdate: Date. Returns a date like \"2006-09-18\"." -msgstr "" - -msgid "" -":stringify: Any type. Turns the value into text by converting values into\n" -" text and concatenating them." -msgstr "" - -msgid ":strip: Any text. Strips all leading and trailing whitespace." -msgstr "" - -msgid "" -":tabindent: Any text. Returns the text, with every line except the\n" -" first starting with a tab character." -msgstr "" - -msgid "" -":urlescape: Any text. Escapes all \"special\" characters. For example,\n" -" \"foo bar\" becomes \"foo%20bar\"." -msgstr "" - -msgid ":user: Any text. Returns the user portion of an email address.\n" +msgid ".. filtersmarker\n" msgstr "" msgid "Valid URLs are of the form::" @@ -12800,14 +12912,6 @@ msgstr "" #, python-format -msgid "updating bookmark %s\n" -msgstr "opdaterer bogmærke %s\n" - -#, python-format -msgid "not updating divergent bookmark %s\n" -msgstr "opdaterer ikke divergerende bogmærke %s\n" - -#, python-format msgid "%d changesets found\n" msgstr "fandt %d ændringer\n" @@ -12918,6 +13022,10 @@ msgstr "ugyldig lokal adresse: %s" #, python-format +msgid "'\\n' and '\\r' disallowed in filenames: %r" +msgstr "'\\n' og '\\r' må ikke forekomme i filnavne: %r" + +#, python-format msgid "failed to remove %s from manifest" msgstr "kunne ikke fjerne %s fra manifest" @@ -13004,10 +13112,6 @@ msgid "note: possible conflict - %s was renamed multiple times to:\n" msgstr "" -#, python-format -msgid "branch %s not found" -msgstr "gren %s blev ikke fundet" - msgid "merging with a working directory ancestor has no effect" msgstr "sammenføjning med en forfader til arbejdskataloget har ingen effekt" @@ -13122,10 +13226,6 @@ msgstr "" #, python-format -msgid "malformed patch %s %s" -msgstr "" - -#, python-format msgid "unsupported parser state: %s" msgstr "" @@ -13238,86 +13338,12 @@ msgstr "ikke en funktion: %s" msgid "" -"``id(string)``\n" -" Revision non-ambiguously specified by the given hex string prefix." -msgstr "" - -#. i18n: "id" is a keyword -msgid "id requires one argument" -msgstr "id kræver et argument" - -#. i18n: "id" is a keyword -msgid "id requires a string" -msgstr "id kræver en streng" - -msgid "" -"``rev(number)``\n" -" Revision with the given numeric identifier." -msgstr "" - -#. i18n: "rev" is a keyword -msgid "rev requires one argument" -msgstr "rev kræver et argument" - -#. i18n: "rev" is a keyword -msgid "rev requires a number" -msgstr "rev kræver et tal" - -#. i18n: "rev" is a keyword -msgid "rev expects a number" -msgstr "rev forventer et revisionsnummer" - -msgid "" -"``p1([set])``\n" -" First parent of changesets in set, or the working directory." -msgstr "" - -msgid "" -"``p2([set])``\n" -" Second parent of changesets in set, or the working directory." -msgstr "" - -msgid "" -"``parents([set])``\n" -" The set of all parents for all changesets in set, or the working " -"directory." -msgstr "" - -msgid "" -"``max(set)``\n" -" Changeset with highest revision number in set." -msgstr "" - -msgid "" -"``min(set)``\n" -" Changeset with lowest revision number in set." -msgstr "" - -msgid "" -"``limit(set, n)``\n" -" First n members of set." -msgstr "" - -#. i18n: "limit" is a keyword -msgid "limit requires two arguments" -msgstr "limit kræver to argumenter" - -#. i18n: "limit" is a keyword -msgid "limit requires a number" -msgstr "limit kræver et tal" - -#. i18n: "limit" is a keyword -msgid "limit expects a number" -msgstr "" - -msgid "" -"``children(set)``\n" -" Child changesets of changesets in set." -msgstr "" - -msgid "" -"``branch(set)``\n" -" All changesets belonging to the branches of changesets in set." +"``adds(pattern)``\n" +" Changesets that add a file matching pattern." +msgstr "" + +#. i18n: "adds" is a keyword +msgid "adds requires a pattern" msgstr "" msgid "" @@ -13339,6 +13365,80 @@ msgstr "" msgid "" +"``author(string)``\n" +" Alias for ``user(string)``." +msgstr "" + +#. i18n: "author" is a keyword +msgid "author requires a string" +msgstr "" + +msgid "" +"``bisected(string)``\n" +" Changesets marked in the specified bisect state (good, bad, skip)." +msgstr "" + +msgid "bisect requires a string" +msgstr "bisect kræver en streng" + +msgid "invalid bisect state" +msgstr "ugyldig tilstand for halvering" + +msgid "" +"``bookmark([name])``\n" +" The named bookmark or all bookmarks." +msgstr "" +"``bookmark([name])``\n" +" Det navngivne bogmærke eller alle bogmærker." + +#. i18n: "bookmark" is a keyword +msgid "bookmark takes one or no arguments" +msgstr "bookmark tager et eller to argumenter" + +#. i18n: "bookmark" is a keyword +msgid "the argument to bookmark must be a string" +msgstr "argumentet til bookmark skal være en streng" + +msgid "" +"``branch(string or set)``\n" +" All changesets belonging to the given branch or the branches of the " +"given\n" +" changesets." +msgstr "" + +msgid "" +"``children(set)``\n" +" Child changesets of changesets in set." +msgstr "" + +msgid "" +"``closed()``\n" +" Changeset is closed." +msgstr "" + +#. i18n: "closed" is a keyword +msgid "closed takes no arguments" +msgstr "" + +msgid "" +"``contains(pattern)``\n" +" Revision contains pattern." +msgstr "" + +#. i18n: "contains" is a keyword +msgid "contains requires a pattern" +msgstr "" + +msgid "" +"``date(interval)``\n" +" Changesets within the interval, see :hg:`help dates`." +msgstr "" + +#. i18n: "date" is a keyword +msgid "date requires a string" +msgstr "" + +msgid "" "``descendants(set)``\n" " Changesets which are descendants of changesets in set." msgstr "" @@ -13353,22 +13453,12 @@ msgstr "" msgid "" -"``date(interval)``\n" -" Changesets within the interval, see :hg:`help dates`." -msgstr "" - -#. i18n: "date" is a keyword -msgid "date requires a string" -msgstr "" - -msgid "" -"``keyword(string)``\n" -" Search commit message, user name, and names of changed files for\n" -" string." -msgstr "" - -#. i18n: "keyword" is a keyword -msgid "keyword requires a string" +"``all()``\n" +" All changesets, the same as ``0:tip``." +msgstr "" + +#. i18n: "all" is a keyword +msgid "all takes no arguments" msgstr "" msgid "" @@ -13386,20 +13476,6 @@ msgstr "ugyldigt søgemønster: %s" msgid "" -"``author(string)``\n" -" Alias for ``user(string)``." -msgstr "" - -#. i18n: "author" is a keyword -msgid "author requires a string" -msgstr "" - -msgid "" -"``user(string)``\n" -" User name is string." -msgstr "" - -msgid "" "``file(pattern)``\n" " Changesets affecting files matched by pattern." msgstr "" @@ -13409,12 +13485,63 @@ msgstr "" msgid "" -"``contains(pattern)``\n" -" Revision contains pattern." -msgstr "" - -#. i18n: "contains" is a keyword -msgid "contains requires a pattern" +"``head()``\n" +" Changeset is a named branch head." +msgstr "" + +#. i18n: "head" is a keyword +msgid "head takes no arguments" +msgstr "" + +msgid "" +"``heads(set)``\n" +" Members of set with no children in set." +msgstr "" + +msgid "" +"``keyword(string)``\n" +" Search commit message, user name, and names of changed files for\n" +" string." +msgstr "" + +#. i18n: "keyword" is a keyword +msgid "keyword requires a string" +msgstr "" + +msgid "" +"``limit(set, n)``\n" +" First n members of set." +msgstr "" + +#. i18n: "limit" is a keyword +msgid "limit requires two arguments" +msgstr "limit kræver to argumenter" + +#. i18n: "limit" is a keyword +msgid "limit requires a number" +msgstr "limit kræver et tal" + +#. i18n: "limit" is a keyword +msgid "limit expects a number" +msgstr "" + +msgid "" +"``max(set)``\n" +" Changeset with highest revision number in set." +msgstr "" + +msgid "" +"``merge()``\n" +" Changeset is a merge changeset." +msgstr "" + +#. i18n: "merge" is a keyword +msgid "merge takes no arguments" +msgstr "" + +msgid "" +"``min(set)``\n" +" Changeset with lowest revision number in set." msgstr "" msgid "" @@ -13427,12 +13554,48 @@ msgstr "" msgid "" -"``adds(pattern)``\n" -" Changesets that add a file matching pattern." -msgstr "" - -#. i18n: "adds" is a keyword -msgid "adds requires a pattern" +"``id(string)``\n" +" Revision non-ambiguously specified by the given hex string prefix." +msgstr "" + +#. i18n: "id" is a keyword +msgid "id requires one argument" +msgstr "id kræver et argument" + +#. i18n: "id" is a keyword +msgid "id requires a string" +msgstr "id kræver en streng" + +msgid "" +"``outgoing([path])``\n" +" Changesets not found in the specified destination repository, or the\n" +" default push location." +msgstr "" + +#. i18n: "outgoing" is a keyword +msgid "outgoing requires a repository path" +msgstr "" + +msgid "" +"``p1([set])``\n" +" First parent of changesets in set, or the working directory." +msgstr "" + +msgid "" +"``p2([set])``\n" +" Second parent of changesets in set, or the working directory." +msgstr "" + +msgid "" +"``parents([set])``\n" +" The set of all parents for all changesets in set, or the working " +"directory." +msgstr "" + +msgid "" +"``present(set)``\n" +" An empty set, if any revision in set isn't found; otherwise,\n" +" all revisions in set." msgstr "" msgid "" @@ -13445,31 +13608,21 @@ msgstr "" msgid "" -"``merge()``\n" -" Changeset is a merge changeset." -msgstr "" - -#. i18n: "merge" is a keyword -msgid "merge takes no arguments" -msgstr "" - -msgid "" -"``closed()``\n" -" Changeset is closed." -msgstr "" - -#. i18n: "closed" is a keyword -msgid "closed takes no arguments" -msgstr "" - -msgid "" -"``head()``\n" -" Changeset is a named branch head." -msgstr "" - -#. i18n: "head" is a keyword -msgid "head takes no arguments" -msgstr "" +"``rev(number)``\n" +" Revision with the given numeric identifier." +msgstr "" + +#. i18n: "rev" is a keyword +msgid "rev requires one argument" +msgstr "rev kræver et argument" + +#. i18n: "rev" is a keyword +msgid "rev requires a number" +msgstr "rev kræver et tal" + +#. i18n: "rev" is a keyword +msgid "rev expects a number" +msgstr "rev forventer et revisionsnummer" msgid "" "``reverse(set)``\n" @@ -13477,9 +13630,8 @@ msgstr "" msgid "" -"``present(set)``\n" -" An empty set, if any revision in set isn't found; otherwise,\n" -" all revisions in set." +"``roots(set)``\n" +" Changesets with no parent changeset in set." msgstr "" msgid "" @@ -13511,35 +13663,6 @@ msgstr "ukendt sorteringsnøgle %r" msgid "" -"``all()``\n" -" All changesets, the same as ``0:tip``." -msgstr "" - -#. i18n: "all" is a keyword -msgid "all takes no arguments" -msgstr "" - -msgid "" -"``heads(set)``\n" -" Members of set with no children in set." -msgstr "" - -msgid "" -"``roots(set)``\n" -" Changesets with no parent changeset in set." -msgstr "" - -msgid "" -"``outgoing([path])``\n" -" Changesets not found in the specified destination repository, or the\n" -" default push location." -msgstr "" - -#. i18n: "outgoing" is a keyword -msgid "outgoing requires a repository path" -msgstr "" - -msgid "" "``tag(name)``\n" " The specified tag by name, or all tagged revisions if no name is given." msgstr "" @@ -13556,19 +13679,9 @@ msgstr "argumentet til tag skal være en streng" msgid "" -"``bookmark([name])``\n" -" The named bookmark or all bookmarks." -msgstr "" -"``bookmark([name])``\n" -" Det navngivne bogmærke eller alle bogmærker." - -#. i18n: "bookmark" is a keyword -msgid "bookmark takes one or no arguments" -msgstr "bookmark tager et eller to argumenter" - -#. i18n: "bookmark" is a keyword -msgid "the argument to bookmark must be a string" -msgstr "argumentet til bookmark skal være en streng" +"``user(string)``\n" +" User name is string." +msgstr "" msgid "can't negate that" msgstr "" @@ -13716,12 +13829,8 @@ msgstr "fjerner ikke depotet %s fordi det er ændret.\n" #, python-format -msgid "cloning subrepo %s\n" -msgstr "kloner underdepot %s\n" - -#, python-format -msgid "pulling subrepo %s\n" -msgstr "hiver underdepot %s\n" +msgid "cloning subrepo %s from %s\n" +msgstr "kloner underdepot %s fra %s\n" #, python-format msgid "revision %s does not exist in subrepo %s\n" @@ -13861,6 +13970,9 @@ msgid "edit failed" msgstr "redigering fejlede" +msgid "file:// URLs can only refer to localhost" +msgstr "" + #, python-format msgid "ignoring invalid [auth] key '%s'\n" msgstr "ignorerer ugyldig [auth] nøgle '%s'\n" @@ -13937,6 +14049,18 @@ msgstr "kommandoen '%s' fejlede: %s" #, python-format +msgid "filename contains '%s', which is reserved on Windows" +msgstr "" + +#, python-format +msgid "filename contains %r, which is invalid on Windows" +msgstr "" + +#, python-format +msgid "filename ends with '%s', which is not allowed on Windows" +msgstr "" + +#, python-format msgid "path ends in directory separator: %s" msgstr "" @@ -13945,8 +14069,8 @@ msgstr "stien indeholder ugyldig komponent: %s" #, python-format -msgid "path %r is inside repo %r" -msgstr "stien %r er inde i repo %r" +msgid "path %r is inside nested repo %r" +msgstr "stien %r er inden i depotet %r" #, python-format msgid "path %r traverses symbolic link %r" @@ -13979,11 +14103,24 @@ msgid "impossible time zone offset: %d" msgstr "umuligt tidszone: %d" +msgid "dates cannot consist entirely of whitespace" +msgstr "datoer kan ikke bestå udelukkende af tomrum" + +msgid "invalid day spec, use 'DATE'" +msgstr "ugyldig datospecifikation, brug '>DATO'" + #, python-format msgid "invalid day spec: %s" msgstr "ugyldig datospecifikation: %s" #, python-format +msgid "%s must be nonnegative (see 'hg help dates')" +msgstr "" + +#, python-format msgid "%.0f GB" msgstr "%.0f GB" diff -r 7b627923739f -r 680c3c6fcb48 mercurial/ancestor.py --- a/mercurial/ancestor.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/ancestor.py Fri May 06 14:29:53 2011 +0200 @@ -9,9 +9,10 @@ def ancestor(a, b, pfunc): """ - return a minimal-distance ancestor of nodes a and b, or None if there is no - such ancestor. Note that there can be several ancestors with the same - (minimal) distance, and the one returned is arbitrary. + Returns the common ancestor of a and b that is furthest from a + root (as measured by longest path) or None if no ancestor is + found. If there are multiple common ancestors at the same + distance, the first one found is returned. pfunc must return a list of parent vertices for a given vertex """ @@ -22,6 +23,7 @@ a, b = sorted([a, b]) # find depth from root of all ancestors + # depth is stored as a negative for heapq parentcache = {} visit = [a, b] depth = {} @@ -39,6 +41,7 @@ if p not in depth: visit.append(p) if visit[-1] == vertex: + # -(maximum distance of parents + 1) depth[vertex] = min([depth[p] for p in pl]) - 1 visit.pop() diff -r 7b627923739f -r 680c3c6fcb48 mercurial/archival.py --- a/mercurial/archival.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/archival.py Fri May 06 14:29:53 2011 +0200 @@ -8,8 +8,8 @@ from i18n import _ from node import hex import cmdutil -import util, encoding -import cStringIO, os, stat, tarfile, time, zipfile +import scmutil, util, encoding +import cStringIO, os, tarfile, time, zipfile import zlib, gzip def tidyprefix(dest, kind, prefix): @@ -172,10 +172,10 @@ # unzip will not honor unix file modes unless file creator is # set to unix (id 3). i.create_system = 3 - ftype = stat.S_IFREG + ftype = 0x8000 # UNX_IFREG in unzip source code if islink: mode = 0777 - ftype = stat.S_IFLNK + ftype = 0xa000 # UNX_IFLNK in unzip source code i.external_attr = (mode | ftype) << 16L self.z.writestr(i, data) @@ -187,7 +187,7 @@ def __init__(self, name, mtime): self.basedir = name - self.opener = util.opener(self.basedir) + self.opener = scmutil.opener(self.basedir) def addfile(self, name, mode, islink, data): if islink: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/bdiff.c --- a/mercurial/bdiff.c Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/bdiff.c Fri May 06 14:29:53 2011 +0200 @@ -49,7 +49,7 @@ #include "util.h" struct line { - int h, len, n, e; + int hash, len, n, e; const char *l; }; @@ -63,9 +63,10 @@ struct hunk *next; }; -int splitlines(const char *a, int len, struct line **lr) +static int splitlines(const char *a, int len, struct line **lr) { - int h, i; + unsigned hash; + int i; const char *p, *b = a; const char * const plast = a + len - 1; struct line *l; @@ -81,14 +82,14 @@ return -1; /* build the line array and calculate hashes */ - h = 0; + hash = 0; for (p = a; p < a + len; p++) { /* Leonid Yuriev's hash */ - h = (h * 1664525) + *p + 1013904223; + hash = (hash * 1664525) + (unsigned char)*p + 1013904223; if (*p == '\n' || p == plast) { - l->h = h; - h = 0; + l->hash = hash; + hash = 0; l->len = p - b + 1; l->l = b; l->n = INT_MAX; @@ -98,14 +99,15 @@ } /* set up a sentinel */ - l->h = l->len = 0; + l->hash = 0; + l->len = 0; l->l = a + len; return i - 1; } -int inline cmp(struct line *a, struct line *b) +static inline int cmp(struct line *a, struct line *b) { - return a->h != b->h || a->len != b->len || memcmp(a->l, b->l, a->len); + return a->hash != b->hash || a->len != b->len || memcmp(a->l, b->l, a->len); } static int equatelines(struct line *a, int an, struct line *b, int bn) @@ -138,7 +140,7 @@ /* add lines to the hash table chains */ for (i = bn - 1; i >= 0; i--) { /* find the equivalence class */ - for (j = b[i].h & buckets; h[j].pos != INT_MAX; + for (j = b[i].hash & buckets; h[j].pos != INT_MAX; j = (j + 1) & buckets) if (!cmp(b + i, b + h[j].pos)) break; @@ -156,7 +158,7 @@ /* match items in a to their equivalence class in b */ for (i = 0; i < an; i++) { /* find the equivalence class */ - for (j = a[i].h & buckets; h[j].pos != INT_MAX; + for (j = a[i].hash & buckets; h[j].pos != INT_MAX; j = (j + 1) & buckets) if (!cmp(a + i, b + h[j].pos)) break; diff -r 7b627923739f -r 680c3c6fcb48 mercurial/bookmarks.py --- a/mercurial/bookmarks.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/bookmarks.py Fri May 06 14:29:53 2011 +0200 @@ -6,9 +6,9 @@ # GNU General Public License version 2 or any later version. from mercurial.i18n import _ -from mercurial.node import nullid, nullrev, bin, hex, short -from mercurial import encoding, util -import os +from mercurial.node import hex +from mercurial import encoding, error, util +import errno, os def valid(mark): for c in (':', '\0', '\n', '\r'): @@ -23,14 +23,18 @@ in the .hg/bookmarks file. Read the file and return a (name=>nodeid) dictionary ''' + bookmarks = {} try: - bookmarks = {} for line in repo.opener('bookmarks'): sha, refspec = line.strip().split(' ', 1) refspec = encoding.tolocal(refspec) - bookmarks[refspec] = repo.changelog.lookup(sha) - except: - pass + try: + bookmarks[refspec] = repo.changelog.lookup(sha) + except error.RepoLookupError: + pass + except IOError, inst: + if inst.errno != errno.ENOENT: + raise return bookmarks def readcurrent(repo): @@ -41,12 +45,18 @@ is stored in .hg/bookmarks.current ''' mark = None - if os.path.exists(repo.join('bookmarks.current')): + try: file = repo.opener('bookmarks.current') + except IOError, inst: + if inst.errno != errno.ENOENT: + raise + return None + try: # No readline() in posixfile_nt, reading everything is cheap mark = encoding.tolocal((file.readlines() or [''])[0]) if mark == '' or mark not in repo._bookmarks: mark = None + finally: file.close() return mark @@ -62,10 +72,12 @@ refs = repo._bookmarks try: - bms = repo.opener('bookmarks').read() - except IOError: + bms = repo.opener.read('bookmarks') + except IOError, inst: + if inst.errno != errno.ENOENT: + raise bms = '' - repo.opener('undo.bookmarks', 'w').write(bms) + repo.opener.write('undo.bookmarks', bms) if repo._bookmarkcurrent not in refs: setcurrent(repo, None) @@ -101,13 +113,7 @@ if current == mark: return - refs = repo._bookmarks - - # do not update if we do update to a rev equal to the current bookmark - if (mark and mark not in refs and - current and refs[current] == repo.changectx('.').node()): - return - if mark not in refs: + if mark not in repo._bookmarks: mark = '' if not valid(mark): raise util.Abort(_("bookmark '%s' contains illegal " @@ -122,6 +128,15 @@ wlock.release() repo._bookmarkcurrent = mark +def updatecurrentbookmark(repo, oldnode, curbranch): + try: + update(repo, oldnode, repo.branchtags()[curbranch]) + except KeyError: + if curbranch == "default": # no default branch! + update(repo, oldnode, repo.lookup("tip")) + else: + raise util.Abort(_("branch %s not found") % curbranch) + def update(repo, parents, node): marks = repo._bookmarks update = False @@ -163,6 +178,28 @@ finally: w.release() +def updatefromremote(ui, repo, remote): + ui.debug("checking for updated bookmarks\n") + rb = remote.listkeys('bookmarks') + changed = False + for k in rb.keys(): + if k in repo._bookmarks: + nr, nl = rb[k], repo._bookmarks[k] + if nr in repo: + cr = repo[nr] + cl = repo[nl] + if cl.rev() >= cr.rev(): + continue + if cr in cl.descendants(): + repo._bookmarks[k] = cr.node() + changed = True + ui.status(_("updating bookmark %s\n") % k) + else: + ui.warn(_("not updating divergent" + " bookmark %s\n") % k) + if changed: + write(repo) + def diff(ui, repo, remote): ui.status(_("searching for changed bookmarks\n")) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/bundlerepo.py --- a/mercurial/bundlerepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/bundlerepo.py Fri May 06 14:29:53 2011 +0200 @@ -13,13 +13,12 @@ from node import nullid from i18n import _ -import os, struct, tempfile, shutil +import os, tempfile, shutil import changegroup, util, mdiff, discovery import localrepo, changelog, manifest, filelog, revlog, error class bundlerevlog(revlog.revlog): - def __init__(self, opener, indexfile, bundle, - linkmapper=None): + def __init__(self, opener, indexfile, bundle, linkmapper): # How it works: # to retrieve a revision, we need to know the offset of # the revision in the bundle (an unbundle object). @@ -32,43 +31,39 @@ revlog.revlog.__init__(self, opener, indexfile) self.bundle = bundle self.basemap = {} - def chunkpositer(): - while 1: - chunk = bundle.chunk() - if not chunk: - break - pos = bundle.tell() - yield chunk, pos - len(chunk) n = len(self) - prev = None - for chunk, start in chunkpositer(): - size = len(chunk) - if size < 80: - raise util.Abort(_("invalid changegroup")) - start += 80 - size -= 80 - node, p1, p2, cs = struct.unpack("20s20s20s20s", chunk[:80]) + chain = None + while 1: + chunkdata = bundle.deltachunk(chain) + if not chunkdata: + break + node = chunkdata['node'] + p1 = chunkdata['p1'] + p2 = chunkdata['p2'] + cs = chunkdata['cs'] + deltabase = chunkdata['deltabase'] + delta = chunkdata['delta'] + + size = len(delta) + start = bundle.tell() - size + + link = linkmapper(cs) if node in self.nodemap: - prev = node + # this can happen if two branches make the same change + chain = node continue + for p in (p1, p2): if not p in self.nodemap: raise error.LookupError(p, self.indexfile, _("unknown parent")) - if linkmapper is None: - link = n - else: - link = linkmapper(cs) - - if not prev: - prev = p1 # start, size, full unc. size, base (unused), link, p1, p2, node e = (revlog.offset_type(start, 0), size, -1, -1, link, self.rev(p1), self.rev(p2), node) - self.basemap[n] = prev + self.basemap[n] = deltabase self.index.insert(-1, e) self.nodemap[node] = n - prev = node + chain = node n += 1 def inbundle(self, rev): @@ -144,7 +139,9 @@ class bundlechangelog(bundlerevlog, changelog.changelog): def __init__(self, opener, bundle): changelog.changelog.__init__(self, opener) - bundlerevlog.__init__(self, opener, self.indexfile, bundle) + linkmapper = lambda x: x + bundlerevlog.__init__(self, opener, self.indexfile, bundle, + linkmapper) class bundlemanifest(bundlerevlog, manifest.manifest): def __init__(self, opener, bundle, linkmapper): @@ -200,6 +197,8 @@ @util.propertycache def changelog(self): + # consume the header if it exists + self.bundle.changelogheader() c = bundlechangelog(self.sopener, self.bundle) self.manstart = self.bundle.tell() return c @@ -207,6 +206,8 @@ @util.propertycache def manifest(self): self.bundle.seek(self.manstart) + # consume the header if it exists + self.bundle.manifestheader() m = bundlemanifest(self.sopener, self.bundle, self.changelog.rev) self.filestart = self.bundle.tell() return m @@ -228,12 +229,13 @@ if not self.bundlefilespos: self.bundle.seek(self.filestart) while 1: - chunk = self.bundle.chunk() - if not chunk: + chunkdata = self.bundle.filelogheader() + if not chunkdata: break - self.bundlefilespos[chunk] = self.bundle.tell() + fname = chunkdata['filename'] + self.bundlefilespos[fname] = self.bundle.tell() while 1: - c = self.bundle.chunk() + c = self.bundle.deltachunk(None) if not c: break @@ -274,9 +276,9 @@ cwd = os.path.join(cwd,'') if parentpath.startswith(cwd): parentpath = parentpath[len(cwd):] - path = util.drop_scheme('file', path) - if path.startswith('bundle:'): - path = util.drop_scheme('bundle', path) + u = util.url(path) + path = u.localpath() + if u.scheme == 'bundle': s = path.split("+", 1) if len(s) == 1: repopath, bundlename = parentpath, s[0] @@ -286,27 +288,49 @@ repopath, bundlename = parentpath, path return bundlerepository(ui, repopath, bundlename) -def getremotechanges(ui, repo, other, revs=None, bundlename=None, force=False): - tmp = discovery.findcommonincoming(repo, other, heads=revs, force=force) +def getremotechanges(ui, repo, other, onlyheads=None, bundlename=None, + force=False): + '''obtains a bundle of changes incoming from other + + "onlyheads" restricts the returned changes to those reachable from the + specified heads. + "bundlename", if given, stores the bundle to this file path permanently; + otherwise it's stored to a temp file and gets deleted again when you call + the returned "cleanupfn". + "force" indicates whether to proceed on unrelated repos. + + Returns a tuple (local, csets, cleanupfn): + + "local" is a local repo from which to obtain the actual incoming changesets; it + is a bundlerepo for the obtained bundle when the original "other" is remote. + "csets" lists the incoming changeset node ids. + "cleanupfn" must be called without arguments when you're done processing the + changes; it closes both the original "other" and the one returned here. + ''' + tmp = discovery.findcommonincoming(repo, other, heads=onlyheads, force=force) common, incoming, rheads = tmp if not incoming: try: os.unlink(bundlename) - except: + except OSError: pass - return other, None, None + return other, [], other.close bundle = None + bundlerepo = None + localrepo = other if bundlename or not other.local(): # create a bundle (uncompressed if other repo is not local) - if revs is None and other.capable('changegroupsubset'): - revs = rheads + if onlyheads is None and other.capable('changegroupsubset'): + onlyheads = rheads - if revs is None: + if other.capable('getbundle'): + cg = other.getbundle('incoming', common=common, heads=onlyheads) + elif onlyheads is None: cg = other.changegroup(incoming, "incoming") else: - cg = other.changegroupsubset(incoming, revs, 'incoming') + cg = other.changegroupsubset(incoming, onlyheads, 'incoming') bundletype = other.local() and "HG10BZ" or "HG10UN" fname = bundle = changegroup.writebundle(cg, bundlename, bundletype) # keep written bundle? @@ -314,6 +338,18 @@ bundle = None if not other.local(): # use the created uncompressed bundlerepo - other = bundlerepository(ui, repo.root, fname) - return (other, incoming, bundle) + localrepo = bundlerepo = bundlerepository(ui, repo.root, fname) + # this repo contains local and other now, so filter out local again + common = repo.heads() + + csets = localrepo.changelog.findmissing(common, onlyheads) + def cleanup(): + if bundlerepo: + bundlerepo.close() + if bundle: + os.unlink(bundle) + other.close() + + return (localrepo, csets, cleanup) + diff -r 7b627923739f -r 680c3c6fcb48 mercurial/changegroup.py --- a/mercurial/changegroup.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/changegroup.py Fri May 06 14:29:53 2011 +0200 @@ -6,9 +6,12 @@ # GNU General Public License version 2 or any later version. from i18n import _ -import util +from node import nullrev +import mdiff, util import struct, os, bz2, zlib, tempfile +_BUNDLE10_DELTA_HEADER = "20s20s20s20s" + def readexactly(stream, n): '''read n bytes from stream.read and abort if less was available''' s = stream.read(n) @@ -43,21 +46,14 @@ return "" bundletypes = { - "": ("", nocompress), + "": ("", 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. "HG10UN": ("HG10UN", nocompress), "HG10BZ": ("HG10", lambda: bz2.BZ2Compressor()), "HG10GZ": ("HG10GZ", lambda: zlib.compressobj()), } -def collector(cl, mmfs, files): - # Gather information about changeset nodes going out in a bundle. - # We want to gather manifests needed and filelogs affected. - def collect(node): - c = cl.read(node) - files.update(c[3]) - mmfs.setdefault(c[0], node) - return collect - # hgweb uses this list to communicate its preferred type bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN'] @@ -135,6 +131,8 @@ return util.chunkbuffer(generator(fh)) class unbundle10(object): + deltaheader = _BUNDLE10_DELTA_HEADER + deltaheadersize = struct.calcsize(deltaheader) def __init__(self, fh, alg): self._stream = decompressor(fh, alg) self._type = alg @@ -161,19 +159,40 @@ self.callback() return l - 4 - def chunk(self): - """return the next chunk from changegroup 'source' as a string""" - l = self.chunklength() - return readexactly(self._stream, l) + def changelogheader(self): + """v10 does not have a changelog header chunk""" + return {} - def parsechunk(self): + def manifestheader(self): + """v10 does not have a manifest header chunk""" + return {} + + def filelogheader(self): + """return the header of the filelogs chunk, v10 only has the filename""" l = self.chunklength() if not l: return {} - h = readexactly(self._stream, 80) - node, p1, p2, cs = struct.unpack("20s20s20s20s", h) - data = readexactly(self._stream, l - 80) - return dict(node=node, p1=p1, p2=p2, cs=cs, data=data) + fname = readexactly(self._stream, l) + return dict(filename=fname) + + def _deltaheader(self, headertuple, prevnode): + node, p1, p2, cs = headertuple + if prevnode is None: + deltabase = p1 + else: + deltabase = prevnode + return node, p1, p2, deltabase, cs + + def deltachunk(self, prevnode): + l = self.chunklength() + if not l: + return {} + headerdata = readexactly(self._stream, self.deltaheadersize) + header = struct.unpack(self.deltaheader, headerdata) + delta = readexactly(self._stream, l - self.deltaheadersize) + node, p1, p2, deltabase, cs = self._deltaheader(header, prevnode) + return dict(node=node, p1=p1, p2=p2, cs=cs, + deltabase=deltabase, delta=delta) class headerlessfixup(object): def __init__(self, fh, h): @@ -203,3 +222,35 @@ if version != '10': raise util.Abort(_('%s: unknown bundle version %s') % (fname, version)) return unbundle10(fh, alg) + +class bundle10(object): + deltaheader = _BUNDLE10_DELTA_HEADER + def __init__(self, lookup): + self._lookup = lookup + def close(self): + return closechunk() + def fileheader(self, fname): + return chunkheader(len(fname)) + fname + def revchunk(self, revlog, rev, prev): + node = revlog.node(rev) + p1, p2 = revlog.parentrevs(rev) + base = prev + + prefix = '' + if base == nullrev: + delta = revlog.revision(node) + prefix = mdiff.trivialdiffheader(len(delta)) + else: + delta = revlog.revdiff(base, rev) + linknode = self._lookup(revlog, node) + p1n, p2n = revlog.parents(node) + basenode = revlog.node(base) + meta = self.builddeltaheader(node, p1n, p2n, basenode, linknode) + meta += prefix + l = len(meta) + len(delta) + yield chunkheader(l) + yield meta + yield delta + def builddeltaheader(self, node, p1n, p2n, basenode, linknode): + # do nothing with basenode, it is implicitly the previous one in HG10 + return struct.pack(self.deltaheader, node, p1n, p2n, linknode) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/changelog.py --- a/mercurial/changelog.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/changelog.py Fri May 06 14:29:53 2011 +0200 @@ -118,7 +118,9 @@ self.opener = self._realopener # move redirected index data back into place if self._divert: - n = self.opener(self.indexfile + ".a").name + nfile = self.opener(self.indexfile + ".a") + n = nfile.name + nfile.close() util.rename(n, n[:-2]) elif self._delaybuf: fp = self.opener(self.indexfile, 'a') @@ -185,7 +187,7 @@ try: # various tools did silly things with the time zone field. timezone = int(extra_data[0]) - except: + except ValueError: timezone = 0 extra = {} else: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/cmdutil.py --- a/mercurial/cmdutil.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/cmdutil.py Fri May 06 14:29:53 2011 +0200 @@ -8,7 +8,7 @@ from node import hex, nullid, nullrev, short from i18n import _ import os, sys, errno, re, glob, tempfile -import util, templater, patch, error, encoding, templatekw +import util, scmutil, templater, patch, error, templatekw import match as matchmod import similar, revset, subrepo @@ -72,7 +72,7 @@ return p def bail_if_changed(repo): - if repo.dirstate.parents()[1] != nullid: + if 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: @@ -91,7 +91,7 @@ if logfile == '-': message = sys.stdin.read() else: - message = open(logfile).read() + message = util.readfile(logfile) except IOError, inst: raise util.Abort(_("can't read commit message '%s': %s") % (logfile, inst.strerror)) @@ -122,12 +122,12 @@ def revpair(repo, revs): if not revs: - return repo.dirstate.parents()[0], None + return repo.dirstate.p1(), None l = revrange(repo, revs) if len(l) == 0: - return repo.dirstate.parents()[0], None + return repo.dirstate.p1(), None if len(l) == 1: return repo.lookup(l[0]), None @@ -174,7 +174,7 @@ pass # fall through to new-style queries if old-style fails - m = revset.match(spec) + m = revset.match(repo.ui, spec) for r in m(repo, range(len(repo))): if r not in seen: l.append(r) @@ -230,7 +230,7 @@ def make_file(repo, pat, node=None, total=None, seqno=None, revwidth=None, mode='wb', pathname=None): - writable = 'w' in mode or 'a' in mode + writable = mode not in ('r', 'rb') if not pat or pat == '-': fp = writable and sys.stdout or sys.stdin @@ -286,14 +286,14 @@ similarity = float(opts.get('similarity') or 0) # we'd use status here, except handling of symlinks and ignore is tricky added, unknown, deleted, removed = [], [], [], [] - audit_path = util.path_auditor(repo.root) + audit_path = scmutil.pathauditor(repo.root) m = match(repo, pats, opts) for abs in repo.walk(m): target = repo.wjoin(abs) good = True try: audit_path(abs) - except: + except (OSError, util.Abort): good = False rel = m.rel(abs) exact = m.exact(abs) @@ -429,12 +429,14 @@ # relsrc: ossep # otarget: ossep def copyfile(abssrc, relsrc, otarget, exact): - abstarget = util.canonpath(repo.root, cwd, otarget) + abstarget = scmutil.canonpath(repo.root, cwd, otarget) reltarget = repo.pathto(abstarget, cwd) target = repo.wjoin(abstarget) src = repo.wjoin(abssrc) state = repo.dirstate[abstarget] + scmutil.checkportable(ui, abstarget) + # check for collisions prevsrc = targets.get(abstarget) if prevsrc is not None: @@ -495,7 +497,7 @@ # return: function that takes hgsep and returns ossep def targetpathfn(pat, dest, srcs): if os.path.isdir(pat): - abspfx = util.canonpath(repo.root, cwd, pat) + abspfx = scmutil.canonpath(repo.root, cwd, pat) abspfx = util.localpath(abspfx) if destdirexists: striplen = len(os.path.split(abspfx)[0]) @@ -521,7 +523,7 @@ res = lambda p: os.path.join(dest, os.path.basename(util.localpath(p))) else: - abspfx = util.canonpath(repo.root, cwd, pat) + abspfx = scmutil.canonpath(repo.root, cwd, pat) if len(abspfx) < len(srcs[0][0]): # A directory. Either the target path contains the last # component of the source path or it does not. @@ -1312,9 +1314,15 @@ match.bad = lambda x, y: bad.append(x) or oldbad(x, y) names = [] wctx = repo[None] + cca = None + abort, warn = scmutil.checkportabilityalert(ui) + if abort or warn: + cca = scmutil.casecollisionauditor(ui, abort, wctx) for f in repo.walk(match): exact = match.exact(f) if exact or f not in repo.dirstate: + if cca: + cca(f) names.append(f) if ui.verbose or not exact: ui.status(_('adding %s\n') % match.rel(join(f))) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/commands.py --- a/mercurial/commands.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/commands.py Fri May 06 14:29:53 2011 +0200 @@ -5,16 +5,17 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -from node import hex, nullid, nullrev, short +from node import hex, bin, nullid, nullrev, short from lock import release from i18n import _, gettext import os, re, sys, difflib, time, tempfile -import hg, util, revlog, extensions, copies, error, bookmarks -import patch, help, mdiff, url, encoding, templatekw, discovery +import hg, scmutil, util, revlog, extensions, copies, error, bookmarks +import patch, help, url, encoding, templatekw, discovery import archival, changegroup, cmdutil, sshserver, hbisect, hgweb, hgweb.server import merge as mergemod -import minirst, revset -import dagparser +import minirst, revset, templatefilters +import dagparser, context, simplemerge +import random, setdiscovery, treediscovery, dagutil # Commands start here, listed alphabetically @@ -98,7 +99,7 @@ if opts.get('follow'): # --follow is deprecated and now just an alias for -f/--file # to mimic the behavior of Mercurial before version 1.5 - opts['file'] = 1 + opts['file'] = True datefunc = ui.quiet and util.shortdate or util.datestr getdate = util.cachefunc(lambda x: datefunc(x[0].date())) @@ -115,7 +116,7 @@ if (not opts.get('user') and not opts.get('changeset') and not opts.get('date') and not opts.get('file')): - opts['number'] = 1 + opts['number'] = True linenumber = opts.get('line_number') is not None if linenumber and (not opts.get('changeset')) and (not opts.get('number')): @@ -126,8 +127,12 @@ lastfunc = funcmap[-1] funcmap[-1] = lambda x: "%s:%s" % (lastfunc(x), x[1]) + def bad(x, y): + raise util.Abort("%s: %s" % (x, y)) + ctx = cmdutil.revsingle(repo, opts.get('rev')) m = cmdutil.match(repo, pats, opts) + m.bad = bad follow = not opts.get('no_follow') for abs in ctx.walk(m): fctx = ctx[abs] @@ -303,7 +308,8 @@ return 0 def bisect(ui, repo, rev=None, extra=None, command=None, - reset=None, good=None, bad=None, skip=None, noupdate=None): + reset=None, good=None, bad=None, skip=None, extend=None, + noupdate=None): """subdivision search of changesets This command helps to find changesets which introduce problems. To @@ -326,6 +332,17 @@ Returns 0 on success. """ + def extendbisectrange(nodes, good): + # bisect is incomplete when it ends on a merge node and + # one of the parent was not checked. + parents = repo[nodes[0]].parents() + if len(parents) > 1: + side = good and state['bad'] or state['good'] + num = len(set(i.node() for i in parents) & set(side)) + if num == 1: + return parents[0].ancestor(parents[1]) + return None + def print_result(nodes, good): displayer = cmdutil.show_changeset(ui, repo, {}) if len(nodes) == 1: @@ -335,15 +352,12 @@ else: ui.write(_("The first bad revision is:\n")) displayer.show(repo[nodes[0]]) - parents = repo[nodes[0]].parents() - if len(parents) > 1: - side = good and state['bad'] or state['good'] - num = len(set(i.node() for i in parents) & set(side)) - if num == 1: - common = parents[0].ancestor(parents[1]) - ui.write(_('Not all ancestors of this changeset have been' - ' checked.\nTo check the other ancestors, start' - ' from the common ancestor, %s.\n' % common)) + extendnode = extendbisectrange(nodes, good) + if extendnode is not None: + ui.write(_('Not all ancestors of this changeset have been' + ' checked.\nUse bisect --extend to continue the ' + 'bisection from\nthe common ancestor, %s.\n') + % extendnode) else: # multiple possible revisions if good: @@ -376,7 +390,7 @@ bad = True else: reset = True - elif extra or good + bad + skip + reset + bool(command) > 1: + elif extra or good + bad + skip + reset + extend + bool(command) > 1: raise util.Abort(_('incompatible arguments')) if reset: @@ -440,6 +454,18 @@ # actually bisect nodes, changesets, good = hbisect.bisect(repo.changelog, state) + if extend: + if not changesets: + extendnode = extendbisectrange(nodes, good) + if extendnode is not None: + ui.write(_("Extending search to changeset %d:%s\n" + % (extendnode.rev(), extendnode))) + if noupdate: + return + cmdutil.bail_if_changed(repo) + return hg.clean(repo, extendnode.node()) + raise util.Abort(_("nothing to extend")) + if changesets == 0: print_result(nodes, good) else: @@ -457,7 +483,8 @@ cmdutil.bail_if_changed(repo) return hg.clean(repo, node) -def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None): +def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, + rename=None, inactive=False): '''track a line of development with movable markers Bookmarks are pointers to certain commits that move when @@ -481,13 +508,14 @@ if rename: if rename not in marks: - raise util.Abort(_("a bookmark of this name does not exist")) + raise util.Abort(_("bookmark '%s' does not exist") % rename) if mark in marks and not force: - raise util.Abort(_("a bookmark of the same name already exists")) + raise util.Abort(_("bookmark '%s' already exists " + "(use -f to force)") % mark) if mark is None: raise util.Abort(_("new bookmark name required")) marks[mark] = marks[rename] - if repo._bookmarkcurrent == rename: + if repo._bookmarkcurrent == rename and not inactive: bookmarks.setcurrent(repo, mark) del marks[rename] bookmarks.write(repo) @@ -497,7 +525,7 @@ if mark is None: raise util.Abort(_("bookmark name required")) if mark not in marks: - raise util.Abort(_("a bookmark of this name does not exist")) + raise util.Abort(_("bookmark '%s' does not exist") % mark) if mark == repo._bookmarkcurrent: bookmarks.setcurrent(repo, None) del marks[mark] @@ -511,8 +539,12 @@ if not mark: raise util.Abort(_("bookmark names cannot consist entirely of " "whitespace")) + if inactive and mark == repo._bookmarkcurrent: + bookmarks.setcurrent(repo, None) + return if mark in marks and not force: - raise util.Abort(_("a bookmark of the same name already exists")) + raise util.Abort(_("bookmark '%s' already exists " + "(use -f to force)") % mark) if ((mark in repo.branchtags() or mark == repo.dirstate.branch()) and not force): raise util.Abort( @@ -521,7 +553,7 @@ marks[mark] = repo.lookup(rev) else: marks[mark] = repo.changectx('.').node() - if repo.changectx('.').node() == marks[mark]: + if not inactive and repo.changectx('.').node() == marks[mark]: bookmarks.setcurrent(repo, mark) bookmarks.write(repo) return @@ -570,14 +602,14 @@ """ if opts.get('clean'): - label = repo[None].parents()[0].branch() + label = repo[None].p1().branch() repo.dirstate.setbranch(label) ui.status(_('reset working directory to branch %s\n') % label) elif label: if not opts.get('force') and label in repo.branchtags(): if label not in [p.branch() for p in repo.parents()]: - raise util.Abort(_('a branch of the same name already exists' - " (use 'hg update' to switch to it)")) + raise util.Abort(_('a branch of the same name already exists'), + hint=_("use 'hg update' to switch to it")) repo.dirstate.setbranch(label) ui.status(_('marked working directory as branch %s\n') % label) else: @@ -669,49 +701,21 @@ if dest: raise util.Abort(_("--base is incompatible with specifying " "a destination")) - base = [repo.lookup(rev) for rev in base] - # create the right base - # XXX: nodesbetween / changegroup* should be "fixed" instead - o = [] - has = set((nullid,)) - for n in base: - has.update(repo.changelog.reachable(n)) - if revs: - revs = [repo.lookup(rev) for rev in revs] - visit = revs[:] - has.difference_update(visit) - else: - visit = repo.changelog.heads() - seen = {} - while visit: - n = visit.pop(0) - parents = [p for p in repo.changelog.parents(n) if p not in has] - if len(parents) == 0: - if n not in has: - o.append(n) - else: - for p in parents: - if p not in seen: - seen[p] = 1 - visit.append(p) + common = [repo.lookup(rev) for rev in base] else: dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest, opts.get('branch')) other = hg.repository(hg.remoteui(repo, opts), dest) revs, checkout = hg.addbranchrevs(repo, other, branches, revs) - if revs: - revs = [repo.lookup(rev) for rev in revs] - o = discovery.findoutgoing(repo, other, force=opts.get('force')) - - if not o: + inc = discovery.findcommonincoming(repo, other, force=opts.get('force')) + common, _anyinc, _heads = inc + + nodes = revs and map(repo.lookup, revs) or revs + cg = repo.getbundle('bundle', common=common, heads=nodes) + if not cg: ui.status(_("no changes found\n")) return 1 - if revs: - cg = repo.changegroupsubset(o, revs, 'bundle') - else: - cg = repo.changegroup(o, 'bundle') - bundletype = opts.get('type', 'bzip2').lower() btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ'} bundletype = btypes.get(bundletype) @@ -868,7 +872,12 @@ node = cmdutil.commit(ui, repo, commitfunc, pats, opts) if not node: - ui.status(_("nothing changed\n")) + stat = repo.status(match=cmdutil.match(repo, pats, opts)) + if stat[3]: + ui.status(_("nothing changed (%d missing files, see 'hg status')\n") + % len(stat[3])) + else: + ui.status(_("nothing changed\n")) return 1 ctx = repo[node] @@ -942,7 +951,7 @@ """find the ancestor revision of two revisions in a given index""" if len(args) == 3: index, rev1, rev2 = args - r = revlog.revlog(util.opener(os.getcwd(), audit=False), index) + r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), index) lookup = r.lookup elif len(args) == 2: if not repo: @@ -958,7 +967,6 @@ def debugbuilddag(ui, repo, text, mergeable_file=False, - appended_file=False, overwritten_file=False, new_file=False): """builds a repo with a given dag from scratch in the current empty repo @@ -975,8 +983,6 @@ - "/p2" is a merge of the preceding node and p2 - ":tag" defines a local tag for the preceding node - "@branch" sets the named branch for subsequent nodes - - "!command" runs the command using your shell - - "!!my command\\n" is like "!", but to the end of the line - "#...\\n" is a comment up to the end of the line Whitespace between the above elements is ignored. @@ -990,28 +996,12 @@ All string valued-elements are either strictly alphanumeric, or must be enclosed in double quotes ("..."), with "\\" as escape character. - - Note that the --overwritten-file and --appended-file options imply the - use of "HGMERGE=internal:local" during DAG buildup. """ - if not (mergeable_file or appended_file or overwritten_file or new_file): - raise util.Abort(_('need at least one of -m, -a, -o, -n')) - - if len(repo.changelog) > 0: + cl = repo.changelog + if len(cl) > 0: raise util.Abort(_('repository is not empty')) - if overwritten_file or appended_file: - # we don't want to fail in merges during buildup - os.environ['HGMERGE'] = 'internal:local' - - def writefile(fname, text, fmode="wb"): - f = open(fname, fmode) - try: - f.write(text) - finally: - f.close() - if mergeable_file: linesperrev = 2 # determine number of revs in DAG @@ -1020,58 +1010,95 @@ if type == 'n': n += 1 # make a file with k lines per rev - writefile("mf", "\n".join(str(i) for i in xrange(0, n * linesperrev)) - + "\n") - - at = -1 - atbranch = 'default' - for type, data in dagparser.parsedag(text): - if type == 'n': - ui.status('node %s\n' % str(data)) - id, ps = data - p1 = ps[0] - if p1 != at: - update(ui, repo, node=str(p1), clean=True) - at = p1 - if repo.dirstate.branch() != atbranch: - branch(ui, repo, atbranch, force=True) - if len(ps) > 1: - p2 = ps[1] - merge(ui, repo, node=p2) - - if mergeable_file: - f = open("mf", "rb+") - try: - lines = f.read().split("\n") - lines[id * linesperrev] += " r%i" % id - f.seek(0) - f.write("\n".join(lines)) - finally: - f.close() - - if appended_file: - writefile("af", "r%i\n" % id, "ab") - - if overwritten_file: - writefile("of", "r%i\n" % id) - - if new_file: - writefile("nf%i" % id, "r%i\n" % id) - - commit(ui, repo, addremove=True, message="r%i" % id, date=(id, 0)) - at = id - elif type == 'l': - id, name = data - ui.status('tag %s\n' % name) - tag(ui, repo, name, local=True) - elif type == 'a': - ui.status('branch %s\n' % data) - atbranch = data - elif type in 'cC': - r = util.system(data, cwd=repo.root) - if r: - desc, r = util.explain_exit(r) - raise util.Abort(_('%s command %s') % (data, desc)) + initialmergedlines = [str(i) for i in xrange(0, n * linesperrev)] + initialmergedlines.append("") + + tags = [] + + tr = repo.transaction("builddag") + try: + + at = -1 + atbranch = 'default' + nodeids = [] + for type, data in dagparser.parsedag(text): + if type == 'n': + ui.note('node %s\n' % str(data)) + id, ps = data + + files = [] + fctxs = {} + + p2 = None + if mergeable_file: + fn = "mf" + p1 = repo[ps[0]] + if len(ps) > 1: + p2 = repo[ps[1]] + pa = p1.ancestor(p2) + base, local, other = [x[fn].data() for x in pa, p1, p2] + m3 = simplemerge.Merge3Text(base, local, other) + ml = [l.strip() for l in m3.merge_lines()] + ml.append("") + elif at > 0: + ml = p1[fn].data().split("\n") + else: + ml = initialmergedlines + ml[id * linesperrev] += " r%i" % id + mergedtext = "\n".join(ml) + files.append(fn) + fctxs[fn] = context.memfilectx(fn, mergedtext) + + if overwritten_file: + fn = "of" + files.append(fn) + fctxs[fn] = context.memfilectx(fn, "r%i\n" % id) + + if new_file: + fn = "nf%i" % id + files.append(fn) + fctxs[fn] = context.memfilectx(fn, "r%i\n" % id) + if len(ps) > 1: + if not p2: + p2 = repo[ps[1]] + for fn in p2: + if fn.startswith("nf"): + files.append(fn) + fctxs[fn] = p2[fn] + + def fctxfn(repo, cx, path): + return fctxs.get(path) + + if len(ps) == 0 or ps[0] < 0: + pars = [None, None] + elif len(ps) == 1: + pars = [nodeids[ps[0]], None] + else: + pars = [nodeids[p] for p in ps] + cx = context.memctx(repo, pars, "r%i" % id, files, fctxfn, + date=(id, 0), + user="debugbuilddag", + extra={'branch': atbranch}) + nodeid = repo.commitctx(cx) + nodeids.append(nodeid) + at = id + elif type == 'l': + id, name = data + ui.note('tag %s\n' % name) + tags.append("%s %s\n" % (hex(repo.changelog.node(id)), name)) + elif type == 'a': + ui.note('branch %s\n' % data) + atbranch = data + tr.close() + finally: + tr.release() + + if tags: + tagsf = repo.opener("localtags", "w") + try: + tagsf.write("".join(tags)) + finally: + tagsf.close() def debugcommands(ui, cmd='', *args): """list all available commands and options""" @@ -1106,7 +1133,7 @@ def debugfsinfo(ui, path = "."): """show information detected about current filesystem""" - open('.debugfsinfo', 'w').write('') + util.writefile('.debugfsinfo', '') ui.write('exec: %s\n' % (util.checkexec(path) and 'yes' or 'no')) ui.write('symlink: %s\n' % (util.checklink(path) and 'yes' or 'no')) ui.write('case-sensitive: %s\n' % (util.checkcase('.debugfsinfo') @@ -1166,7 +1193,7 @@ Returns 0 on success. """ - for f in util.rcpath(): + for f in scmutil.rcpath(): ui.debug(_('read config from: %s\n') % f) untrusted = bool(opts.get('untrusted')) if values: @@ -1175,6 +1202,7 @@ if len(items) > 1 or items and sections: raise util.Abort(_('only one config item permitted')) for section, name, value in ui.walkconfig(untrusted=untrusted): + value = str(value).replace('\n', '\\n') sectname = section + '.' + name if values: for v in values: @@ -1191,6 +1219,90 @@ ui.configsource(section, name, untrusted)) ui.write('%s=%s\n' % (sectname, value)) +def debugknown(ui, repopath, *ids, **opts): + """test whether node ids are known to a repo + + Every ID must be a full-length hex node id string. Returns a list of 0s and 1s + indicating unknown/known. + """ + repo = hg.repository(ui, repopath) + if not repo.capable('known'): + raise util.Abort("known() not supported by target repository") + flags = repo.known([bin(s) for s in ids]) + ui.write("%s\n" % ("".join([f and "1" or "0" for f in flags]))) + +def debugbundle(ui, bundlepath, all=None, **opts): + """lists the contents of a bundle""" + f = url.open(ui, bundlepath) + try: + gen = changegroup.readbundle(f, bundlepath) + if all: + ui.write("format: id, p1, p2, cset, delta base, len(delta)\n") + + def showchunks(named): + ui.write("\n%s\n" % named) + chain = None + while 1: + chunkdata = gen.deltachunk(chain) + if not chunkdata: + break + node = chunkdata['node'] + p1 = chunkdata['p1'] + p2 = chunkdata['p2'] + cs = chunkdata['cs'] + deltabase = chunkdata['deltabase'] + delta = chunkdata['delta'] + ui.write("%s %s %s %s %s %s\n" % + (hex(node), hex(p1), hex(p2), + hex(cs), hex(deltabase), len(delta))) + chain = node + + chunkdata = gen.changelogheader() + showchunks("changelog") + chunkdata = gen.manifestheader() + showchunks("manifest") + while 1: + chunkdata = gen.filelogheader() + if not chunkdata: + break + fname = chunkdata['filename'] + showchunks(fname) + else: + chunkdata = gen.changelogheader() + chain = None + while 1: + chunkdata = gen.deltachunk(chain) + if not chunkdata: + break + node = chunkdata['node'] + ui.write("%s\n" % hex(node)) + chain = node + finally: + f.close() + +def debuggetbundle(ui, repopath, bundlepath, head=None, common=None, **opts): + """retrieves a bundle from a repo + + Every ID must be a full-length hex node id string. Saves the bundle to the + given file. + """ + repo = hg.repository(ui, repopath) + if not repo.capable('getbundle'): + raise util.Abort("getbundle() not supported by target repository") + args = {} + if common: + args['common'] = [bin(s) for s in common] + if head: + args['heads'] = [bin(s) for s in head] + bundle = repo.getbundle('debug', **args) + + bundletype = opts.get('type', 'bzip2').lower() + btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ'} + bundletype = btypes.get(bundletype) + if bundletype not in changegroup.bundletypes: + raise util.Abort(_('unknown bundle type specified with --type')) + changegroup.writebundle(bundle, bundlepath, bundletype) + def debugpushkey(ui, repopath, namespace, *keyinfo): '''access the pushkey key/value protocol @@ -1214,9 +1326,12 @@ def debugrevspec(ui, repo, expr): '''parse and apply a revision specification''' if ui.verbose: - tree = revset.parse(expr) + tree = revset.parse(expr)[0] ui.note(tree, "\n") - func = revset.match(expr) + newtree = revset.findaliases(ui, tree) + if newtree != tree: + ui.note(newtree, "\n") + func = revset.match(ui, expr) for c in func(repo, range(len(repo))): ui.write("%s\n" % c) @@ -1238,11 +1353,15 @@ finally: wlock.release() -def debugstate(ui, repo, nodates=None): +def debugstate(ui, repo, nodates=None, datesort=None): """show the contents of the current dirstate""" timestr = "" showdate = not nodates - for file_, ent in sorted(repo.dirstate._map.iteritems()): + if datesort: + keyfunc = lambda x: (x[1][3], x[0]) # sort by mtime, then by filename + else: + keyfunc = None # sort by filename + for file_, ent in sorted(repo.dirstate._map.iteritems(), key=keyfunc): if showdate: if ent[3] == -1: # Pad or slice to locale representation @@ -1280,7 +1399,7 @@ spaces = opts.get('spaces') dots = opts.get('dots') if file_: - rlog = revlog.revlog(util.opener(os.getcwd(), audit=False), file_) + rlog = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), file_) revs = set((int(r) for r in revs)) def events(): for r in rlog: @@ -1330,7 +1449,8 @@ if len(filelog): r = filelog if not r: - r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_[:-2] + ".i") + r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), + file_[:-2] + ".i") try: ui.write(r.revision(r.lookup(rev))) except KeyError: @@ -1356,6 +1476,65 @@ else: raise util.Abort(_("no ignore patterns found")) +def debugdiscovery(ui, repo, remoteurl="default", **opts): + """runs the changeset discovery protocol in isolation""" + remoteurl, branches = hg.parseurl(ui.expandpath(remoteurl), opts.get('branch')) + remote = hg.repository(hg.remoteui(repo, opts), remoteurl) + ui.status(_('comparing with %s\n') % util.hidepassword(remoteurl)) + + # make sure tests are repeatable + random.seed(12323) + + def doit(localheads, remoteheads): + if opts.get('old'): + if localheads: + raise util.Abort('cannot use localheads with old style discovery') + common, _in, hds = treediscovery.findcommonincoming(repo, remote, + force=True) + common = set(common) + if not opts.get('nonheads'): + ui.write("unpruned common: %s\n" % " ".join([short(n) + for n in common])) + dag = dagutil.revlogdag(repo.changelog) + all = dag.ancestorset(dag.internalizeall(common)) + common = dag.externalizeall(dag.headsetofconnecteds(all)) + else: + common, any, hds = setdiscovery.findcommonheads(ui, repo, remote) + common = set(common) + rheads = set(hds) + lheads = set(repo.heads()) + ui.write("common heads: %s\n" % " ".join([short(n) for n in common])) + if lheads <= common: + ui.write("local is subset\n") + elif rheads <= common: + ui.write("remote is subset\n") + + serverlogs = opts.get('serverlog') + if serverlogs: + for filename in serverlogs: + logfile = open(filename, 'r') + try: + line = logfile.readline() + while line: + parts = line.strip().split(';') + op = parts[1] + if op == 'cg': + pass + elif op == 'cgss': + doit(parts[2].split(' '), parts[3].split(' ')) + elif op == 'unb': + doit(parts[3].split(' '), parts[2].split(' ')) + line = logfile.readline() + finally: + logfile.close() + + else: + remoterevs, _checkout = hg.addbranchrevs(repo, remote, branches, + opts.get('remote_head')) + localrevs = opts.get('local_head') + doit(localrevs, remoterevs) + + def debugindex(ui, repo, file_, **opts): """dump the contents of an index file""" r = None @@ -1369,7 +1548,7 @@ raise util.Abort(_("unknown format %d") % format) if not r: - r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_) + r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), file_) if format == 0: ui.write(" rev offset length base linkrev" @@ -1402,7 +1581,7 @@ if len(filelog): r = filelog if not r: - r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_) + r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False), file_) ui.write("digraph G {\n") for i in r: node = r.node(i) @@ -1457,45 +1636,6 @@ ui.write(_(" (templates seem to have been installed incorrectly)\n")) problems += 1 - # patch - ui.status(_("Checking patch...\n")) - patchproblems = 0 - a = "1\n2\n3\n4\n" - b = "1\n2\n3\ninsert\n4\n" - fa = writetemp(a) - d = mdiff.unidiff(a, None, b, None, os.path.basename(fa), - os.path.basename(fa)) - fd = writetemp(d) - - files = {} - try: - patch.patch(fd, ui, cwd=os.path.dirname(fa), files=files) - except util.Abort, e: - ui.write(_(" patch call failed:\n")) - ui.write(" " + str(e) + "\n") - patchproblems += 1 - else: - if list(files) != [os.path.basename(fa)]: - ui.write(_(" unexpected patch output!\n")) - patchproblems += 1 - a = open(fa).read() - if a != b: - ui.write(_(" patch test failed!\n")) - patchproblems += 1 - - if patchproblems: - if ui.config('ui', 'patch'): - ui.write(_(" (Current patch tool may be incompatible with patch," - " or misconfigured. Please check your configuration" - " file)\n")) - else: - ui.write(_(" Internal patcher failure, please report this error" - " to http://mercurial.selenic.com/wiki/BugTracker\n")) - problems += patchproblems - - os.unlink(fa) - os.unlink(fd) - # editor ui.status(_("Checking commit editor...\n")) editor = ui.geteditor() @@ -1555,6 +1695,21 @@ line = fmt % (abs, m.rel(abs), m.exact(abs) and 'exact' or '') ui.write("%s\n" % line.rstrip()) +def debugwireargs(ui, repopath, *vals, **opts): + repo = hg.repository(hg.remoteui(ui, opts), repopath) + for opt in remoteopts: + del opts[opt[1]] + args = {} + for k, v in opts.iteritems(): + if v: + args[k] = v + # run twice to check that we don't mess up the stream for the next command + res1 = repo.debugwireargs(*vals, **args) + res2 = repo.debugwireargs(*vals, **args) + ui.write("%s\n" % res1) + if res1 != res2: + ui.warn("%s\n" % res2) + def diff(ui, repo, *pats, **opts): """diff repository (or selected files) @@ -1595,8 +1750,8 @@ msg = _('cannot specify --rev and --change at the same time') raise util.Abort(msg) elif change: - node2 = repo.lookup(change) - node1 = repo[node2].parents()[0].node() + node2 = cmdutil.revsingle(repo, change, None).node() + node1 = repo[node2].p1().node() else: node1, node2 = cmdutil.revpair(repo, revs) @@ -1782,6 +1937,10 @@ datefunc = ui.quiet and util.shortdate or util.datestr found = False filerevmatches = {} + def binary(): + flog = getfile(fn) + return util.binary(flog.read(ctx.filenode(fn))) + if opts.get('all'): iter = difflinestates(pstates, states) else: @@ -1808,9 +1967,12 @@ after = l.line[l.colend:] ui.write(sep.join(cols)) if before is not None: - ui.write(sep + before) - ui.write(match, label='grep.match') - ui.write(after) + if not opts.get('text') and binary(): + ui.write(sep + " Binary file matches") + else: + ui.write(sep + before) + ui.write(match, label='grep.match') + ui.write(after) ui.write(eol) found = True return found @@ -1823,7 +1985,7 @@ def prep(ctx, fns): rev = ctx.rev() - pctx = ctx.parents()[0] + pctx = ctx.p1() parent = pctx.rev() matches.setdefault(rev, {}) matches.setdefault(parent, {}) @@ -1858,7 +2020,7 @@ for ctx in cmdutil.walkchangerevs(repo, matchfn, opts, prep): rev = ctx.rev() - parent = ctx.parents()[0].rev() + parent = ctx.p1().rev() for fn in sorted(revfiles.get(rev, [])): states = matches[rev][fn] copy = copies.get(rev, {}).get(fn) @@ -1951,7 +2113,7 @@ displayer.show(ctx) displayer.close() -def help_(ui, name=None, with_version=False, unknowncmd=False): +def help_(ui, name=None, with_version=False, unknowncmd=False, full=True): """show help for a given topic or a help overview With no arguments, print a list of commands with short help messages. @@ -1962,7 +2124,7 @@ Returns 0 if successful. """ option_lists = [] - textwidth = ui.termwidth() - 2 + textwidth = min(ui.termwidth(), 80) - 2 def addglobalopts(aliases): if ui.verbose: @@ -1974,6 +2136,8 @@ if name == 'shortlist': msg = _('use "hg help" for the full list of commands ' 'or "hg -v" for details') + elif name and not full: + msg = _('use "hg help %s" to show the full help text' % name) elif aliases: msg = _('use "hg -v help%s" to show builtin aliases and ' 'global options') % (name and " " + name or "") @@ -2012,7 +2176,7 @@ ui.write('hg %s\n' % aliases[0]) # aliases - if not ui.quiet and len(aliases) > 1: + if full and not ui.quiet and len(aliases) > 1: ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:])) # description @@ -2024,7 +2188,7 @@ doc = _('shell alias for::\n\n %s') % entry[0].definition[1:] else: doc = _('alias for: hg %s\n\n%s') % (entry[0].definition, doc) - if ui.quiet: + if ui.quiet or not full: doc = doc.splitlines()[0] keep = ui.verbose and ['verbose'] or [] formatted, pruned = minirst.format(doc, textwidth, keep=keep) @@ -2141,6 +2305,8 @@ 'extensions\n')) help.addtopichook('revsets', revset.makedoc) + help.addtopichook('templates', templatekw.makedoc) + help.addtopichook('templates', templatefilters.makedoc) if name and name != 'shortlist': i = None @@ -2265,64 +2431,86 @@ hexfunc = ui.debugflag and hex or short default = not (num or id or branch or tags or bookmarks) output = [] - revs = [] + if source: source, branches = hg.parseurl(ui.expandpath(source)) repo = hg.repository(ui, source) revs, checkout = hg.addbranchrevs(repo, repo, branches, None) if not repo.local(): + if num or branch or tags: + raise util.Abort( + _("can't query remote revision number, branch, or tags")) if not rev and revs: rev = revs[0] if not rev: rev = "tip" - if num or branch or tags or bookmarks: - raise util.Abort(_("can't query remote revision number," - " branch, tags, or bookmarks")) - output = [hexfunc(repo.lookup(rev))] - elif not rev: - ctx = repo[None] - parents = ctx.parents() - changed = False - if default or id or num: - changed = util.any(repo.status()) - if default or id: - output = ["%s%s" % ('+'.join([hexfunc(p.node()) for p in parents]), - (changed) and "+" or "")] - if num: - output.append("%s%s" % ('+'.join([str(p.rev()) for p in parents]), - (changed) and "+" or "")) - else: - ctx = cmdutil.revsingle(repo, rev) + + remoterev = repo.lookup(rev) if default or id: - output = [hexfunc(ctx.node())] - if num: - output.append(str(ctx.rev())) - - if repo.local() and default and not ui.quiet: - b = ctx.branch() - if b != 'default': - output.append("(%s)" % b) - - # multiple tags for a single parent separated by '/' - t = "/".join(ctx.tags()) - if t: - output.append(t) - - # multiple bookmarks for a single parent separated by '/' - bm = '/'.join(ctx.bookmarks()) - if bm: - output.append(bm) - - if branch: - output.append(ctx.branch()) - - if tags: - output.extend(ctx.tags()) - - if bookmarks: - output.extend(ctx.bookmarks()) + output = [hexfunc(remoterev)] + + def getbms(): + bms = [] + + if 'bookmarks' in repo.listkeys('namespaces'): + hexremoterev = hex(remoterev) + bms = [bm for bm, bmr in repo.listkeys('bookmarks').iteritems() + if bmr == hexremoterev] + + return bms + + if bookmarks: + output.extend(getbms()) + elif default and not ui.quiet: + # multiple bookmarks for a single parent separated by '/' + bm = '/'.join(getbms()) + if bm: + output.append(bm) + else: + if not rev: + ctx = repo[None] + parents = ctx.parents() + changed = "" + if default or id or num: + changed = util.any(repo.status()) and "+" or "" + if default or id: + output = ["%s%s" % + ('+'.join([hexfunc(p.node()) for p in parents]), changed)] + if num: + output.append("%s%s" % + ('+'.join([str(p.rev()) for p in parents]), changed)) + else: + ctx = cmdutil.revsingle(repo, rev) + if default or id: + output = [hexfunc(ctx.node())] + if num: + output.append(str(ctx.rev())) + + if default and not ui.quiet: + b = ctx.branch() + if b != 'default': + output.append("(%s)" % b) + + # multiple tags for a single parent separated by '/' + t = '/'.join(ctx.tags()) + if t: + output.append(t) + + # multiple bookmarks for a single parent separated by '/' + bm = '/'.join(ctx.bookmarks()) + if bm: + output.append(bm) + else: + if branch: + output.append(ctx.branch()) + + if tags: + output.extend(ctx.tags()) + + if bookmarks: + output.extend(ctx.bookmarks()) ui.write("%s\n" % ' '.join(output)) @@ -2485,7 +2673,7 @@ raise util.Abort(_('no diffs found')) if msgs: - repo.opener('last-message.txt', 'wb').write('\n* * *\n'.join(msgs)) + repo.opener.write('last-message.txt', '\n* * *\n'.join(msgs)) finally: release(lock, wlock) @@ -2513,7 +2701,7 @@ if 'bookmarks' not in other.listkeys('namespaces'): ui.warn(_("remote doesn't support bookmarks\n")) return 0 - ui.status(_('comparing with %s\n') % url.hidepassword(source)) + ui.status(_('comparing with %s\n') % util.hidepassword(source)) return bookmarks.diff(ui, repo, other) ret = hg.incoming(ui, repo, source, opts) @@ -2711,7 +2899,7 @@ ``--tool`` can be used to specify the merge tool used for file merges. It overrides the HGMERGE environment variable and your - configuration files. + configuration files. See :hg:`help merge-tools` for options. If no revision is specified, the working directory's parent is a head revision, and the current branch contains exactly one other @@ -2736,28 +2924,27 @@ branch = repo[None].branch() bheads = repo.branchheads(branch) if len(bheads) > 2: - raise util.Abort(_( - 'branch \'%s\' has %d heads - ' - 'please merge with an explicit rev\n' - '(run \'hg heads .\' to see heads)') - % (branch, len(bheads))) - - parent = repo.dirstate.parents()[0] + raise util.Abort(_("branch '%s' has %d heads - " + "please merge with an explicit rev") + % (branch, len(bheads)), + hint=_("run 'hg heads .' to see heads")) + + parent = repo.dirstate.p1() if len(bheads) == 1: if len(repo.heads()) > 1: - raise util.Abort(_( - 'branch \'%s\' has one head - ' - 'please merge with an explicit rev\n' - '(run \'hg heads\' to see all heads)') - % branch) + raise util.Abort(_("branch '%s' has one head - " + "please merge with an explicit rev") + % branch, + hint=_("run 'hg heads' to see all heads")) msg = _('there is nothing to merge') if parent != repo.lookup(repo[None].branch()): msg = _('%s - use "hg update" instead') % msg raise util.Abort(msg) if parent not in bheads: - raise util.Abort(_('working dir not at a head rev - ' - 'use "hg update" or merge with an explicit rev')) + raise util.Abort(_('working directory not at a head revision'), + hint=_("use 'hg update' or merge with an " + "explicit revision")) node = parent == bheads[0] and bheads[-1] or bheads[0] else: node = cmdutil.revsingle(repo, node).node() @@ -2800,7 +2987,7 @@ if 'bookmarks' not in other.listkeys('namespaces'): ui.warn(_("remote doesn't support bookmarks\n")) return 0 - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) return bookmarks.diff(ui, other, repo) ret = hg.outgoing(ui, repo, dest, opts) @@ -2874,13 +3061,13 @@ if search: for name, path in ui.configitems("paths"): if name == search: - ui.write("%s\n" % url.hidepassword(path)) + ui.write("%s\n" % util.hidepassword(path)) return ui.warn(_("not found!\n")) return 1 else: for name, path in ui.configitems("paths"): - ui.write("%s = %s\n" % (name, url.hidepassword(path))) + ui.write("%s = %s\n" % (name, util.hidepassword(path))) def postincoming(ui, repo, modheads, optupdate, checkout): if modheads == 0: @@ -2891,7 +3078,13 @@ else: ui.status(_("not updating, since new heads added\n")) if modheads > 1: - ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n")) + currentbranchheads = len(repo.branchheads()) + if currentbranchheads == modheads: + ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n")) + elif currentbranchheads > 1: + ui.status(_("(run 'hg heads .' to see heads, 'hg merge' to merge)\n")) + else: + ui.status(_("(run 'hg heads' to see heads)\n")) else: ui.status(_("(run 'hg update' to get a working copy)\n")) @@ -2917,7 +3110,7 @@ """ source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch')) other = hg.repository(hg.remoteui(repo, opts), source) - ui.status(_('pulling from %s\n') % url.hidepassword(source)) + ui.status(_('pulling from %s\n') % util.hidepassword(source)) revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) if opts.get('bookmark'): @@ -2938,6 +3131,7 @@ raise util.Abort(err) modheads = repo.pull(other, heads=revs, force=opts.get('force')) + bookmarks.updatefromremote(ui, repo, other) if checkout: checkout = str(repo.changelog.rev(other.lookup(checkout))) repo._subtoppath = source @@ -2999,7 +3193,7 @@ dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = hg.parseurl(dest, opts.get('branch')) - ui.status(_('pushing to %s\n') % url.hidepassword(dest)) + ui.status(_('pushing to %s\n') % util.hidepassword(dest)) revs, checkout = hg.addbranchrevs(repo, repo, branches, opts.get('rev')) other = hg.repository(hg.remoteui(repo, opts), dest) if revs: @@ -3265,8 +3459,9 @@ directory, the reverted files will thus appear modified afterwards. - If a file has been deleted, it is restored. If the executable mode - of a file was changed, it is reset. + If a file has been deleted, it is restored. Files scheduled for + addition are just unscheduled and left as they are. If the + executable mode of a file was changed, it is reset. If names are given, all files matching the names are reverted. If no arguments are given, no files are reverted. @@ -3423,7 +3618,7 @@ fc = ctx[f] repo.wwrite(f, fc.data(), fc.flags()) - audit_path = util.path_auditor(repo.root) + audit_path = scmutil.pathauditor(repo.root) for f in remove[0]: if repo.dirstate[f] == 'a': repo.dirstate.forget(f) @@ -3645,7 +3840,7 @@ raise util.Abort(msg) elif change: node2 = repo.lookup(change) - node1 = repo[node2].parents()[0].node() + node1 = repo[node2].p1().node() else: node1, node2 = cmdutil.revpair(repo, revs) @@ -3817,7 +4012,7 @@ source, branches = hg.parseurl(ui.expandpath('default')) other = hg.repository(hg.remoteui(repo, {}), source) revs, checkout = hg.addbranchrevs(repo, other, branches, opts.get('rev')) - ui.debug('comparing with %s\n' % url.hidepassword(source)) + ui.debug('comparing with %s\n' % util.hidepassword(source)) repo.ui.pushbuffer() common, incoming, rheads = discovery.findcommonincoming(repo, other) repo.ui.popbuffer() @@ -3827,11 +4022,11 @@ dest, branches = hg.parseurl(ui.expandpath('default-push', 'default')) revs, checkout = hg.addbranchrevs(repo, repo, branches, None) other = hg.repository(hg.remoteui(repo, {}), dest) - ui.debug('comparing with %s\n' % url.hidepassword(dest)) + ui.debug('comparing with %s\n' % util.hidepassword(dest)) repo.ui.pushbuffer() - o = discovery.findoutgoing(repo, other) + common, _anyinc, _heads = discovery.findcommonincoming(repo, other) repo.ui.popbuffer() - o = repo.changelog.nodesbetween(o, None)[0] + o = repo.changelog.findmissing(common=common) if o: t.append(_('%d outgoing') % len(o)) if 'bookmarks' in other.listkeys('namespaces'): @@ -3888,7 +4083,7 @@ raise util.Abort(_('tag names must be unique')) for n in names: if n in ['tip', '.', 'null']: - raise util.Abort(_('the name \'%s\' is reserved') % n) + raise util.Abort(_("the name '%s' is reserved") % n) if not n: raise util.Abort(_('tag names cannot consist entirely of whitespace')) if opts.get('rev') and opts.get('remove'): @@ -3900,12 +4095,12 @@ expectedtype = opts.get('local') and 'local' or 'global' for n in names: if not repo.tagtype(n): - raise util.Abort(_('tag \'%s\' does not exist') % n) + raise util.Abort(_("tag '%s' does not exist") % n) if repo.tagtype(n) != expectedtype: if expectedtype == 'global': - raise util.Abort(_('tag \'%s\' is not a global tag') % n) + raise util.Abort(_("tag '%s' is not a global tag") % n) else: - raise util.Abort(_('tag \'%s\' is not a local tag') % n) + raise util.Abort(_("tag '%s' is not a local tag") % n) rev_ = nullid if not message: # we don't translate commit messages @@ -3913,8 +4108,8 @@ elif not opts.get('force'): for n in names: if n in repo.tags(): - raise util.Abort(_('tag \'%s\' already exists ' - '(use -f to force)') % n) + raise util.Abort(_("tag '%s' already exists " + "(use -f to force)") % n) if not opts.get('local'): p1, p2 = repo.dirstate.parents() if p2 != nullid: @@ -3955,19 +4150,16 @@ ui.write("%s\n" % t) continue - try: - hn = hexfunc(n) - r = "%5d:%s" % (repo.changelog.rev(n), hn) - except error.LookupError: - r = " ?:%s" % hn - else: - spaces = " " * (30 - encoding.colwidth(t)) - if ui.verbose: - if repo.tagtype(t) == 'local': - tagtype = " local" - else: - tagtype = "" - ui.write("%s%s %s%s\n" % (t, spaces, r, tagtype)) + hn = hexfunc(n) + r = "%5d:%s" % (repo.changelog.rev(n), hn) + spaces = " " * (30 - encoding.colwidth(t)) + + if ui.verbose: + if repo.tagtype(t) == 'local': + tagtype = " local" + else: + tagtype = "" + ui.write("%s%s %s%s\n" % (t, spaces, r, tagtype)) def tip(ui, repo, **opts): """show the tip revision @@ -3998,15 +4190,16 @@ fnames = (fname1,) + fnames lock = repo.lock() + wc = repo['.'] try: for fname in fnames: f = url.open(ui, fname) gen = changegroup.readbundle(f, fname) modheads = repo.addchangegroup(gen, 'unbundle', 'bundle:' + fname, lock=lock) + bookmarks.updatecurrentbookmark(repo, wc.node(), wc.branch()) finally: lock.release() - return postincoming(ui, repo, modheads, opts.get('update'), None) def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False): @@ -4053,7 +4246,7 @@ if rev and node: raise util.Abort(_("please specify just one revision")) - if not rev: + if rev is None or rev == '': rev = node # if we defined a bookmark, we have to remember the original bookmark name @@ -4070,7 +4263,7 @@ raise util.Abort(_("uncommitted local changes")) if date: - if rev: + if rev is not None: raise util.Abort(_("you can't specify a revision and a date")) rev = cmdutil.finddate(ui, repo, date) @@ -4269,6 +4462,7 @@ ('g', 'good', False, _('mark changeset good')), ('b', 'bad', False, _('mark changeset bad')), ('s', 'skip', False, _('skip testing changeset')), + ('e', 'extend', False, _('extend the bisect range')), ('c', 'command', '', _('use command to check changeset state'), _('CMD')), ('U', 'noupdate', False, _('do not update to target'))], @@ -4278,8 +4472,9 @@ [('f', 'force', False, _('force')), ('r', 'rev', '', _('revision'), _('REV')), ('d', 'delete', False, _('delete a given bookmark')), - ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))], - _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')), + ('m', 'rename', '', _('rename a given bookmark'), _('NAME')), + ('i', 'inactive', False, _('do not mark a new bookmark active'))], + _('hg bookmarks [-f] [-d] [-i] [-m NAME] [-r REV] [NAME]')), "branch": (branch, [('f', 'force', None, @@ -4354,11 +4549,15 @@ "debugbuilddag": (debugbuilddag, [('m', 'mergeable-file', None, _('add single file mergeable changes')), - ('a', 'appended-file', None, _('add single file all revs append to')), ('o', 'overwritten-file', None, _('add single file all revs overwrite')), ('n', 'new-file', None, _('add new file at each rev')), ], _('[OPTION]... TEXT')), + "debugbundle": + (debugbundle, + [('a', 'all', None, _('show all details')), + ], + _('FILE')), "debugcheckstate": (debugcheckstate, [], ''), "debugcommands": (debugcommands, [], _('[COMMAND]')), "debugcomplete": @@ -4378,13 +4577,29 @@ [('e', 'extended', None, _('try extended date formats'))], _('[-e] DATE [RANGE]')), "debugdata": (debugdata, [], _('FILE REV')), + "debugdiscovery": (debugdiscovery, + [('', 'old', None, + _('use old-style discovery')), + ('', 'nonheads', None, + _('use old-style discovery with non-heads included')), + ] + remoteopts, + _('[-l REV] [-r REV] [-b BRANCH]...' + ' [OTHER]')), "debugfsinfo": (debugfsinfo, [], _('[PATH]')), + "debuggetbundle": + (debuggetbundle, + [('H', 'head', [], _('id of head node'), _('ID')), + ('C', 'common', [], _('id of common node'), _('ID')), + ('t', 'type', 'bzip2', _('bundle compression type to use'), _('TYPE')), + ], + _('REPO FILE [-H|-C ID]...')), "debugignore": (debugignore, [], ''), "debugindex": (debugindex, [('f', 'format', 0, _('revlog format'), _('FORMAT'))], _('FILE')), "debugindexdot": (debugindexdot, [], _('FILE')), "debuginstall": (debuginstall, [], ''), + "debugknown": (debugknown, [], _('REPO ID...')), "debugpushkey": (debugpushkey, [], _('REPO NAMESPACE [KEY OLD NEW]')), "debugrebuildstate": (debugrebuildstate, @@ -4402,7 +4617,8 @@ (debugsetparents, [], _('REV1 [REV2]')), "debugstate": (debugstate, - [('', 'nodates', None, _('do not display the saved mtime'))], + [('', 'nodates', None, _('do not display the saved mtime')), + ('', 'datesort', None, _('sort by saved mtime'))], _('[OPTION]...')), "debugsub": (debugsub, @@ -4410,6 +4626,13 @@ _('revision to check'), _('REV'))], _('[-r REV] [REV]')), "debugwalk": (debugwalk, walkopts, _('[OPTION]... [FILE]...')), + "debugwireargs": + (debugwireargs, + [('', 'three', '', 'three'), + ('', 'four', '', 'four'), + ('', 'five', '', 'five'), + ] + remoteopts, + _('REPO [OPTIONS]... [ONE [TWO]]')), "^diff": (diff, [('r', 'rev', [], @@ -4435,6 +4658,7 @@ (grep, [('0', 'print0', None, _('end fields with NUL')), ('', 'all', None, _('print all revisions that match')), + ('a', 'text', None, _('treat all files as text')), ('f', 'follow', None, _('follow changeset history,' ' or file history across copies and renames')), @@ -4743,6 +4967,7 @@ } norepo = ("clone init version help debugcommands debugcomplete" - " debugdate debuginstall debugfsinfo debugpushkey") + " debugdate debuginstall debugfsinfo debugpushkey debugwireargs" + " debugknown debuggetbundle debugbundle") optionalrepo = ("identify paths serve showconfig debugancestor debugdag" " debugdata debugindex debugindexdot") diff -r 7b627923739f -r 680c3c6fcb48 mercurial/config.py --- a/mercurial/config.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/config.py Fri May 06 14:29:53 2011 +0200 @@ -138,5 +138,5 @@ def read(self, path, fp=None, sections=None, remap=None): if not fp: - fp = open(path) + fp = util.posixfile(path) self.parse(path, fp.read(), sections, remap, self.read) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/context.py --- a/mercurial/context.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/context.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,7 @@ from node import nullid, nullrev, short, hex from i18n import _ -import ancestor, bdiff, error, util, subrepo, patch, encoding +import ancestor, bdiff, error, util, scmutil, subrepo, patch, encoding import os, errno, stat propertycache = util.propertycache @@ -402,6 +402,15 @@ return [filectx(self._repo, p, fileid=n, filelog=l) for p, n, l in pl if n != nullid] + def p1(self): + return self.parents()[0] + + def p2(self): + p = self.parents() + if len(p) == 2: + return p[1] + return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog) + def children(self): # hard for renames c = self._filelog.children(self._filenode) @@ -652,6 +661,12 @@ return man + def __iter__(self): + d = self._repo.dirstate + for f in d: + if d[f] != 'r': + yield f + @propertycache def _status(self): return self._repo.status()[:4] @@ -792,10 +807,11 @@ try: rejected = [] for f in list: + scmutil.checkportable(ui, join(f)) p = self._repo.wjoin(f) try: st = os.lstat(p) - except: + except OSError: ui.warn(_("%s does not exist!\n") % join(f)) rejected.append(f) continue diff -r 7b627923739f -r 680c3c6fcb48 mercurial/copies.py --- a/mercurial/copies.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/copies.py Fri May 06 14:29:53 2011 +0200 @@ -93,7 +93,7 @@ return {}, {} # avoid silly behavior for parent -> working dir - if c2.node() is None and c1.node() == repo.dirstate.parents()[0]: + if c2.node() is None and c1.node() == repo.dirstate.p1(): return repo.dirstate.copies(), {} limit = _findlimit(repo, c1.rev(), c2.rev()) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/dagutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/dagutil.py Fri May 06 14:29:53 2011 +0200 @@ -0,0 +1,248 @@ +# dagutil.py - dag utilities for mercurial +# +# Copyright 2010 Benoit Boissinot +# and Peter Arrenbrecht +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from node import nullrev + + +class basedag(object): + '''generic interface for DAGs + + terms: + "ix" (short for index) identifies a nodes internally, + "id" identifies one externally. + + All params are ixs unless explicitly suffixed otherwise. + Pluralized params are lists or sets. + ''' + + def __init__(self): + self._inverse = None + + def nodeset(self): + '''set of all node idxs''' + raise NotImplementedError() + + def heads(self): + '''list of head ixs''' + raise NotImplementedError() + + def parents(self, ix): + '''list of parents ixs of ix''' + raise NotImplementedError() + + def inverse(self): + '''inverse DAG, where parents becomes children, etc.''' + raise NotImplementedError() + + def ancestorset(self, starts, stops=None): + ''' + set of all ancestors of starts (incl), but stop walk at stops (excl) + ''' + raise NotImplementedError() + + def descendantset(self, starts, stops=None): + ''' + set of all descendants of starts (incl), but stop walk at stops (excl) + ''' + return self.inverse().ancestorset(starts, stops) + + def headsetofconnecteds(self, ixs): + ''' + subset of connected list of ixs so that no node has a descendant in it + + By "connected list" we mean that if an ancestor and a descendant are in + the list, then so is at least one path connecting them. + ''' + raise NotImplementedError() + + def externalize(self, ix): + '''return a list of (or set if given a set) of node ids''' + return self._externalize(ix) + + def externalizeall(self, ixs): + '''return a list of (or set if given a set) of node ids''' + ids = self._externalizeall(ixs) + if isinstance(ixs, set): + return set(ids) + return list(ids) + + def internalize(self, id): + '''return a list of (or set if given a set) of node ixs''' + return self._internalize(id) + + def internalizeall(self, ids, filterunknown=False): + '''return a list of (or set if given a set) of node ids''' + ixs = self._internalizeall(ids, filterunknown) + if isinstance(ids, set): + return set(ixs) + return list(ixs) + + +class genericdag(basedag): + '''generic implementations for DAGs''' + + def ancestorset(self, starts, stops=None): + stops = stops and set(stops) or set() + seen = set() + pending = list(starts) + while pending: + n = pending.pop() + if n not in seen and n not in stops: + seen.add(n) + pending.extend(self.parents(n)) + return seen + + def headsetofconnecteds(self, ixs): + hds = set(ixs) + if not hds: + return hds + for n in ixs: + for p in self.parents(n): + hds.discard(p) + assert hds + return hds + + +class revlogbaseddag(basedag): + '''generic dag interface to a revlog''' + + def __init__(self, revlog, nodeset): + basedag.__init__(self) + self._revlog = revlog + self._heads = None + self._nodeset = nodeset + + def nodeset(self): + return self._nodeset + + def heads(self): + if self._heads is None: + self._heads = self._getheads() + return self._heads + + def _externalize(self, ix): + return self._revlog.index[ix][7] + def _externalizeall(self, ixs): + idx = self._revlog.index + return [idx[i][7] for i in ixs] + + def _internalize(self, id): + ix = self._revlog.rev(id) + if ix == nullrev: + raise LookupError(id, self._revlog.indexfile, _('nullid')) + return ix + def _internalizeall(self, ids, filterunknown): + rl = self._revlog + if filterunknown: + return [r for r in map(rl.nodemap.get, ids) + if r is not None and r != nullrev] + return map(self._internalize, ids) + + +class revlogdag(revlogbaseddag): + '''dag interface to a revlog''' + + def __init__(self, revlog): + revlogbaseddag.__init__(self, revlog, set(xrange(len(revlog)))) + + def _getheads(self): + return [r for r in self._revlog.headrevs() if r != nullrev] + + def parents(self, ix): + rlog = self._revlog + idx = rlog.index + revdata = idx[ix] + prev = revdata[5] + if prev != nullrev: + prev2 = revdata[6] + if prev2 == nullrev: + return [prev] + return [prev, prev2] + prev2 = revdata[6] + if prev2 != nullrev: + return [prev2] + return [] + + def inverse(self): + if self._inverse is None: + self._inverse = inverserevlogdag(self) + return self._inverse + + def ancestorset(self, starts, stops=None): + rlog = self._revlog + idx = rlog.index + stops = stops and set(stops) or set() + seen = set() + pending = list(starts) + while pending: + rev = pending.pop() + if rev not in seen and rev not in stops: + seen.add(rev) + revdata = idx[rev] + for i in [5, 6]: + prev = revdata[i] + if prev != nullrev: + pending.append(prev) + return seen + + def headsetofconnecteds(self, ixs): + if not ixs: + return set() + rlog = self._revlog + idx = rlog.index + headrevs = set(ixs) + for rev in ixs: + revdata = idx[rev] + for i in [5, 6]: + prev = revdata[i] + if prev != nullrev: + headrevs.discard(prev) + assert headrevs + return headrevs + + +class inverserevlogdag(revlogbaseddag, genericdag): + '''inverse of an existing revlog dag; see revlogdag.inverse()''' + + def __init__(self, orig): + revlogbaseddag.__init__(self, orig._revlog, orig._nodeset) + self._orig = orig + self._children = {} + self._roots = [] + self._walkfrom = len(self._revlog) - 1 + + def _walkto(self, walkto): + rev = self._walkfrom + cs = self._children + roots = self._roots + idx = self._revlog.index + while rev >= walkto: + data = idx[rev] + isroot = True + for prev in [data[5], data[6]]: # parent revs + if prev != nullrev: + cs.setdefault(prev, []).append(rev) + isroot = False + if isroot: + roots.append(rev) + rev -= 1 + self._walkfrom = rev - 1 + + def _getheads(self): + self._walkto(nullrev) + return self._roots + + def parents(self, ix): + if ix is None: + return [] + if ix <= self._walkfrom: + self._walkto(ix) + return self._children.get(ix, []) + + def inverse(self): + return self._orig diff -r 7b627923739f -r 680c3c6fcb48 mercurial/dirstate.py --- a/mercurial/dirstate.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/dirstate.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,7 @@ from node import nullid from i18n import _ -import util, ignore, osutil, parsers, encoding +import scmutil, util, ignore, osutil, parsers, encoding import struct, os, stat, errno import cStringIO @@ -49,6 +49,7 @@ self._rootdir = os.path.join(root, '') self._dirty = False self._dirtypl = False + self._lastnormaltime = None self._ui = ui @propertycache @@ -73,7 +74,7 @@ @propertycache def _branch(self): try: - return self._opener("branch").read().strip() or "default" + return self._opener.read("branch").strip() or "default" except IOError: return "default" @@ -202,6 +203,12 @@ def parents(self): return [self._validate(p) for p in self._pl] + def p1(self): + return self._validate(self._pl[0]) + + def p2(self): + return self._validate(self._pl[1]) + def branch(self): return encoding.tolocal(self._branch) @@ -213,13 +220,13 @@ if branch in ['tip', '.', 'null']: raise util.Abort(_('the name \'%s\' is reserved') % branch) self._branch = encoding.fromlocal(branch) - self._opener("branch", "w").write(self._branch + '\n') + self._opener.write("branch", self._branch + '\n') def _read(self): self._map = {} self._copymap = {} try: - st = self._opener("dirstate").read() + st = self._opener.read("dirstate") except IOError, err: if err.errno != errno.ENOENT: raise @@ -236,6 +243,7 @@ "_ignore"): if a in self.__dict__: delattr(self, a) + self._lastnormaltime = None self._dirty = False def copy(self, source, dest): @@ -261,9 +269,7 @@ def _addpath(self, f, check=False): oldstate = self[f] if check or oldstate == "r": - if '\r' in f or '\n' in f: - raise util.Abort( - _("'\\n' and '\\r' disallowed in filenames: %r") % f) + scmutil.checkfilename(f) if f in self._dirs: raise util.Abort(_('directory %r already in dirstate') % f) # shadows @@ -281,9 +287,15 @@ self._dirty = True self._addpath(f) s = os.lstat(self._join(f)) - self._map[f] = ('n', s.st_mode, s.st_size, int(s.st_mtime)) + mtime = int(s.st_mtime) + self._map[f] = ('n', s.st_mode, s.st_size, mtime) if f in self._copymap: del self._copymap[f] + if mtime > self._lastnormaltime: + # Remember the most recent modification timeslot for status(), + # to make sure we won't miss future size-preserving file content + # modifications that happen within the same timeslot. + self._lastnormaltime = mtime def normallookup(self, f): '''Mark a file normal, but possibly dirty.''' @@ -397,6 +409,7 @@ delattr(self, "_dirs") self._copymap = {} self._pl = [nullid, nullid] + self._lastnormaltime = None self._dirty = True def rebuild(self, parent, files): @@ -444,6 +457,7 @@ write(f) st.write(cs.getvalue()) st.rename() + self._lastnormaltime = None self._dirty = self._dirtypl = False def _dirignore(self, f): @@ -680,6 +694,7 @@ # lines are an expansion of "islink => checklink" # where islink means "is this a link?" and checklink # means "can we check links?". + mtime = int(st.st_mtime) if (size >= 0 and (size != st.st_size or ((mode ^ st.st_mode) & 0100 and self._checkexec)) @@ -687,9 +702,15 @@ or size == -2 # other parent or fn in self._copymap): madd(fn) - elif (time != int(st.st_mtime) + elif (mtime != time and (mode & lnkkind != lnkkind or self._checklink)): ladd(fn) + elif mtime == self._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. + # Force lookup, so we don't miss such a racy file change. + ladd(fn) elif listclean: cadd(fn) elif state == 'm': diff -r 7b627923739f -r 680c3c6fcb48 mercurial/discovery.py --- a/mercurial/discovery.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/discovery.py Fri May 06 14:29:53 2011 +0200 @@ -7,184 +7,41 @@ from node import nullid, short from i18n import _ -import util, error +import util, setdiscovery, treediscovery def findcommonincoming(repo, remote, heads=None, force=False): - """Return a tuple (common, missing roots, heads) used to identify - missing nodes from remote. - - If a list of heads is specified, return only nodes which are heads - or ancestors of these heads. - """ - m = repo.changelog.nodemap - search = [] - fetch = set() - seen = set() - seenbranch = set() - base = set() - - if not heads: - heads = remote.heads() - - if repo.changelog.tip() == nullid: - base.add(nullid) - if heads != [nullid]: - return [nullid], [nullid], list(heads) - return [nullid], [], [] - - # assume we're closer to the tip than the root - # and start by examining the heads - repo.ui.status(_("searching for changes\n")) - - unknown = [] - for h in heads: - if h not in m: - unknown.append(h) - else: - base.add(h) - - heads = unknown - if not unknown: - return list(base), [], [] - - req = set(unknown) - reqcnt = 0 + """Return a tuple (common, anyincoming, heads) used to identify the common + subset of nodes between repo and remote. - # search through remote branches - # a 'branch' here is a linear segment of history, with four parts: - # head, root, first parent, second parent - # (a branch always has two parents (or none) by definition) - unknown = remote.branches(unknown) - while unknown: - r = [] - while unknown: - n = unknown.pop(0) - if n[0] in seen: - continue + "common" is a list of (at least) the heads of the common subset. + "anyincoming" is testable as a boolean indicating if any nodes are missing + locally. If remote does not support getbundle, this actually is a list of + roots of the nodes that would be incoming, to be supplied to + changegroupsubset. No code except for pull should be relying on this fact + any longer. + "heads" is either the supplied heads, or else the remote's heads. - repo.ui.debug("examining %s:%s\n" - % (short(n[0]), short(n[1]))) - if n[0] == nullid: # found the end of the branch - pass - elif n in seenbranch: - repo.ui.debug("branch already found\n") - continue - elif n[1] and n[1] in m: # do we know the base? - repo.ui.debug("found incomplete branch %s:%s\n" - % (short(n[0]), short(n[1]))) - search.append(n[0:2]) # schedule branch range for scanning - seenbranch.add(n) - else: - if n[1] not in seen and n[1] not in fetch: - if n[2] in m and n[3] in m: - repo.ui.debug("found new changeset %s\n" % - short(n[1])) - fetch.add(n[1]) # earliest unknown - for p in n[2:4]: - if p in m: - base.add(p) # latest known - - for p in n[2:4]: - if p not in req and p not in m: - r.append(p) - req.add(p) - seen.add(n[0]) + If you pass heads and they are all known locally, the reponse lists justs + these heads in "common" and in "heads". + """ - if r: - reqcnt += 1 - repo.ui.progress(_('searching'), reqcnt, unit=_('queries')) - repo.ui.debug("request %d: %s\n" % - (reqcnt, " ".join(map(short, r)))) - for p in xrange(0, len(r), 10): - for b in remote.branches(r[p:p + 10]): - repo.ui.debug("received %s:%s\n" % - (short(b[0]), short(b[1]))) - unknown.append(b) - - # do binary search on the branches we found - while search: - newsearch = [] - reqcnt += 1 - repo.ui.progress(_('searching'), reqcnt, unit=_('queries')) - for n, l in zip(search, remote.between(search)): - l.append(n[1]) - p = n[0] - f = 1 - for i in l: - repo.ui.debug("narrowing %d:%d %s\n" % (f, len(l), short(i))) - if i in m: - if f <= 2: - repo.ui.debug("found new branch changeset %s\n" % - short(p)) - fetch.add(p) - base.add(i) - else: - repo.ui.debug("narrowed branch search to %s:%s\n" - % (short(p), short(i))) - newsearch.append((p, i)) - break - p, f = i, f * 2 - search = newsearch - - # sanity check our fetch list - for f in fetch: - if f in m: - raise error.RepoError(_("already have changeset ") - + short(f[:4])) + if not remote.capable('getbundle'): + return treediscovery.findcommonincoming(repo, remote, heads, force) - base = list(base) - if base == [nullid]: - if force: - repo.ui.warn(_("warning: repository is unrelated\n")) - else: - raise util.Abort(_("repository is unrelated")) - - repo.ui.debug("found new changesets starting at " + - " ".join([short(f) for f in fetch]) + "\n") - - repo.ui.progress(_('searching'), None) - repo.ui.debug("%d total queries\n" % reqcnt) - - return base, list(fetch), heads - -def findoutgoing(repo, remote, base=None, remoteheads=None, force=False): - """Return list of nodes that are roots of subsets not in remote + if heads: + allknown = True + nm = repo.changelog.nodemap + for h in heads: + if nm.get(h) is None: + allknown = False + break + if allknown: + return (heads, False, heads) - If base dict is specified, assume that these nodes and their parents - exist on the remote side. - If remotehead is specified, assume it is the list of the heads from - the remote repository. - """ - if base is None: - base = findcommonincoming(repo, remote, heads=remoteheads, - force=force)[0] - else: - base = list(base) - - repo.ui.debug("common changesets up to " - + " ".join(map(short, base)) + "\n") - - remain = set(repo.changelog.nodemap) - - # prune everything remote has from the tree - remain.remove(nullid) - remove = base - while remove: - n = remove.pop(0) - if n in remain: - remain.remove(n) - for p in repo.changelog.parents(n): - remove.append(p) - - # find every node whose parents have been pruned - subset = [] - # find every remote head that will get new children - for n in remain: - p1, p2 = repo.changelog.parents(n) - if p1 not in remain and p2 not in remain: - subset.append(n) - - return subset + res = setdiscovery.findcommonheads(repo.ui, repo, remote, + abortwhenunrelated=not force) + common, anyinc, srvheads = res + return (list(common), anyinc, heads or list(srvheads)) def prepush(repo, remote, force, revs, newbranch): '''Analyze the local and remote repositories and determine which @@ -200,15 +57,12 @@ changegroup is a readable file-like object whose read() returns successive changegroup chunks ready to be sent over the wire and remoteheads is the list of remote heads.''' - remoteheads = remote.heads() - common, inc, rheads = findcommonincoming(repo, remote, heads=remoteheads, - force=force) + common, inc, remoteheads = findcommonincoming(repo, remote, force=force) cl = repo.changelog - update = findoutgoing(repo, remote, common, remoteheads) - outg, bases, heads = cl.nodesbetween(update, revs) + outg = cl.findmissing(common, revs) - if not bases: + if not outg: repo.ui.status(_("no changes found\n")) return None, 1 @@ -309,8 +163,7 @@ if revs is None: # use the fast path, no race possible on push - nodes = repo.changelog.findmissing(common) - cg = repo._changegroup(nodes, 'push') + cg = repo._changegroup(outg, 'push') else: - cg = repo.changegroupsubset(update, revs, 'push') + cg = repo.getbundle('push', heads=revs, common=common) return cg, remoteheads diff -r 7b627923739f -r 680c3c6fcb48 mercurial/dispatch.py --- a/mercurial/dispatch.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/dispatch.py Fri May 06 14:29:53 2011 +0200 @@ -90,7 +90,7 @@ except error.CommandError, inst: if inst.args[0]: ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1])) - commands.help_(ui, inst.args[0]) + commands.help_(ui, inst.args[0], full=False) else: ui.warn(_("hg: %s\n") % inst.args[1]) commands.help_(ui, 'shortlist') @@ -133,7 +133,8 @@ elif hasattr(inst, "reason"): try: # usually it is in the form (errno, strerror) reason = inst.reason.args[1] - except: # it might be anything, for example a string + except (AttributeError, IndexError): + # it might be anything, for example a string reason = inst.reason ui.warn(_("abort: error: %s\n") % reason) elif hasattr(inst, "args") and inst.args[0] == errno.EPIPE: @@ -586,8 +587,8 @@ if guess and repos.count(guess) == len(repos): return _dispatch(ui, ['--repository', guess] + fullargs) if not path: - raise error.RepoError(_("There is no Mercurial repository" - " here (.hg not found)")) + raise error.RepoError(_("no repository found in %r" + " (.hg not found)") % os.getcwd()) raise args.insert(0, repo) elif rpath: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/encoding.py --- a/mercurial/encoding.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/encoding.py Fri May 06 14:29:53 2011 +0200 @@ -148,3 +148,17 @@ return sum([w(c) in wide and 2 or 1 for c in d]) return len(d) +def lower(s): + "best-effort encoding-aware case-folding of local string s" + try: + if isinstance(s, localstr): + u = s._utf8.decode("utf-8") + else: + u = s.decode(encoding, encodingmode) + + lu = u.lower() + if u == lu: + return s # preserve localstring + return lu.encode(encoding) + except UnicodeError: + return s.lower() # we don't know how to fold this except in ASCII diff -r 7b627923739f -r 680c3c6fcb48 mercurial/extensions.py --- a/mercurial/extensions.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/extensions.py Fri May 06 14:29:53 2011 +0200 @@ -11,7 +11,7 @@ _extensions = {} _order = [] -_ignore = ['hbisect', 'bookmarks'] +_ignore = ['hbisect', 'bookmarks', 'parentrevspec'] def extensions(): for name in _order: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/filelog.py --- a/mercurial/filelog.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/filelog.py Fri May 06 14:29:53 2011 +0200 @@ -6,17 +6,28 @@ # GNU General Public License version 2 or any later version. import revlog +import re +_mdre = re.compile('\1\n') def _parsemeta(text): - if not text.startswith('\1\n'): - return {} - s = text.index('\1\n', 2) - mt = text[2:s] - m = {} - for l in mt.splitlines(): + """return (metadatadict, keylist, metadatasize)""" + # text can be buffer, so we can't use .startswith or .index + if text[:2] != '\1\n': + return None, None, None + s = _mdre.search(text, 2).start() + mtext = text[2:s] + meta = {} + keys = [] + for l in mtext.splitlines(): k, v = l.split(": ", 1) - m[k] = v - return m + meta[k] = v + keys.append(k) + return meta, keys, (s + 2) + +def _packmeta(meta, keys=None): + if not keys: + keys = sorted(meta.iterkeys()) + return "".join("%s: %s\n" % (k, meta[k]) for k in keys) class filelog(revlog.revlog): def __init__(self, opener, path): @@ -32,15 +43,14 @@ def add(self, text, meta, transaction, link, p1=None, p2=None): if meta or text.startswith('\1\n'): - mt = ["%s: %s\n" % (k, v) for k, v in sorted(meta.iteritems())] - text = "\1\n%s\1\n%s" % ("".join(mt), text) + text = "\1\n%s\1\n%s" % (_packmeta(meta), text) return self.addrevision(text, transaction, link, p1, p2) def renamed(self, node): if self.parents(node)[0] != revlog.nullid: return False t = self.revision(node) - m = _parsemeta(t) + m = _parsemeta(t)[0] if m and "copy" in m: return (m["copy"], revlog.bin(m["copyrev"])) return False diff -r 7b627923739f -r 680c3c6fcb48 mercurial/filemerge.py --- a/mercurial/filemerge.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/filemerge.py Fri May 06 14:29:53 2011 +0200 @@ -113,14 +113,14 @@ def _matcheol(file, origfile): "Convert EOL markers in a file to match origfile" - tostyle = _eoltype(open(origfile, "rb").read()) + tostyle = _eoltype(util.readfile(origfile)) if tostyle: - data = open(file, "rb").read() + data = util.readfile(file) style = _eoltype(data) if style: newdata = data.replace(style, tostyle) if newdata != data: - open(file, "wb").write(newdata) + util.writefile(file, newdata) def filemerge(repo, mynode, orig, fcd, fco, fca): """perform a 3-way merge in the working directory diff -r 7b627923739f -r 680c3c6fcb48 mercurial/graphmod.py --- a/mercurial/graphmod.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/graphmod.py Fri May 06 14:29:53 2011 +0200 @@ -21,40 +21,39 @@ CHANGESET = 'C' -def revisions(repo, start, stop): +def dagwalker(repo, revs): """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples - This generator function walks through the revision history from revision - start to revision stop (which must be less than or equal to start). It - returns a tuple for each node. The node and parent ids are arbitrary - integers which identify a node in the context of the graph returned. + This generator function walks through revisions (which should be ordered + from bigger to lower). It returns a tuple for each node. The node and parent + ids are arbitrary integers which identify a node in the context of the graph + returned. """ - cur = start - while cur >= stop: - ctx = repo[cur] - parents = set([p.rev() for p in ctx.parents() if p.rev() != nullrev]) - yield (cur, CHANGESET, ctx, sorted(parents)) - cur -= 1 + if not revs: + return -def filerevs(repo, path, start, stop, limit=None): - """file cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples + cl = repo.changelog + lowestrev = min(revs) + gpcache = {} - This generator function walks through the revision history of a single - file from revision start down to revision stop. - """ - filerev = len(repo.file(path)) - 1 - rev = stop + 1 - count = 0 - while filerev >= 0 and rev > stop: - fctx = repo.filectx(path, fileid=filerev) - parents = set([f.linkrev() for f in fctx.parents() if f.path() == path]) - rev = fctx.rev() - if rev <= start: - yield (rev, CHANGESET, fctx.changectx(), sorted(parents)) - count += 1 - if count == limit: - break - filerev -= 1 + knownrevs = set(revs) + for rev in revs: + ctx = repo[rev] + parents = sorted(set([p.rev() for p in ctx.parents() + if p.rev() in knownrevs])) + mpars = [p.rev() for p in ctx.parents() if + p.rev() != nullrev and p.rev() not in parents] + + for mpar in mpars: + gp = gpcache.get(mpar) + if gp is None: + gp = gpcache[mpar] = grandparent(cl, lowestrev, revs, mpar) + if not gp: + parents.append(mpar) + else: + parents.extend(g for g in gp if g not in parents) + + yield (ctx.rev(), CHANGESET, ctx, parents) def nodes(repo, nodes): """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples @@ -120,3 +119,21 @@ # Yield and move on yield (cur, type, data, (col, color), edges) seen = next + +def grandparent(cl, lowestrev, roots, head): + """Return all ancestors of head in roots which revision is + greater or equal to lowestrev. + """ + pending = set([head]) + seen = set() + kept = set() + llowestrev = max(nullrev, lowestrev) + while pending: + r = pending.pop() + if r >= llowestrev and r not in seen: + if r in roots: + kept.add(r) + else: + pending.update([p for p in cl.parentrevs(r)]) + seen.add(r) + return sorted(kept) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hbisect.py --- a/mercurial/hbisect.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hbisect.py Fri May 06 14:29:53 2011 +0200 @@ -54,10 +54,10 @@ return badrev, None return badrev, ancestors - good = 0 + good = False badrev, ancestors = buildancestors(state['bad'], state['good']) if not ancestors: # looking for bad to good transition? - good = 1 + good = True badrev, ancestors = buildancestors(state['good'], state['bad']) bad = changelog.node(badrev) if not ancestors: # now we're confused diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help.py --- a/mercurial/help.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/help.py Fri May 06 14:29:53 2011 +0200 @@ -8,6 +8,7 @@ from i18n import gettext, _ import sys, os import extensions +import util def moduledoc(file): @@ -79,14 +80,14 @@ break path = os.path.join(docdir, topic + ".txt") - doc = gettext(open(path).read()) + doc = gettext(util.readfile(path)) for rewriter in helphooks.get(topic, []): doc = rewriter(topic, doc) return doc return loader -helptable = [ +helptable = sorted([ (["config", "hgrc"], _("Configuration Files"), loaddoc('config')), (["dates"], _("Date Formats"), loaddoc('dates')), (["patterns"], _("File Name Patterns"), loaddoc('patterns')), @@ -103,10 +104,12 @@ loaddoc('templates')), (['urls'], _('URL Paths'), loaddoc('urls')), (["extensions"], _("Using additional features"), extshelp), - (["subrepo", "subrepos"], _("Subrepositories"), loaddoc('subrepos')), - (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')), - (["glossary"], _("Glossary"), loaddoc('glossary')), -] + (["subrepo", "subrepos"], _("Subrepositories"), loaddoc('subrepos')), + (["hgweb"], _("Configuring hgweb"), loaddoc('hgweb')), + (["glossary"], _("Glossary"), loaddoc('glossary')), + (["hgignore", "ignore"], _("syntax for Mercurial ignore files"), + loaddoc('hgignore')), +]) # Map topics to lists of callable taking the current topic help and # returning the updated version @@ -115,3 +118,19 @@ def addtopichook(topic, rewriter): helphooks.setdefault(topic, []).append(rewriter) + +def makeitemsdoc(topic, doc, marker, items): + """Extract docstring from the items key to function mapping, build a + .single documentation block and use it to overwrite the marker in doc + """ + entries = [] + for name in sorted(items): + text = (items[name].__doc__ or '').rstrip() + if not text: + continue + text = gettext(text) + lines = text.splitlines() + lines[1:] = [(' ' + l.strip()) for l in lines[1:]] + entries.append('\n'.join(lines)) + entries = '\n\n'.join(entries) + return doc.replace(marker, entries) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help/dates.txt --- a/mercurial/help/dates.txt Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/help/dates.txt Fri May 06 14:29:53 2011 +0200 @@ -30,7 +30,7 @@ The log command also accepts date ranges: -- ``<{datetime}`` - at or before a given date/time -- ``>{datetime}`` - on or after a given date/time -- ``{datetime} to {datetime}`` - a date range, inclusive -- ``-{days}`` - within a given number of days of today +- ``DATE`` - on or after a given date/time +- ``DATE to DATE`` - a date range, inclusive +- ``-DAYS`` - within a given number of days of today diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help/environment.txt --- a/mercurial/help/environment.txt Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/help/environment.txt Fri May 06 14:29:53 2011 +0200 @@ -59,6 +59,14 @@ Equivalent options set via command line flags or environment variables are not overridden. +HGPLAINEXCEPT + This is a comma-separated list of features to preserve when + HGPLAIN is enabled. Currently the only value supported is "i18n", + which preserves internationalization in plain mode. + + Setting HGPLAINEXCEPT to anything (even an empty string) will + enable plain mode. + HGUSER This is the string used as the author of a commit. If not set, available values will be considered in this order: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help/hgignore.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/help/hgignore.txt Fri May 06 14:29:53 2011 +0200 @@ -0,0 +1,81 @@ +Synopsis +-------- + +The Mercurial system uses a file called ``.hgignore`` in the root +directory of a repository to control its behavior when it searches +for files that it is not currently tracking. + +Description +----------- + +The working directory of a Mercurial repository will often contain +files that should not be tracked by Mercurial. These include backup +files created by editors and build products created by compilers. +These files can be ignored by listing them in a ``.hgignore`` file in +the root of the working directory. The ``.hgignore`` file must be +created manually. It is typically put under version control, so that +the settings will propagate to other repositories with push and pull. + +An untracked file is ignored if its path relative to the repository +root directory, or any prefix path of that path, is matched against +any pattern in ``.hgignore``. + +For example, say we have an untracked file, ``file.c``, at +``a/b/file.c`` inside our repository. Mercurial will ignore ``file.c`` +if any pattern in ``.hgignore`` matches ``a/b/file.c``, ``a/b`` or ``a``. + +In addition, a Mercurial configuration file can reference a set of +per-user or global ignore files. See the +``http://www.selenic.com/mercurial/hgrc.5.html`` man page for details +of how to configure these files. Look for the "ignore" entry in the +"ui" section. + +To control Mercurial's handling of files that it manages, see the +``http://www.selenic.com/mercurial/hg.1.html`` man page. Look for +the ``-I`` and ``-X`` options. + +Syntax +------ + +An ignore file is a plain text file consisting of a list of patterns, +with one pattern per line. Empty lines are skipped. The ``#`` +character is treated as a comment character, and the ``\`` character +is treated as an escape character. + +Mercurial supports several pattern syntaxes. The default syntax used +is Python/Perl-style regular expressions. + +To change the syntax used, use a line of the following form:: + + syntax: NAME + +where ``NAME`` is one of the following: + +``regexp`` + Regular expression, Python/Perl syntax. +``glob`` + Shell-style glob. + +The chosen syntax stays in effect when parsing all patterns that +follow, until another syntax is selected. + +Neither glob nor regexp patterns are rooted. A glob-syntax pattern of +the form ``*.c`` will match a file ending in ``.c`` in any directory, +and a regexp pattern of the form ``\.c$`` will do the same. To root a +regexp pattern, start it with ``^``. + +Example +------- + +Here is an example ignore file. :: + + # use glob syntax. + syntax: glob + + *.elc + *.pyc + *~ + + # switch to regexp syntax. + syntax: regexp + ^\.pc/ diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help/revsets.txt --- a/mercurial/help/revsets.txt Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/help/revsets.txt Fri May 06 14:29:53 2011 +0200 @@ -42,11 +42,45 @@ ``x - y`` Changesets in x but not in y. + +``x^n`` + The nth parent of x, n == 0, 1, or 2. + For n == 0, x; for n == 1, the first parent of each changeset in x; + for n == 2, the second parent of changeset in x. + +``x~n`` + The nth first ancestor of x; ``x~0`` is x; ``x~3`` is ``x^^^``. + +There is a single postfix operator: + +``x^`` + Equivalent to ``x^1``, the first parent of each changeset in x. + The following predicates are supported: .. predicatesmarker +New predicates (known as "aliases") can be defined, using any combination of +existing predicates or other aliases. An alias definition looks like:: + + = + +in the ``revsetalias`` section of ``.hgrc``. Arguments of the form `$1`, `$2`, +etc. are substituted from the alias into the definition. + +For example, + +:: + + [revsetalias] + h = heads() + d($1) = sort($1, date) + rs($1, $2) = reverse(sort($1, $2)) + +defines three aliases, ``h``, ``d``, and ``rs``. ``rs(0:tip, author)`` is +exactly equivalent to ``reverse(sort(0:tip, author))``. + Command line equivalents for :hg:`log`:: -f -> ::. diff -r 7b627923739f -r 680c3c6fcb48 mercurial/help/templates.txt --- a/mercurial/help/templates.txt Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/help/templates.txt Fri May 06 14:29:53 2011 +0200 @@ -23,52 +23,7 @@ keywords depends on the exact context of the templater. These keywords are usually available for templating a log-like command: -:author: String. The unmodified author of the changeset. - -:branch: String. The name of the branch on which the changeset was - committed. - -:branches: List of strings. The name of the branch on which the - changeset was committed. Will be empty if the branch name was - default. - -:children: List of strings. The children of the changeset. - -:date: Date information. The date when the changeset was committed. - -:desc: String. The text of the changeset description. - -:diffstat: String. Statistics of changes with the following format: - "modified files: +added/-removed lines" - -:files: List of strings. All files modified, added, or removed by this - changeset. - -:file_adds: List of strings. Files added by this changeset. - -:file_copies: List of strings. Files copied in this changeset with - their sources. - -:file_copies_switch: List of strings. Like "file_copies" but displayed - only if the --copied switch is set. - -:file_mods: List of strings. Files modified by this changeset. - -:file_dels: List of strings. Files removed by this changeset. - -:node: String. The changeset identification hash, as a 40 hexadecimal - digit string. - -:parents: List of strings. The parents of the changeset. - -:rev: Integer. The repository-local changeset revision number. - -:tags: List of strings. Any tags associated with the changeset. - -:latesttag: String. Most recent global tag in the ancestors of this - changeset. - -:latesttagdistance: Integer. Longest path to the latest tag. +.. keywordsmarker The "date" keyword does not produce human-readable output. If you want to use a date in your output, you can use a filter to process @@ -82,82 +37,4 @@ List of filters: -:addbreaks: Any text. Add an XHTML "
" tag before the end of - every line except the last. - -:age: Date. Returns a human-readable date/time difference between the - given date/time and the current date/time. - -:basename: Any text. Treats the text as a path, and returns the last - component of the path after splitting by the path separator - (ignoring trailing separators). For example, "foo/bar/baz" becomes - "baz" and "foo/bar//" becomes "bar". - -:stripdir: Treat the text as path and strip a directory level, if - possible. For example, "foo" and "foo/bar" becomes "foo". - -:date: Date. Returns a date in a Unix date format, including the - timezone: "Mon Sep 04 15:13:13 2006 0700". - -:domain: Any text. Finds the first string that looks like an email - address, and extracts just the domain component. Example: ``User - `` becomes ``example.com``. - -:email: Any text. Extracts the first string that looks like an email - address. Example: ``User `` becomes - ``user@example.com``. - -:escape: Any text. Replaces the special XML/XHTML characters "&", "<" - and ">" with XML entities. - -:hex: Any text. Convert a binary Mercurial node identifier into - its long hexadecimal representation. - -:fill68: Any text. Wraps the text to fit in 68 columns. - -:fill76: Any text. Wraps the text to fit in 76 columns. - -:firstline: Any text. Returns the first line of text. - -:nonempty: Any text. Returns '(none)' if the string is empty. - -:hgdate: Date. Returns the date as a pair of numbers: "1157407993 - 25200" (Unix timestamp, timezone offset). - -:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00 - +0200". - -:isodatesec: Date. Returns the date in ISO 8601 format, including - seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date - filter. - -:localdate: Date. Converts a date to local date. - -:obfuscate: Any text. Returns the input text rendered as a sequence of - XML entities. - -:person: Any text. Returns the text before an email address. - -:rfc822date: Date. Returns a date using the same format used in email - headers: "Tue, 18 Aug 2009 13:00:13 +0200". - -:rfc3339date: Date. Returns a date using the Internet date format - specified in RFC 3339: "2009-08-18T13:00:13+02:00". - -:short: Changeset hash. Returns the short form of a changeset hash, - i.e. a 12 hexadecimal digit string. - -:shortdate: Date. Returns a date like "2006-09-18". - -:stringify: Any type. Turns the value into text by converting values into - text and concatenating them. - -:strip: Any text. Strips all leading and trailing whitespace. - -:tabindent: Any text. Returns the text, with every line except the - first starting with a tab character. - -:urlescape: Any text. Escapes all "special" characters. For example, - "foo bar" becomes "foo%20bar". - -:user: Any text. Returns the user portion of an email address. +.. filtersmarker diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hg.py --- a/mercurial/hg.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hg.py Fri May 06 14:29:53 2011 +0200 @@ -8,16 +8,16 @@ from i18n import _ from lock import release -from node import hex, nullid, nullrev, short -import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo -import lock, util, extensions, error, encoding, node -import cmdutil, discovery, url +from node import hex, nullid +import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo, bookmarks +import lock, util, extensions, error, node +import cmdutil, discovery import merge as mergemod import verify as verifymod import errno, os, shutil def _local(path): - path = util.expandpath(util.drop_scheme('file', path)) + path = util.expandpath(util.localpath(path)) return (os.path.isfile(path) and bundlerepo or localrepo) def addbranchrevs(lrepo, repo, branches, revs): @@ -51,13 +51,15 @@ revs.append(hashbranch) return revs, revs[0] -def parseurl(url, branches=None): +def parseurl(path, branches=None): '''parse url#branch, returning (url, (branch, branches))''' - if '#' not in url: - return url, (None, branches or []) - url, branch = url.split('#', 1) - return url, (branch, branches or []) + u = util.url(path) + branch = None + if u.fragment: + branch = u.fragment + u.fragment = None + return str(u), (branch, branches or []) schemes = { 'bundle': bundlerepo, @@ -69,11 +71,8 @@ } def _lookup(path): - scheme = 'file' - if path: - c = path.find(':') - if c > 0: - scheme = path[:c] + u = util.url(path) + scheme = u.scheme or 'file' thing = schemes.get(scheme) or schemes['file'] try: return thing(path) @@ -103,15 +102,6 @@ '''return default destination of clone if none is given''' return os.path.basename(os.path.normpath(source)) -def localpath(path): - if path.startswith('file://localhost/'): - return path[16:] - if path.startswith('file://'): - return path[7:] - if path.startswith('file:'): - return path[5:] - return path - def share(ui, source, dest=None, update=True): '''create a shared repository''' @@ -143,26 +133,27 @@ if not os.path.isdir(root): os.mkdir(root) - os.mkdir(roothg) + util.makedir(roothg, notindexed=True) requirements = '' try: - requirements = srcrepo.opener('requires').read() + requirements = srcrepo.opener.read('requires') except IOError, inst: if inst.errno != errno.ENOENT: raise requirements += 'shared\n' - file(os.path.join(roothg, 'requires'), 'w').write(requirements) - file(os.path.join(roothg, 'sharedpath'), 'w').write(sharedpath) + util.writefile(os.path.join(roothg, 'requires'), requirements) + util.writefile(os.path.join(roothg, 'sharedpath'), sharedpath) + + r = repository(ui, root) default = srcrepo.ui.config('paths', 'default') if default: - f = file(os.path.join(roothg, 'hgrc'), 'w') - f.write('[paths]\ndefault = %s\n' % default) - f.close() - - r = repository(ui, root) + fp = r.opener("hgrc", "w", text=True) + fp.write("[paths]\n") + fp.write("default = %s\n" % default) + fp.close() if update: r.ui.status(_("updating working directory\n")) @@ -231,8 +222,8 @@ else: dest = ui.expandpath(dest) - dest = localpath(dest) - source = localpath(source) + dest = util.localpath(dest) + source = util.localpath(source) if os.path.exists(dest): if not os.path.isdir(dest): @@ -258,7 +249,7 @@ abspath = origsource copy = False if src_repo.cancopy() and islocal(dest): - abspath = os.path.abspath(util.drop_scheme('file', origsource)) + abspath = os.path.abspath(util.localpath(origsource)) copy = not pull and not rev if copy: @@ -281,7 +272,7 @@ dir_cleanup.dir_ = hgdir try: dest_path = hgdir - os.mkdir(dest_path) + util.makedir(dest_path, notindexed=True) except OSError, inst: if inst.errno == errno.EEXIST: dir_cleanup.close() @@ -366,6 +357,21 @@ dest_repo.ui.status(_("updating to branch %s\n") % bn) _update(dest_repo, uprev) + # clone all bookmarks + if dest_repo.local() and src_repo.capable("pushkey"): + rb = src_repo.listkeys('bookmarks') + for k, n in rb.iteritems(): + try: + m = dest_repo.lookup(n) + dest_repo._bookmarks[k] = m + except error.RepoLookupError: + pass + if rb: + bookmarks.write(dest_repo) + elif src_repo.local() and dest_repo.capable("pushkey"): + for k, n in src_repo._bookmarks.iteritems(): + dest_repo.pushkey('bookmarks', k, '', hex(n)) + return src_repo, dest_repo finally: release(src_lock, dest_lock) @@ -416,19 +422,18 @@ """ source, branches = parseurl(ui.expandpath(source), opts.get('branch')) other = repository(remoteui(repo, opts), source) - ui.status(_('comparing with %s\n') % url.hidepassword(source)) + ui.status(_('comparing with %s\n') % util.hidepassword(source)) revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev')) if revs: revs = [other.lookup(rev) for rev in revs] - other, incoming, bundle = bundlerepo.getremotechanges(ui, repo, other, revs, - opts["bundle"], opts["force"]) - if incoming is None: - ui.status(_("no changes found\n")) - return subreporecurse() + other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other, + revs, opts["bundle"], opts["force"]) + try: + if not chlist: + ui.status(_("no changes found\n")) + return subreporecurse() - try: - chlist = other.changelog.nodesbetween(incoming, revs)[0] displayer = cmdutil.show_changeset(ui, other, opts, buffered) # XXX once graphlog extension makes it into core, @@ -437,10 +442,7 @@ displayer.close() finally: - if hasattr(other, 'close'): - other.close() - if bundle: - os.unlink(bundle) + cleanupfn() subreporecurse() return 0 # exit code is zero since we found incoming changes @@ -472,18 +474,19 @@ def _outgoing(ui, repo, dest, opts): dest = ui.expandpath(dest or 'default-push', dest or 'default') dest, branches = parseurl(dest, opts.get('branch')) - ui.status(_('comparing with %s\n') % url.hidepassword(dest)) + ui.status(_('comparing with %s\n') % util.hidepassword(dest)) revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev')) if revs: revs = [repo.lookup(rev) for rev in revs] other = repository(remoteui(repo, opts), dest) - o = discovery.findoutgoing(repo, other, force=opts.get('force')) + inc = discovery.findcommonincoming(repo, other, force=opts.get('force')) + common, _anyinc, _heads = inc + o = repo.changelog.findmissing(common, revs) if not o: ui.status(_("no changes found\n")) return None - - return repo.changelog.nodesbetween(o, revs)[0] + return o def outgoing(ui, repo, dest, opts): def recurse(): diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/common.py --- a/mercurial/hgweb/common.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/common.py Fri May 06 14:29:53 2011 +0200 @@ -17,12 +17,6 @@ HTTP_METHOD_NOT_ALLOWED = 405 HTTP_SERVER_ERROR = 500 -# Hooks for hgweb permission checks; extensions can add hooks here. Each hook -# is invoked like this: hook(hgweb, request, operation), where operation is -# either read, pull or push. Hooks should either raise an ErrorResponse -# exception, or just return. -# It is possible to do both authentication and authorization through this. -permhooks = [] def checkauthz(hgweb, req, op): '''Check permission for operation based on request data (including @@ -65,18 +59,43 @@ if not result: raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized') -# Add the default permhook, which provides simple authorization. -permhooks.append(checkauthz) +# Hooks for hgweb permission checks; extensions can add hooks here. +# Each hook is invoked like this: hook(hgweb, request, operation), +# where operation is either read, pull or push. Hooks should either +# raise an ErrorResponse exception, or just return. +# +# It is possible to do both authentication and authorization through +# this. +permhooks = [checkauthz] class ErrorResponse(Exception): def __init__(self, code, message=None, headers=[]): if message is None: message = _statusmessage(code) - Exception.__init__(self, code, message) + Exception.__init__(self) self.code = code self.message = message self.headers = headers + def __str__(self): + return self.message + +class continuereader(object): + def __init__(self, f, write): + self.f = f + self._write = write + self.continued = False + + def read(self, amt=-1): + if not self.continued: + self.continued = True + self._write('HTTP/1.1 100 Continue\r\n\r\n') + return self.f.read(amt) + + def __getattr__(self, attr): + if attr in ('close', 'readline', 'readlines', '__iter__'): + return getattr(self.f, attr) + raise AttributeError() def _statusmessage(code): from BaseHTTPServer import BaseHTTPRequestHandler diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/hgweb_mod.py --- a/mercurial/hgweb/hgweb_mod.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/hgweb_mod.py Fri May 06 14:29:53 2011 +0200 @@ -17,6 +17,7 @@ perms = { 'changegroup': 'pull', 'changegroupsubset': 'pull', + 'getbundle': 'pull', 'stream_out': 'pull', 'listkeys': 'pull', 'unbundle': 'push', @@ -125,7 +126,11 @@ self.check_perm(req, perms[cmd]) return protocol.call(self.repo, req, cmd) except ErrorResponse, inst: - if cmd == 'unbundle': + # A client that sends unbundle without 100-continue will + # break if we respond early. + if (cmd == 'unbundle' and + req.env.get('HTTP_EXPECT', + '').lower() != '100-continue'): req.drain() req.respond(inst, protocol.HGTYPE) return '0\n%s\n' % inst.message @@ -183,7 +188,8 @@ req.form['cmd'] = [tmpl.cache['default']] cmd = req.form['cmd'][0] - caching(self, req) # sets ETag header or raises NOT_MODIFIED + if self.configbool('web', 'cache', True): + caching(self, req) # sets ETag header or raises NOT_MODIFIED if cmd not in webcommands.__all__: msg = 'no such method: %s' % cmd raise ErrorResponse(HTTP_BAD_REQUEST, msg) @@ -228,6 +234,7 @@ port = req.env["SERVER_PORT"] port = port != default_port and (":" + port) or "" urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port) + logourl = self.config("web", "logourl", "http://mercurial.selenic.com/") staticurl = self.config("web", "staticurl") or req.url + 'static/' if not staticurl.endswith('/'): staticurl += '/' @@ -267,6 +274,7 @@ tmpl = templater.templater(mapfile, defaults={"url": req.url, + "logourl": logourl, "staticurl": staticurl, "urlbase": urlbase, "repo": self.reponame, diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/hgwebdir_mod.py --- a/mercurial/hgweb/hgwebdir_mod.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/hgwebdir_mod.py Fri May 06 14:29:53 2011 +0200 @@ -6,9 +6,9 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import os, re, time, urlparse +import os, re, time from mercurial.i18n import _ -from mercurial import ui, hg, util, templater +from mercurial import ui, hg, scmutil, util, templater from mercurial import error, encoding from common import ErrorResponse, get_mtime, staticfile, paritygen, \ get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR @@ -33,16 +33,17 @@ repos.append((prefix, root)) continue roothead = os.path.normpath(os.path.abspath(roothead)) - paths = util.walkrepos(roothead, followsym=True, recurse=recurse) + paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse) repos.extend(urlrepos(prefix, roothead, paths)) return repos def urlrepos(prefix, roothead, paths): """yield url paths and filesystem paths from a list of repo paths - >>> list(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt'])) + >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq] + >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt'])) [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')] - >>> list(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt'])) + >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt'])) [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')] """ for path in paths: @@ -76,7 +77,10 @@ if not os.path.exists(self.conf): raise util.Abort(_('config file %s not found!') % self.conf) u.readconfig(self.conf, remap=map, trust=True) - paths = u.configitems('hgweb-paths') + paths = [] + for name, ignored in u.configitems('hgweb-paths'): + for path in u.configlist('hgweb-paths', name): + paths.append((name, path)) elif isinstance(self.conf, (list, tuple)): paths = self.conf elif isinstance(self.conf, dict): @@ -85,7 +89,7 @@ repos = findrepos(paths) for prefix, root in u.configitems('collections'): prefix = util.pconvert(prefix) - for path in util.walkrepos(root, followsym=True): + for path in scmutil.walkrepos(root, followsym=True): repo = os.path.normpath(path) name = util.pconvert(repo) if name.startswith(prefix): @@ -247,6 +251,9 @@ # update time with local timezone try: r = hg.repository(self.ui, path) + except IOError: + u.warn(_('error accessing repository at %s\n') % path) + continue except error.RepoError: u.warn(_('error accessing repository at %s\n') % path) continue @@ -340,6 +347,7 @@ start = url[-1] == '?' and '&' or '?' sessionvars = webutil.sessionvars(vars, start) + logourl = config('web', 'logourl', 'http://mercurial.selenic.com/') staticurl = config('web', 'staticurl') or url + 'static/' if not staticurl.endswith('/'): staticurl += '/' @@ -349,22 +357,15 @@ "footer": footer, "motd": motd, "url": url, + "logourl": logourl, "staticurl": staticurl, "sessionvars": sessionvars}) return tmpl def updatereqenv(self, env): - def splitnetloc(netloc): - if ':' in netloc: - return netloc.split(':', 1) - else: - return (netloc, None) - if self._baseurl is not None: - urlcomp = urlparse.urlparse(self._baseurl) - host, port = splitnetloc(urlcomp[1]) - path = urlcomp[2] - env['SERVER_NAME'] = host - if port: - env['SERVER_PORT'] = port - env['SCRIPT_NAME'] = path + u = util.url(self._baseurl) + env['SERVER_NAME'] = u.host + if u.port: + env['SERVER_PORT'] = u.port + env['SCRIPT_NAME'] = '/' + u.path diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/protocol.py --- a/mercurial/hgweb/protocol.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/protocol.py Fri May 06 14:29:53 2011 +0200 @@ -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 cStringIO, zlib, sys, urllib +import cgi, cStringIO, zlib, sys, urllib from mercurial import util, wireproto from common import HTTP_OK @@ -16,18 +16,31 @@ self.req = req self.response = '' def getargs(self, args): + knownargs = self._args() data = {} keys = args.split() for k in keys: if k == '*': star = {} - for key in self.req.form.keys(): - if key not in keys: - star[key] = self.req.form[key][0] + for key in knownargs.keys(): + if key != 'cmd' and key not in keys: + star[key] = knownargs[key][0] data['*'] = star else: - data[k] = self.req.form[k][0] + data[k] = knownargs[k][0] return [data[k] for k in keys] + def _args(self): + args = self.req.form.copy() + chunks = [] + i = 1 + while 1: + h = self.req.env.get('HTTP_X_HGARG_' + str(i)) + if h is None: + break + chunks += [h] + i += 1 + args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True)) + return args def getfile(self, fp): length = int(self.req.env['CONTENT_LENGTH']) for s in util.filechunkiter(self.req, limit=length): diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/server.py --- a/mercurial/hgweb/server.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/server.py Fri May 06 14:29:53 2011 +0200 @@ -8,6 +8,7 @@ import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback from mercurial import util, error +from mercurial.hgweb import common from mercurial.i18n import _ def _splitURI(uri): @@ -58,6 +59,12 @@ def log_message(self, format, *args): self._log_any(self.server.accesslog, format, *args) + def log_request(self, code='-', size='-'): + xheaders = [h for h in self.headers.items() if h[0].startswith('x-')] + self.log_message('"%s" %s %s%s', + self.requestline, str(code), str(size), + ''.join([' %s:%s' % h for h in sorted(xheaders)])) + def do_write(self): try: self.do_hgweb() @@ -111,6 +118,9 @@ env['SERVER_PROTOCOL'] = self.request_version env['wsgi.version'] = (1, 0) env['wsgi.url_scheme'] = self.url_scheme + if env.get('HTTP_EXPECT', '').lower() == '100-continue': + self.rfile = common.continuereader(self.rfile, self.wfile.write) + env['wsgi.input'] = self.rfile env['wsgi.errors'] = _error_logger(self) env['wsgi.multithread'] = isinstance(self.server, diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/webcommands.py --- a/mercurial/hgweb/webcommands.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/webcommands.py Fri May 06 14:29:53 2011 +0200 @@ -80,7 +80,7 @@ path=webutil.up(f), text=lines(), rev=fctx.rev(), - node=hex(fctx.node()), + node=fctx.hex(), author=fctx.user(), date=fctx.date(), desc=fctx.description(), @@ -108,10 +108,11 @@ revcount = web.maxchanges if 'revcount' in req.form: revcount = int(req.form.get('revcount', [revcount])[0]) + revcount = max(revcount, 1) tmpl.defaults['sessionvars']['revcount'] = revcount lessvars = copy.copy(tmpl.defaults['sessionvars']) - lessvars['revcount'] = revcount / 2 + lessvars['revcount'] = max(revcount / 2, 1) lessvars['rev'] = query morevars = copy.copy(tmpl.defaults['sessionvars']) morevars['revcount'] = revcount * 2 @@ -220,10 +221,11 @@ revcount = shortlog and web.maxshortchanges or web.maxchanges if 'revcount' in req.form: revcount = int(req.form.get('revcount', [revcount])[0]) + revcount = max(revcount, 1) tmpl.defaults['sessionvars']['revcount'] = revcount lessvars = copy.copy(tmpl.defaults['sessionvars']) - lessvars['revcount'] = revcount / 2 + lessvars['revcount'] = max(revcount / 2, 1) morevars = copy.copy(tmpl.defaults['sessionvars']) morevars['revcount'] = revcount * 2 @@ -237,7 +239,7 @@ changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx) return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav, - node=hex(ctx.node()), rev=pos, changesets=count, + node=ctx.hex(), rev=pos, changesets=count, entries=lambda **x: changelist(limit=0,**x), latestentry=lambda **x: changelist(limit=1,**x), archives=web.archivelist("tip"), revcount=revcount, @@ -393,14 +395,11 @@ def bookmarks(web, req, tmpl): i = web.repo._bookmarks.items() - i.reverse() parity = paritygen(web.stripecount) - def entries(notip=False, limit=0, **map): + def entries(limit=0, **map): count = 0 - for k, n in i: - if notip and k == "tip": - continue + for k, n in sorted(i): if limit > 0 and count >= limit: continue count = count + 1 @@ -411,9 +410,8 @@ return tmpl("bookmarks", node=hex(web.repo.changelog.tip()), - entries=lambda **x: entries(False, 0, **x), - entriesnotip=lambda **x: entries(True, 0, **x), - latestentry=lambda **x: entries(True, 1, **x)) + entries=lambda **x: entries(0, **x), + latestentry=lambda **x: entries(1, **x)) def branches(web, req, tmpl): tips = (web.repo[n] for t, n in web.repo.branchtags().iteritems()) @@ -464,6 +462,15 @@ node=hex(n), date=web.repo[n].date()) + def bookmarks(**map): + parity = paritygen(web.stripecount) + b = web.repo._bookmarks.items() + for k, n in sorted(b)[:10]: # limit to 10 bookmarks + yield {'parity': parity.next(), + 'bookmark': k, + 'date': web.repo[n].date(), + 'node': hex(n)} + def branches(**map): parity = paritygen(web.stripecount) @@ -508,6 +515,7 @@ owner=get_contact(web.config) or "unknown", lastchange=tip.date(), tags=tagentries, + bookmarks=bookmarks, branches=branches, shortlog=changelist, node=tip.hex(), @@ -574,7 +582,7 @@ last = fnode yield {"parity": parity.next(), - "node": hex(f.node()), + "node": f.hex(), "rev": f.rev(), "author": f.user(), "desc": f.description(), @@ -590,7 +598,7 @@ annotate=annotate, path=webutil.up(f), rev=fctx.rev(), - node=hex(fctx.node()), + node=fctx.hex(), author=fctx.user(), date=fctx.date(), desc=fctx.description(), @@ -624,10 +632,11 @@ revcount = web.maxshortchanges if 'revcount' in req.form: revcount = int(req.form.get('revcount', [revcount])[0]) + revcount = max(revcount, 1) tmpl.defaults['sessionvars']['revcount'] = revcount lessvars = copy.copy(tmpl.defaults['sessionvars']) - lessvars['revcount'] = revcount / 2 + lessvars['revcount'] = max(revcount / 2, 1) morevars = copy.copy(tmpl.defaults['sessionvars']) morevars['revcount'] = revcount * 2 @@ -646,7 +655,7 @@ l.insert(0, {"parity": parity.next(), "filerev": i, "file": f, - "node": hex(iterfctx.node()), + "node": iterfctx.hex(), "author": iterfctx.user(), "date": iterfctx.date(), "rename": webutil.renamelink(iterfctx), @@ -668,7 +677,7 @@ nodefunc = lambda x: fctx.filectx(fileid=x) nav = webutil.revnavgen(end - 1, revcount, count, nodefunc) - return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav, + return tmpl("filelog", file=f, node=fctx.hex(), nav=nav, entries=lambda **x: entries(limit=0, **x), latestentry=lambda **x: entries(limit=1, **x), revcount=revcount, morevars=morevars, lessvars=lessvars) @@ -725,10 +734,11 @@ revcount = web.maxshortchanges if 'revcount' in req.form: revcount = int(req.form.get('revcount', [revcount])[0]) + revcount = max(revcount, 1) tmpl.defaults['sessionvars']['revcount'] = revcount lessvars = copy.copy(tmpl.defaults['sessionvars']) - lessvars['revcount'] = revcount / 2 + lessvars['revcount'] = max(revcount / 2, 1) morevars = copy.copy(tmpl.defaults['sessionvars']) morevars['revcount'] = revcount * 2 @@ -745,14 +755,14 @@ if rev < web.maxshortchanges: startrev = uprev - dag = graphmod.revisions(web.repo, startrev, downrev) + dag = graphmod.dagwalker(web.repo, range(startrev, downrev - 1, -1)) tree = list(graphmod.colored(dag)) canvasheight = (len(tree) + 1) * bg_height - 27 data = [] for (id, type, ctx, vtx, edges) in tree: if type != graphmod.CHANGESET: continue - node = short(ctx.node()) + node = str(ctx) age = templatefilters.age(ctx.date()) desc = templatefilters.firstline(ctx.description()) desc = cgi.escape(templatefilters.nonempty(desc)) @@ -780,8 +790,6 @@ topicname = req.form.get('node', [None])[0] if not topicname: - topic = [] - def topics(**map): for entries, summary, _ in helpmod.helptable: entries = sorted(entries, key=len) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/webutil.py --- a/mercurial/hgweb/webutil.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/webutil.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,7 @@ # GNU General Public License version 2 or any later version. import os, copy -from mercurial import match, patch, util, error, ui +from mercurial import match, patch, scmutil, error, ui from mercurial.node import hex, nullid def up(p): @@ -66,7 +66,7 @@ if len(siblings) == 1 and siblings[0].rev() == hiderev: return for s in siblings: - d = {'node': hex(s.node()), 'rev': s.rev()} + d = {'node': s.hex(), 'rev': s.rev()} d['user'] = s.user() d['date'] = s.date() d['description'] = s.description() @@ -127,7 +127,7 @@ def cleanpath(repo, path): path = path.lstrip('/') - return util.canonpath(repo.root, '', path) + return scmutil.canonpath(repo.root, '', path) def changectx(repo, req): changeid = "tip" diff -r 7b627923739f -r 680c3c6fcb48 mercurial/hgweb/wsgicgi.py --- a/mercurial/hgweb/wsgicgi.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/hgweb/wsgicgi.py Fri May 06 14:29:53 2011 +0200 @@ -10,6 +10,7 @@ import os, sys from mercurial import util +from mercurial.hgweb import common def launch(application): util.set_binary(sys.stdin) @@ -23,7 +24,11 @@ if environ['PATH_INFO'].startswith(scriptname): environ['PATH_INFO'] = environ['PATH_INFO'][len(scriptname):] - environ['wsgi.input'] = sys.stdin + stdin = sys.stdin + if environ.get('HTTP_EXPECT', '').lower() == '100-continue': + stdin = common.continuereader(stdin, sys.stdout.write) + + environ['wsgi.input'] = stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False diff -r 7b627923739f -r 680c3c6fcb48 mercurial/httprepo.py --- a/mercurial/httprepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/httprepo.py Fri May 06 14:29:53 2011 +0200 @@ -9,7 +9,7 @@ from node import nullid from i18n import _ import changegroup, statichttprepo, error, url, util, wireproto -import os, urllib, urllib2, urlparse, zlib, httplib +import os, urllib, urllib2, zlib, httplib import errno, socket def zgenerator(f): @@ -28,13 +28,13 @@ self.path = path self.caps = None self.handler = None - scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path) - if query or frag: + u = util.url(path) + if u.query or u.fragment: raise util.Abort(_('unsupported URL component: "%s"') % - (query or frag)) + (u.query or u.fragment)) # urllib cannot handle URLs with embedded user or passwd - self._url, authinfo = url.getauthinfo(path) + self._url, authinfo = u.authinfo() self.ui = ui self.ui.debug('using %s\n' % self._url) @@ -52,10 +52,13 @@ # look up capabilities only when needed + def _fetchcaps(self): + self.caps = set(self._call('capabilities').split()) + def get_caps(self): if self.caps is None: try: - self.caps = set(self._call('capabilities').split()) + self._fetchcaps() except error.RepoError: self.caps = set() self.ui.debug('capabilities: %s\n' % @@ -73,8 +76,26 @@ data = args.pop('data', None) headers = args.pop('headers', {}) self.ui.debug("sending %s command\n" % cmd) - q = {"cmd": cmd} - q.update(args) + q = [('cmd', cmd)] + headersize = 0 + if len(args) > 0: + httpheader = self.capable('httpheader') + if httpheader: + headersize = int(httpheader.split(',')[0]) + if headersize > 0: + # The headers can typically carry more data than the URL. + encargs = urllib.urlencode(sorted(args.items())) + headerfmt = 'X-HgArg-%s' + contentlen = headersize - len(headerfmt % '000' + ': \r\n') + headernum = 0 + for i in xrange(0, len(encargs), contentlen): + headernum += 1 + header = headerfmt % str(headernum) + headers[header] = encargs[i:i + contentlen] + varyheaders = [headerfmt % str(h) for h in range(1, headernum + 1)] + headers['Vary'] = ','.join(varyheaders) + else: + q += sorted(args.items()) qs = '?%s' % urllib.urlencode(q) cu = "%s%s" % (self._url, qs) req = urllib2.Request(cu, data, headers) @@ -107,18 +128,18 @@ try: proto = resp.getheader('content-type') except AttributeError: - proto = resp.headers['content-type'] + proto = resp.headers.get('content-type', '') - safeurl = url.hidepassword(self._url) + safeurl = util.hidepassword(self._url) # accept old "text/plain" and "application/hg-changegroup" for now if not (proto.startswith('application/mercurial-') or proto.startswith('text/plain') or proto.startswith('application/hg-changegroup')): - self.ui.debug("requested URL: '%s'\n" % url.hidepassword(cu)) + self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu)) raise error.RepoError( _("'%s' does not appear to be an hg repository:\n" "---%%<--- (%s)\n%s\n---%%<---\n") - % (safeurl, proto, resp.read())) + % (safeurl, proto or 'no content-type', resp.read())) if proto.startswith('application/mercurial-'): try: @@ -145,19 +166,18 @@ # have to stream bundle to a temp file because we do not have # http 1.1 chunked transfer. - type = "" types = self.capable('unbundle') - # servers older than d1b16a746db6 will send 'unbundle' as a - # boolean capability try: types = types.split(',') except AttributeError: + # servers older than d1b16a746db6 will send 'unbundle' as a + # boolean capability. They only support headerless/uncompressed + # bundles. types = [""] - if types: - for x in types: - if x in changegroup.bundletypes: - type = x - break + for x in types: + if x in changegroup.bundletypes: + type = x + break tempname = changegroup.writebundle(cg, None, type) fp = url.httpsendfile(self.ui, tempname, "rb") @@ -196,8 +216,18 @@ inst = httpsrepository(ui, path) else: inst = httprepository(ui, path) - inst.between([(nullid, nullid)]) + try: + # Try to do useful work when checking compatibility. + # Usually saves a roundtrip since we want the caps anyway. + inst._fetchcaps() + except error.RepoError: + # No luck, try older compatibility check. + inst.between([(nullid, nullid)]) return inst - except error.RepoError: - ui.note('(falling back to static-http)\n') - return statichttprepo.instance(ui, "static-" + path, create) + except error.RepoError, httpexception: + try: + r = statichttprepo.instance(ui, "static-" + path, create) + ui.note('(falling back to static-http)\n') + return r + except error.RepoError: + raise httpexception # use the original http RepoError instead diff -r 7b627923739f -r 680c3c6fcb48 mercurial/i18n.py --- a/mercurial/i18n.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/i18n.py Fri May 06 14:29:53 2011 +0200 @@ -51,7 +51,13 @@ # An unknown encoding results in a LookupError. return message -if 'HGPLAIN' in os.environ: +def _plain(): + if 'HGPLAIN' not in os.environ and 'HGPLAINEXCEPT' not in os.environ: + return False + exceptions = os.environ.get('HGPLAINEXCEPT', '').strip().split(',') + return 'i18n' not in exceptions + +if _plain(): _ = lambda message: message else: _ = gettext diff -r 7b627923739f -r 680c3c6fcb48 mercurial/localrepo.py --- a/mercurial/localrepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/localrepo.py Fri May 06 14:29:53 2011 +0200 @@ -10,18 +10,18 @@ import repo, changegroup, subrepo, discovery, pushkey import changelog, dirstate, filelog, manifest, context, bookmarks import lock, transaction, store, encoding -import util, extensions, hook, error +import scmutil, util, extensions, hook, error import match as matchmod import merge as mergemod import tags as tagsmod -import url as urlmod from lock import release import weakref, errno, os, time, inspect propertycache = util.propertycache class localrepository(repo.repository): - capabilities = set(('lookup', 'changegroupsubset', 'branchmap', 'pushkey')) - supportedformats = set(('revlogv1', 'parentdelta')) + capabilities = set(('lookup', 'changegroupsubset', 'branchmap', 'pushkey', + 'known', 'getbundle')) + supportedformats = set(('revlogv1',)) supported = supportedformats | set(('store', 'fncache', 'shared', 'dotencode')) @@ -30,9 +30,9 @@ self.root = os.path.realpath(util.expandpath(path)) self.path = os.path.join(self.root, ".hg") self.origroot = path - self.auditor = util.path_auditor(self.root, self._checknested) - self.opener = util.opener(self.path) - self.wopener = util.opener(self.root) + self.auditor = scmutil.pathauditor(self.root, self._checknested) + self.opener = scmutil.opener(self.path) + self.wopener = scmutil.opener(self.root) self.baseui = baseui self.ui = baseui.copy() @@ -46,7 +46,7 @@ if create: if not os.path.exists(path): util.makedirs(path) - os.mkdir(self.path) + util.makedir(self.path, notindexed=True) requirements = ["revlogv1"] if self.ui.configbool('format', 'usestore', True): os.mkdir(os.path.join(self.path, "store")) @@ -56,12 +56,11 @@ if self.ui.configbool('format', 'dotencode', True): requirements.append('dotencode') # create an invalid changelog - self.opener("00changelog.i", "a").write( + self.opener.append( + "00changelog.i", '\0\0\0\2' # represents revlogv2 ' dummy changelog to prevent using the old repo layout' ) - if self.ui.configbool('format', 'parentdelta', False): - requirements.append("parentdelta") else: raise error.RepoError(_("repository %s not found") % path) elif create: @@ -70,7 +69,7 @@ # find requirements requirements = set() try: - requirements = set(self.opener("requires").read().splitlines()) + requirements = set(self.opener.read("requires").splitlines()) except IOError, inst: if inst.errno != errno.ENOENT: raise @@ -80,7 +79,7 @@ self.sharedpath = self.path try: - s = os.path.realpath(self.opener("sharedpath").read()) + s = os.path.realpath(self.opener.read("sharedpath")) if not os.path.exists(s): raise error.RepoError( _('.hg/sharedpath points to nonexistent directory %s') % s) @@ -89,7 +88,7 @@ if inst.errno != errno.ENOENT: raise - self.store = store.store(requirements, self.sharedpath, util.opener) + self.store = store.store(requirements, self.sharedpath, scmutil.opener) self.spath = self.store.path self.sopener = self.store.opener self.sjoin = self.store.join @@ -116,8 +115,6 @@ def _applyrequirements(self, requirements): self.requirements = requirements self.sopener.options = {} - if 'parentdelta' in requirements: - self.sopener.options['parentdelta'] = 1 def _writerequirements(self): reqfile = self.opener("requires", "w") @@ -189,7 +186,7 @@ warned = [0] def validate(node): try: - r = self.changelog.rev(node) + self.changelog.rev(node) return node except error.LookupError: if not warned[0]: @@ -361,7 +358,12 @@ tags = {} for (name, (node, hist)) in alltags.iteritems(): if node != nullid: - tags[encoding.tolocal(name)] = node + try: + # ignore tags to unknown nodes + self.changelog.lookup(node) + tags[encoding.tolocal(name)] = node + except error.LookupError: + pass tags['tip'] = self.changelog.tip() tagtypes = dict([(encoding.tolocal(name), value) for (name, value) in tagtypes.iteritems()]) @@ -384,10 +386,7 @@ '''return a list of tags ordered by revision''' l = [] for t, n in self.tags().iteritems(): - try: - r = self.changelog.rev(n) - except: - r = -2 # sort to the beginning of the list if unknown + r = self.changelog.rev(n) l.append((r, t, n)) return [(t, n) for r, t, n in sorted(l)] @@ -506,22 +505,24 @@ bheads.extend(newnodes) if len(bheads) <= 1: continue + bheads = sorted(bheads, key=lambda x: self[x].rev()) # starting from tip means fewer passes over reachable while newnodes: latest = newnodes.pop() if latest not in bheads: continue - minbhrev = self[min([self[bh].rev() for bh in bheads])].node() + minbhrev = self[bheads[0]].node() reachable = self.changelog.reachable(latest, minbhrev) reachable.remove(latest) - bheads = [b for b in bheads if b not in reachable] + if reachable: + bheads = [b for b in bheads if b not in reachable] partial[branch] = bheads def lookup(self, key): if isinstance(key, int): return self.changelog.node(key) elif key == '.': - return self.dirstate.parents()[0] + return self.dirstate.p1() elif key == 'null': return nullid elif key == 'tip': @@ -546,7 +547,7 @@ try: if len(key) == 20: key = hex(key) - except: + except TypeError: pass raise error.RepoLookupError(_("unknown revision '%s'") % key) @@ -558,6 +559,10 @@ repo = (remote and remote.local()) and remote or self return repo[key].branch() + def known(self, nodes): + nm = self.changelog.nodemap + return [(n in nm) for n in nodes] + def local(self): return True @@ -644,7 +649,7 @@ if self._link(filename): data = os.readlink(self.wjoin(filename)) else: - data = self.wopener(filename, 'r').read() + data = self.wopener.read(filename) return self._filter(self._encodefilterpats, filename, data) def wwrite(self, filename, data, flags): @@ -652,7 +657,7 @@ if 'l' in flags: self.wopener.symlink(data, filename) else: - self.wopener(filename, 'w').write(data) + self.wopener.write(filename, data) if 'x' in flags: util.set_flags(self.wjoin(filename), False, True) @@ -671,13 +676,14 @@ # save dirstate for rollback try: - ds = self.opener("dirstate").read() + ds = self.opener.read("dirstate") except IOError: ds = "" - self.opener("journal.dirstate", "w").write(ds) - self.opener("journal.branch", "w").write( - encoding.fromlocal(self.dirstate.branch())) - self.opener("journal.desc", "w").write("%d\n%s\n" % (len(self), desc)) + self.opener.write("journal.dirstate", ds) + self.opener.write("journal.branch", + encoding.fromlocal(self.dirstate.branch())) + self.opener.write("journal.desc", + "%d\n%s\n" % (len(self), desc)) renames = [(self.sjoin("journal"), self.sjoin("undo")), (self.join("journal.dirstate"), self.join("undo.dirstate")), @@ -712,7 +718,7 @@ lock = self.lock() if os.path.exists(self.sjoin("undo")): try: - args = self.opener("undo.desc", "r").read().splitlines() + args = self.opener.read("undo.desc").splitlines() if len(args) >= 3 and self.ui.verbose: desc = _("repository tip rolled back to revision %s" " (undo %s: %s)\n") % ( @@ -733,7 +739,7 @@ util.rename(self.join('undo.bookmarks'), self.join('bookmarks')) try: - branch = self.opener("undo.branch").read() + branch = self.opener.read("undo.branch") self.dirstate.setbranch(branch) except IOError: self.ui.warn(_("named branch could not be reset, " @@ -1014,10 +1020,7 @@ raise # update bookmarks, dirstate and mergestate - parents = (p1, p2) - if p2 == nullid: - parents = (p1,) - bookmarks.update(self, parents, ret) + bookmarks.update(self, p1, ret) for f in changes[0] + changes[1]: self.dirstate.normal(f) for f in changes[2]: @@ -1038,8 +1041,6 @@ tr = lock = None removed = list(ctx.removed()) p1, p2 = ctx.p1(), ctx.p2() - m1 = p1.manifest().copy() - m2 = p2.manifest() user = ctx.user() lock = self.lock() @@ -1047,40 +1048,48 @@ tr = self.transaction("commit") trp = weakref.proxy(tr) - # check in files - new = {} - changed = [] - linkrev = len(self) - for f in sorted(ctx.modified() + ctx.added()): - self.ui.note(f + "\n") - try: - fctx = ctx[f] - new[f] = self._filecommit(fctx, m1, m2, linkrev, trp, - changed) - m1.set(f, fctx.flags()) - except OSError, inst: - self.ui.warn(_("trouble committing %s!\n") % f) - raise - except IOError, inst: - errcode = getattr(inst, 'errno', errno.ENOENT) - if error or errcode and errcode != errno.ENOENT: + if ctx.files(): + m1 = p1.manifest().copy() + m2 = p2.manifest() + + # check in files + new = {} + changed = [] + linkrev = len(self) + for f in sorted(ctx.modified() + ctx.added()): + self.ui.note(f + "\n") + try: + fctx = ctx[f] + new[f] = self._filecommit(fctx, m1, m2, linkrev, trp, + changed) + m1.set(f, fctx.flags()) + except OSError, inst: self.ui.warn(_("trouble committing %s!\n") % f) raise - else: - removed.append(f) + except IOError, inst: + errcode = getattr(inst, 'errno', errno.ENOENT) + if error or errcode and errcode != errno.ENOENT: + self.ui.warn(_("trouble committing %s!\n") % f) + raise + else: + removed.append(f) - # update manifest - m1.update(new) - removed = [f for f in sorted(removed) if f in m1 or f in m2] - drop = [f for f in removed if f in m1] - for f in drop: - del m1[f] - mn = self.manifest.add(m1, trp, linkrev, p1.manifestnode(), - p2.manifestnode(), (new, drop)) + # update manifest + m1.update(new) + removed = [f for f in sorted(removed) if f in m1 or f in m2] + drop = [f for f in removed if f in m1] + for f in drop: + del m1[f] + mn = self.manifest.add(m1, trp, linkrev, p1.manifestnode(), + p2.manifestnode(), (new, drop)) + files = changed + removed + else: + mn = p1.manifestnode() + files = [] # update changelog self.changelog.delayupdate() - n = self.changelog.add(mn, changed + removed, ctx.description(), + n = self.changelog.add(mn, files, ctx.description(), trp, p1.node(), p2.node(), user, ctx.date(), ctx.extra().copy()) p = lambda: self.changelog.writepending() and self.root or "" @@ -1221,14 +1230,15 @@ modified, added, clean = [], [], [] for fn in mf2: if fn in mf1: - if (mf1.flags(fn) != mf2.flags(fn) or - (mf1[fn] != mf2[fn] and - (mf2[fn] or ctx1[fn].cmp(ctx2[fn])))): + if (fn not in deleted and + (mf1.flags(fn) != mf2.flags(fn) or + (mf1[fn] != mf2[fn] and + (mf2[fn] or ctx1[fn].cmp(ctx2[fn]))))): modified.append(fn) elif listclean: clean.append(fn) del mf1[fn] - else: + elif fn not in deleted: added.append(fn) removed = mf1.keys() @@ -1327,13 +1337,16 @@ self.ui.status(_("no changes found\n")) result = 0 else: - if heads is None and fetch == [nullid]: + if heads is None and list(common) == [nullid]: self.ui.status(_("requesting all changes\n")) elif heads is None and remote.capable('changegroupsubset'): # issue1320, avoid a race if remote changed after discovery heads = rheads - if heads is None: + if remote.capable('getbundle'): + cg = remote.getbundle('pull', common=common, + heads=heads or rheads) + elif heads is None: cg = remote.changegroup(fetch, 'pull') elif not remote.capable('changegroupsubset'): raise util.Abort(_("partial pull cannot be done because " @@ -1346,27 +1359,6 @@ finally: lock.release() - self.ui.debug("checking for updated bookmarks\n") - rb = remote.listkeys('bookmarks') - changed = False - for k in rb.keys(): - if k in self._bookmarks: - nr, nl = rb[k], self._bookmarks[k] - if nr in self: - cr = self[nr] - cl = self[nl] - if cl.rev() >= cr.rev(): - continue - if cr in cl.descendants(): - self._bookmarks[k] = cr.node() - changed = True - self.ui.status(_("updating bookmark %s\n") % k) - else: - self.ui.warn(_("not updating divergent" - " bookmark %s\n") % k) - if changed: - bookmarks.write(self) - return result def checkpush(self, force, revs): @@ -1446,7 +1438,7 @@ for node in nodes: self.ui.debug("%s\n" % hex(node)) - def changegroupsubset(self, bases, heads, source, extranodes=None): + def changegroupsubset(self, bases, heads, source): """Compute a changegroup consisting of all the nodes that are descendents of any of the bases and ancestors of any of the heads. Return a chunkbuffer object whose read() method will return @@ -1458,214 +1450,129 @@ Another wrinkle is doing the reverse, figuring out which changeset in the changegroup a particular filenode or manifestnode belongs to. - - The caller can specify some nodes that must be included in the - changegroup using the extranodes argument. It should be a dict - where the keys are the filenames (or 1 for the manifest), and the - values are lists of (node, linknode) tuples, where node is a wanted - node and linknode is the changelog node that should be transmitted as - the linkrev. """ - - # Set up some initial variables - # Make it easy to refer to self.changelog cl = self.changelog - # Compute the list of changesets in this changegroup. - # Some bases may turn out to be superfluous, and some heads may be - # too. nodesbetween will return the minimal set of bases and heads - # necessary to re-create the changegroup. if not bases: bases = [nullid] - msng_cl_lst, bases, heads = cl.nodesbetween(bases, heads) + csets, bases, heads = cl.nodesbetween(bases, heads) + # We assume that all ancestors of bases are known + common = set(cl.ancestors(*[cl.rev(n) for n in bases])) + return self._changegroupsubset(common, csets, heads, source) + + def getbundle(self, source, heads=None, common=None): + """Like changegroupsubset, but returns the set difference between the + ancestors of heads and the ancestors common. + + If heads is None, use the local heads. If common is None, use [nullid]. - if extranodes is None: - # can we go through the fast path ? - heads.sort() - allheads = self.heads() - allheads.sort() - if heads == allheads: - return self._changegroup(msng_cl_lst, source) + The nodes in common might not all be known locally due to the way the + current discovery protocol works. + """ + cl = self.changelog + if common: + nm = cl.nodemap + common = [n for n in common if n in nm] + else: + common = [nullid] + if not heads: + heads = cl.heads() + common, missing = cl.findcommonmissing(common, heads) + if not missing: + return None + return self._changegroupsubset(common, missing, heads, source) + + def _changegroupsubset(self, commonrevs, csets, heads, source): + + cl = self.changelog + mf = self.manifest + mfs = {} # needed manifests + fnodes = {} # needed file nodes + changedfiles = set() + fstate = ['', {}] + count = [0] + + # can we go through the fast path ? + heads.sort() + if heads == sorted(self.heads()): + return self._changegroup(csets, source) # slow path self.hook('preoutgoing', throw=True, source=source) - - self.changegroupinfo(msng_cl_lst, source) - - # We assume that all ancestors of bases are known - commonrevs = set(cl.ancestors(*[cl.rev(n) for n in bases])) + self.changegroupinfo(csets, source) - # Make it easy to refer to self.manifest - mnfst = self.manifest - # We don't know which manifests are missing yet - msng_mnfst_set = {} - # Nor do we know which filenodes are missing. - msng_filenode_set = {} - - # A changeset always belongs to itself, so the changenode lookup - # function for a changenode is identity. - def identity(x): - return x + # filter any nodes that claim to be part of the known set + def prune(revlog, missing): + for n in missing: + if revlog.linkrev(revlog.rev(n)) not in commonrevs: + yield n - # A function generating function that sets up the initial environment - # the inner function. - def filenode_collector(changedfiles): - # This gathers information from each manifestnode included in the - # changegroup about which filenodes the manifest node references - # so we can include those in the changegroup too. - # - # It also remembers which changenode each filenode belongs to. It - # does this by assuming the a filenode belongs to the changenode - # the first manifest that references it belongs to. - def collect_msng_filenodes(mnfstnode): - r = mnfst.rev(mnfstnode) - if mnfst.deltaparent(r) in mnfst.parentrevs(r): - # If the previous rev is one of the parents, - # we only need to see a diff. - deltamf = mnfst.readdelta(mnfstnode) - # For each line in the delta - for f, fnode in deltamf.iteritems(): - # And if the file is in the list of files we care - # about. - if f in changedfiles: - # Get the changenode this manifest belongs to - clnode = msng_mnfst_set[mnfstnode] - # Create the set of filenodes for the file if - # there isn't one already. - ndset = msng_filenode_set.setdefault(f, {}) - # And set the filenode's changelog node to the - # manifest's if it hasn't been set already. - ndset.setdefault(fnode, clnode) - else: - # Otherwise we need a full manifest. - m = mnfst.read(mnfstnode) - # For every file in we care about. - for f in changedfiles: - fnode = m.get(f, None) - # If it's in the manifest - if fnode is not None: - # See comments above. - clnode = msng_mnfst_set[mnfstnode] - ndset = msng_filenode_set.setdefault(f, {}) - ndset.setdefault(fnode, clnode) - return collect_msng_filenodes + def lookup(revlog, x): + if revlog == cl: + c = cl.read(x) + changedfiles.update(c[3]) + mfs.setdefault(c[0], x) + count[0] += 1 + self.ui.progress(_('bundling'), count[0], unit=_('changesets')) + return x + elif revlog == mf: + clnode = mfs[x] + mdata = mf.readfast(x) + for f in changedfiles: + if f in mdata: + fnodes.setdefault(f, {}).setdefault(mdata[f], clnode) + count[0] += 1 + self.ui.progress(_('bundling'), count[0], + unit=_('manifests'), total=len(mfs)) + return mfs[x] + else: + self.ui.progress( + _('bundling'), count[0], item=fstate[0], + unit=_('files'), total=len(changedfiles)) + return fstate[1][x] - # If we determine that a particular file or manifest node must be a - # node that the recipient of the changegroup will already have, we can - # also assume the recipient will have all the parents. This function - # prunes them from the set of missing nodes. - def prune(revlog, missingnodes): - hasset = set() - # If a 'missing' filenode thinks it belongs to a changenode we - # assume the recipient must have, then the recipient must have - # that filenode. - for n in missingnodes: - clrev = revlog.linkrev(revlog.rev(n)) - if clrev in commonrevs: - hasset.add(n) - for n in hasset: - missingnodes.pop(n, None) - for r in revlog.ancestors(*[revlog.rev(n) for n in hasset]): - missingnodes.pop(revlog.node(r), None) + bundler = changegroup.bundle10(lookup) - # Add the nodes that were explicitly requested. - def add_extra_nodes(name, nodes): - if not extranodes or name not in extranodes: - return - - for node, linknode in extranodes[name]: - if node not in nodes: - nodes[node] = linknode - - # Now that we have all theses utility functions to help out and - # logically divide up the task, generate the group. def gengroup(): - # The set of changed files starts empty. - changedfiles = set() - collect = changegroup.collector(cl, msng_mnfst_set, changedfiles) - # Create a changenode group generator that will call our functions # back to lookup the owning changenode and collect information. - group = cl.group(msng_cl_lst, identity, collect) - for cnt, chnk in enumerate(group): - yield chnk - # revlog.group yields three entries per node, so - # dividing by 3 gives an approximation of how many - # nodes have been processed. - self.ui.progress(_('bundling'), cnt / 3, - unit=_('changesets')) - changecount = cnt / 3 + for chunk in cl.group(csets, bundler): + yield chunk self.ui.progress(_('bundling'), None) - prune(mnfst, msng_mnfst_set) - add_extra_nodes(1, msng_mnfst_set) - msng_mnfst_lst = msng_mnfst_set.keys() - # Sort the manifestnodes by revision number. - msng_mnfst_lst.sort(key=mnfst.rev) # Create a generator for the manifestnodes that calls our lookup # and data collection functions back. - group = mnfst.group(msng_mnfst_lst, - lambda mnode: msng_mnfst_set[mnode], - filenode_collector(changedfiles)) - efiles = {} - for cnt, chnk in enumerate(group): - if cnt % 3 == 1: - mnode = chnk[:20] - efiles.update(mnfst.readdelta(mnode)) - yield chnk - # see above comment for why we divide by 3 - self.ui.progress(_('bundling'), cnt / 3, - unit=_('manifests'), total=changecount) + count[0] = 0 + for chunk in mf.group(prune(mf, mfs), bundler): + yield chunk self.ui.progress(_('bundling'), None) - efiles = len(efiles) - # These are no longer needed, dereference and toss the memory for - # them. - msng_mnfst_lst = None - msng_mnfst_set.clear() + mfs.clear() - if extranodes: - for fname in extranodes: - if isinstance(fname, int): - continue - msng_filenode_set.setdefault(fname, {}) - changedfiles.add(fname) # Go through all our files in order sorted by name. - for idx, fname in enumerate(sorted(changedfiles)): + count[0] = 0 + for fname in sorted(changedfiles): filerevlog = self.file(fname) if not len(filerevlog): raise util.Abort(_("empty or missing revlog for %s") % fname) - # Toss out the filenodes that the recipient isn't really - # missing. - missingfnodes = msng_filenode_set.pop(fname, {}) - prune(filerevlog, missingfnodes) - add_extra_nodes(fname, missingfnodes) - # If any filenodes are left, generate the group for them, - # otherwise don't bother. - if missingfnodes: - yield changegroup.chunkheader(len(fname)) - yield fname - # Sort the filenodes by their revision # (topological order) - nodeiter = list(missingfnodes) - nodeiter.sort(key=filerevlog.rev) - # Create a group generator and only pass in a changenode - # lookup function as we need to collect no information - # from filenodes. - group = filerevlog.group(nodeiter, - lambda fnode: missingfnodes[fnode]) - for chnk in group: - # even though we print the same progress on - # most loop iterations, put the progress call - # here so that time estimates (if any) can be updated - self.ui.progress( - _('bundling'), idx, item=fname, - unit=_('files'), total=efiles) - yield chnk + fstate[0] = fname + fstate[1] = fnodes.pop(fname, {}) + first = True + + for chunk in filerevlog.group(prune(filerevlog, fstate[1]), + bundler): + if first: + if chunk == bundler.close(): + break + count[0] += 1 + yield bundler.fileheader(fname) + first = False + yield chunk # Signal that no more groups are left. - yield changegroup.closechunk() + yield bundler.close() self.ui.progress(_('bundling'), None) - if msng_cl_lst: - self.hook('outgoing', node=hex(msng_cl_lst[0]), source=source) + if csets: + self.hook('outgoing', node=hex(csets[0]), source=source) return changegroup.unbundle10(util.chunkbuffer(gengroup()), 'UN') @@ -1683,75 +1590,75 @@ nodes is the set of nodes to send""" - self.hook('preoutgoing', throw=True, source=source) + cl = self.changelog + mf = self.manifest + mfs = {} + changedfiles = set() + fstate = [''] + count = [0] - cl = self.changelog - revset = set([cl.rev(n) for n in nodes]) + self.hook('preoutgoing', throw=True, source=source) self.changegroupinfo(nodes, source) - def identity(x): - return x + revset = set([cl.rev(n) for n in nodes]) def gennodelst(log): for r in log: if log.linkrev(r) in revset: yield log.node(r) - def lookuplinkrev_func(revlog): - def lookuplinkrev(n): - return cl.node(revlog.linkrev(revlog.rev(n))) - return lookuplinkrev + def lookup(revlog, x): + if revlog == cl: + c = cl.read(x) + changedfiles.update(c[3]) + mfs.setdefault(c[0], x) + count[0] += 1 + self.ui.progress(_('bundling'), count[0], unit=_('changesets')) + return x + elif revlog == mf: + count[0] += 1 + self.ui.progress(_('bundling'), count[0], + unit=_('manifests'), total=len(mfs)) + return cl.node(revlog.linkrev(revlog.rev(x))) + else: + self.ui.progress( + _('bundling'), count[0], item=fstate[0], + total=len(changedfiles), unit=_('files')) + return cl.node(revlog.linkrev(revlog.rev(x))) + + bundler = changegroup.bundle10(lookup) def gengroup(): '''yield a sequence of changegroup chunks (strings)''' # construct a list of all changed files - changedfiles = set() - mmfs = {} - collect = changegroup.collector(cl, mmfs, changedfiles) - for cnt, chnk in enumerate(cl.group(nodes, identity, collect)): - # revlog.group yields three entries per node, so - # dividing by 3 gives an approximation of how many - # nodes have been processed. - self.ui.progress(_('bundling'), cnt / 3, unit=_('changesets')) - yield chnk - changecount = cnt / 3 + for chunk in cl.group(nodes, bundler): + yield chunk self.ui.progress(_('bundling'), None) - mnfst = self.manifest - nodeiter = gennodelst(mnfst) - efiles = {} - for cnt, chnk in enumerate(mnfst.group(nodeiter, - lookuplinkrev_func(mnfst))): - if cnt % 3 == 1: - mnode = chnk[:20] - efiles.update(mnfst.readdelta(mnode)) - # see above comment for why we divide by 3 - self.ui.progress(_('bundling'), cnt / 3, - unit=_('manifests'), total=changecount) - yield chnk - efiles = len(efiles) + count[0] = 0 + for chunk in mf.group(gennodelst(mf), bundler): + yield chunk self.ui.progress(_('bundling'), None) - for idx, fname in enumerate(sorted(changedfiles)): + count[0] = 0 + for fname in sorted(changedfiles): filerevlog = self.file(fname) if not len(filerevlog): raise util.Abort(_("empty or missing revlog for %s") % fname) - nodeiter = gennodelst(filerevlog) - nodeiter = list(nodeiter) - if nodeiter: - yield changegroup.chunkheader(len(fname)) - yield fname - lookup = lookuplinkrev_func(filerevlog) - for chnk in filerevlog.group(nodeiter, lookup): - self.ui.progress( - _('bundling'), idx, item=fname, - total=efiles, unit=_('files')) - yield chnk + fstate[0] = fname + first = True + for chunk in filerevlog.group(gennodelst(filerevlog), bundler): + if first: + if chunk == bundler.close(): + break + count[0] += 1 + yield bundler.fileheader(fname) + first = False + yield chunk + yield bundler.close() self.ui.progress(_('bundling'), None) - yield changegroup.closechunk() - if nodes: self.hook('outgoing', node=hex(nodes[0]), source=source) @@ -1789,9 +1696,9 @@ # inconsistent view cl = self.changelog cl.delayupdate() - oldheads = len(cl.heads()) + oldheads = cl.heads() - tr = self.transaction("\n".join([srctype, urlmod.hidepassword(url)])) + tr = self.transaction("\n".join([srctype, util.hidepassword(url)])) try: trp = weakref.proxy(tr) # pull off the changeset group @@ -1809,6 +1716,7 @@ pr = prog() source.callback = pr + source.changelogheader() if (cl.addgroup(source, csmap, trp) is None and not emptyok): raise util.Abort(_("received changelog group is empty")) @@ -1828,6 +1736,7 @@ # if the result of the merge of 1 and 2 is the same in 3 and 4, # no new manifest will be created and the manifest group will # be empty during the pull + source.manifestheader() self.manifest.addgroup(source, revmap, trp) self.ui.progress(_('manifests'), None) @@ -1849,9 +1758,10 @@ source.callback = None while 1: - f = source.chunk() - if not f: + chunkdata = source.filelogheader() + if not chunkdata: break + f = chunkdata["filename"] self.ui.debug("adding %s revisions\n" % f) pr() fl = self.file(f) @@ -1880,14 +1790,20 @@ _('missing file data for %s:%s - run hg verify') % (f, hex(n))) - newheads = len(cl.heads()) - heads = "" - if oldheads and newheads != oldheads: - heads = _(" (%+d heads)") % (newheads - oldheads) + dh = 0 + if oldheads: + heads = cl.heads() + dh = len(heads) - len(oldheads) + for h in heads: + if h not in oldheads and 'close' in self[h].extra(): + dh -= 1 + htext = "" + if dh: + htext = _(" (%+d heads)") % dh self.ui.status(_("added %d changesets" " with %d changes to %d files%s\n") - % (changesets, revisions, files, heads)) + % (changesets, revisions, files, htext)) if changesets > 0: p = lambda: cl.writepending() and self.root or "" @@ -1915,16 +1831,11 @@ self.hook("incoming", node=hex(cl.node(i)), source=srctype, url=url) - # FIXME - why does this care about tip? - if newheads == oldheads: - bookmarks.update(self, self.dirstate.parents(), self['tip'].node()) - # never return 0 here: - if newheads < oldheads: - return newheads - oldheads - 1 + if dh < 0: + return dh - 1 else: - return newheads - oldheads + 1 - + return dh + 1 def stream_in(self, remote, requirements): lock = self.lock() @@ -2014,10 +1925,22 @@ return self.pull(remote, heads) def pushkey(self, namespace, key, old, new): - return pushkey.push(self, namespace, key, old, new) + self.hook('prepushkey', throw=True, namespace=namespace, key=key, + old=old, new=new) + ret = pushkey.push(self, namespace, key, old, new) + self.hook('pushkey', namespace=namespace, key=key, old=old, new=new, + ret=ret) + return ret def listkeys(self, namespace): - return pushkey.list(self, namespace) + self.hook('prelistkeys', throw=True, namespace=namespace) + values = pushkey.list(self, namespace) + self.hook('listkeys', namespace=namespace, values=values) + return values + + def debugwireargs(self, one, two, three=None, four=None, five=None): + '''used to test argument passing over the wire''' + return "%s %s %s %s %s" % (one, two, three, four, five) # used to avoid circular references so destructors work def aftertrans(files): @@ -2028,7 +1951,7 @@ return a def instance(ui, path, create): - return localrepository(ui, util.drop_scheme('file', path), create) + return localrepository(ui, util.localpath(path), create) def islocal(path): return True diff -r 7b627923739f -r 680c3c6fcb48 mercurial/manifest.py --- a/mercurial/manifest.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/manifest.py Fri May 06 14:29:53 2011 +0200 @@ -38,6 +38,14 @@ r = self.rev(node) return self.parse(mdiff.patchtext(self.revdiff(self.deltaparent(r), r))) + def readfast(self, node): + '''use the faster of readdelta or read''' + r = self.rev(node) + deltaparent = self.deltaparent(r) + if deltaparent != revlog.nullrev and deltaparent in self.parentrevs(r): + return self.readdelta(node) + return self.read(node) + def read(self, node): if node == revlog.nullid: return manifestdict() # don't upset local cache diff -r 7b627923739f -r 680c3c6fcb48 mercurial/match.py --- a/mercurial/match.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/match.py Fri May 06 14:29:53 2011 +0200 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. import re -import util +import scmutil, util from i18n import _ class match(object): @@ -269,13 +269,13 @@ pats = [] for kind, name in [_patsplit(p, default) for p in names]: if kind in ('glob', 'relpath'): - name = util.canonpath(root, cwd, name, auditor) + name = scmutil.canonpath(root, cwd, name, auditor) elif kind in ('relglob', 'path'): name = util.normpath(name) elif kind in ('listfile', 'listfile0'): delimiter = kind == 'listfile0' and '\0' or '\n' try: - files = open(name, 'r').read().split(delimiter) + files = util.readfile(name).split(delimiter) files = [f for f in files if f] except EnvironmentError: raise util.Abort(_("unable to read file list (%s)") % name) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/merge.py --- a/mercurial/merge.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/merge.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,7 @@ from node import nullid, nullrev, hex, bin from i18n import _ -import util, filemerge, copies, subrepo +import scmutil, util, filemerge, copies, subrepo import errno, os, shutil class mergestate(object): @@ -47,7 +47,7 @@ self._dirty = False def add(self, fcl, fco, fca, fd, flags): hash = util.sha1(fcl.path()).hexdigest() - self._repo.opener("merge/" + hash, "w").write(fcl.data()) + self._repo.opener.write("merge/" + hash, fcl.data()) self._state[fd] = ['u', hash, fcl.path(), fca.path(), hex(fca.filenode()), fco.path(), flags] self._dirty = True @@ -268,10 +268,9 @@ updated, merged, removed, unresolved = 0, 0, 0, 0 ms = mergestate(repo) - ms.reset(wctx.parents()[0].node()) + ms.reset(wctx.p1().node()) moves = [] action.sort(key=actionkey) - substate = wctx.substate # prime # prescan for merges u = repo.ui @@ -286,7 +285,7 @@ fco = mctx[f2] if mctx == actx: # backwards, use working dir parent as ancestor if fcl.parents(): - fca = fcl.parents()[0] + fca = fcl.p1() else: fca = repo.filectx(f, fileid=nullrev) else: @@ -303,7 +302,7 @@ repo.ui.debug("removing %s\n" % f) os.unlink(repo.wjoin(f)) - audit_path = util.path_auditor(repo.root) + audit_path = scmutil.pathauditor(repo.root) numupdates = len(action) for i, a in enumerate(action): @@ -439,7 +438,7 @@ if f: repo.dirstate.forget(f) -def update(repo, node, branchmerge, force, partial): +def update(repo, node, branchmerge, force, partial, ancestor=None): """ Perform a merge between the working directory and the given node @@ -492,9 +491,12 @@ overwrite = force and not branchmerge pl = wc.parents() p1, p2 = pl[0], repo[node] - pa = p1.ancestor(p2) + if ancestor: + pa = repo[ancestor] + else: + pa = p1.ancestor(p2) + fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) - fastforward = False ### check phase if not overwrite and len(pl) > 1: @@ -504,9 +506,7 @@ raise util.Abort(_("merging with a working directory ancestor" " has no effect")) elif pa == p1: - if p1.branch() != p2.branch(): - fastforward = True - else: + if p1.branch() == p2.branch(): raise util.Abort(_("nothing to merge (use 'hg update'" " or check 'hg heads')")) if not force and (wc.files() or wc.deleted()): @@ -551,7 +551,7 @@ if not partial: repo.dirstate.setparents(fp1, fp2) recordupdates(repo, action, branchmerge) - if not branchmerge and not fastforward: + if not branchmerge: repo.dirstate.setbranch(p2.branch()) finally: wlock.release() diff -r 7b627923739f -r 680c3c6fcb48 mercurial/minirst.py --- a/mercurial/minirst.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/minirst.py Fri May 06 14:29:53 2011 +0200 @@ -467,7 +467,7 @@ print return blocks - text = open(sys.argv[1]).read() + text = util.readfile(sys.argv[1]) blocks = debug(findblocks, text) blocks = debug(findliteralblocks, blocks) blocks, pruned = debug(prunecontainers, blocks, sys.argv[2:]) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/osutil.c --- a/mercurial/osutil.c Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/osutil.c Fri May 06 14:29:53 2011 +0200 @@ -514,6 +514,22 @@ } #endif +#ifdef __APPLE__ +#include + +static PyObject *isgui(PyObject *self) +{ + CFDictionaryRef dict = CGSessionCopyCurrentDictionary(); + + if (dict != NULL) { + CFRelease(dict); + return Py_True; + } else { + return Py_False; + } +} +#endif + static char osutil_doc[] = "Native operating system services."; static PyMethodDef methods[] = { @@ -524,6 +540,12 @@ "Open a file with POSIX-like semantics.\n" "On error, this function may raise either a WindowsError or an IOError."}, #endif +#ifdef __APPLE__ + { + "isgui", (PyCFunction)isgui, METH_NOARGS, + "Is a CoreGraphics session available?" + }, +#endif {NULL, NULL} }; diff -r 7b627923739f -r 680c3c6fcb48 mercurial/parser.py --- a/mercurial/parser.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/parser.py Fri May 06 14:29:53 2011 +0200 @@ -78,7 +78,9 @@ 'generate a parse tree from a message' self._iter = self._tokenizer(message) self._advance() - return self._parse() + res = self._parse() + token, value, pos = self.current + return res, pos def eval(self, tree): 'recursively evaluate a parse tree using node methods' if not isinstance(tree, tuple): diff -r 7b627923739f -r 680c3c6fcb48 mercurial/patch.py --- a/mercurial/patch.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/patch.py Fri May 06 14:29:53 2011 +0200 @@ -11,7 +11,7 @@ from i18n import _ from node import hex, nullid, short -import base85, mdiff, util, diffhelpers, copies, encoding +import base85, mdiff, scmutil, util, diffhelpers, copies, encoding gitre = re.compile('diff --git a/(.*) b/(.*)') @@ -21,7 +21,8 @@ # helper functions def copyfile(src, dst, basedir): - abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]] + abssrc, absdst = [scmutil.canonpath(basedir, basedir, x) + for x in [src, dst]] if os.path.lexists(absdst): raise util.Abort(_("cannot create %s: destination already exists") % dst) @@ -405,7 +406,7 @@ self.ui.warn(_("unable to find '%s' for patching\n") % self.fname) self.hash = {} - self.dirty = 0 + self.dirty = False self.offset = 0 self.skew = 0 self.rej = [] @@ -488,11 +489,6 @@ cand.sort(key=lambda x: abs(x - linenum)) return cand - def hashlines(self): - self.hash = {} - for x, s in enumerate(self.lines): - self.hash.setdefault(s, []).append(x) - def makerejlines(self, fname): base = os.path.basename(fname) yield "--- %s\n+++ %s\n" % (base, base) @@ -543,7 +539,7 @@ else: self.lines[:] = h.new() self.offset += len(h.new()) - self.dirty = 1 + self.dirty = True return 0 horig = h @@ -571,11 +567,13 @@ else: self.lines[start : start + h.lena] = h.new() self.offset += h.lenb - h.lena - self.dirty = 1 + self.dirty = True return 0 - # ok, we couldn't match the hunk. Lets look for offsets and fuzz it - self.hashlines() + # ok, we couldn't match the hunk. Lets look for offsets and fuzz it + self.hash = {} + for x, s in enumerate(self.lines): + self.hash.setdefault(s, []).append(x) if h.hunk[-1][0] != ' ': # if the hunk tried to put something at the bottom of the file # override the start line and use eof here @@ -594,7 +592,7 @@ self.lines[l : l + len(old)] = newlines self.offset += len(newlines) - len(old) self.skew = l - orig_start - self.dirty = 1 + self.dirty = True offset = l - orig_start - fuzzlen if fuzzlen: msg = _("Hunk #%d succeeded at %d " @@ -613,6 +611,12 @@ self.rej.append(horig) return -1 + def close(self): + if self.dirty: + self.writelines(self.fname, self.lines) + self.write_rej() + return len(self.rej) + class hunk(object): def __init__(self, desc, num, lr, context, create=False, remove=False): self.number = num @@ -680,6 +684,7 @@ del self.b[-1] self.lena -= 1 self.lenb -= 1 + self._fixnewline(lr) def read_context_hunk(self, lr): self.desc = lr.readline() @@ -782,9 +787,14 @@ self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena, self.startb, self.lenb) self.hunk[0] = self.desc + self._fixnewline(lr) - def fix_newline(self): - diffhelpers.fix_newline(self.hunk, self.a, self.b) + def _fixnewline(self, lr): + l = lr.readline() + if l.startswith('\ '): + diffhelpers.fix_newline(self.hunk, self.a, self.b) + else: + lr.push(l) def complete(self): return len(self.a) == self.lena and len(self.b) == self.lenb @@ -993,12 +1003,11 @@ maps filenames to gitpatch records. Unique event. """ changed = {} - current_hunk = None afile = "" bfile = "" state = None hunknum = 0 - emitfile = False + emitfile = newfile = False git = False # our states @@ -1007,15 +1016,9 @@ lr = linereader(fp) while True: - newfile = newgitfile = False x = lr.readline() if not x: break - if current_hunk: - if x.startswith('\ '): - current_hunk.fix_newline() - yield 'hunk', current_hunk - current_hunk = None if (state == BFILE and ((not context and x[0] == '@') or ((context is not False) and x.startswith('***************')))): if context is None and x.startswith('***************'): @@ -1023,18 +1026,20 @@ gpatch = changed.get(bfile) create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD' remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE' - current_hunk = hunk(x, hunknum + 1, lr, context, create, remove) + h = hunk(x, hunknum + 1, lr, context, create, remove) hunknum += 1 if emitfile: emitfile = False - yield 'file', (afile, bfile, current_hunk) + yield 'file', (afile, bfile, h) + yield 'hunk', h elif state == BFILE and x.startswith('GIT binary patch'): - current_hunk = binhunk(changed[bfile]) + h = binhunk(changed[bfile]) hunknum += 1 if emitfile: emitfile = False - yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk) - current_hunk.extract(lr) + yield 'file', ('a/' + afile, 'b/' + bfile, h) + h.extract(lr) + yield 'hunk', h elif x.startswith('diff --git'): # check for git diff, scanning the whole patch file if needed m = gitre.match(x) @@ -1052,7 +1057,7 @@ if gp and (gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode): afile = bfile - newgitfile = True + newfile = True elif x.startswith('---'): # check for a unified diff l2 = lr.readline() @@ -1079,16 +1084,11 @@ afile = parsefilename(x) bfile = parsefilename(l2) - if newgitfile or newfile: + if newfile: + newfile = False emitfile = True state = BFILE hunknum = 0 - if current_hunk: - if current_hunk.complete(): - yield 'hunk', current_hunk - else: - raise PatchError(_("malformed patch %s %s") % (afile, - current_hunk.desc)) def applydiff(ui, fp, changed, strip=1, eolmode='strict'): """Reads a patch from fp and tries to apply it. @@ -1112,15 +1112,7 @@ err = 0 current_file = None cwd = os.getcwd() - opener = util.opener(cwd) - - def closefile(): - if not current_file: - return 0 - if current_file.dirty: - current_file.writelines(current_file.fname, current_file.lines) - current_file.write_rej() - return len(current_file.rej) + opener = scmutil.opener(cwd) for state, values in iterhunks(ui, fp): if state == 'hunk': @@ -1132,15 +1124,16 @@ if ret > 0: err = 1 elif state == 'file': - rejects += closefile() + if current_file: + rejects += current_file.close() afile, bfile, first_hunk = values try: current_file, missing = selectfile(afile, bfile, first_hunk, strip) current_file = patcher(ui, current_file, opener, missing=missing, eolmode=eolmode) - except PatchError, err: - ui.warn(str(err) + '\n') + except PatchError, inst: + ui.warn(str(inst) + '\n') current_file = None rejects += 1 continue @@ -1157,13 +1150,14 @@ else: raise util.Abort(_('unsupported parser state: %s') % state) - rejects += closefile() + if current_file: + rejects += current_file.close() if rejects: return -1 return err -def externalpatch(patcher, patchname, ui, strip, cwd, files): +def _externalpatch(patcher, patchname, ui, strip, cwd, files): """use to apply to the working directory. returns whether patch was applied with fuzz factor.""" @@ -1247,7 +1241,7 @@ files = {} try: if patcher: - return externalpatch(patcher, patchname, ui, strip, cwd, files) + return _externalpatch(patcher, patchname, ui, strip, cwd, files) return internalpatch(patchname, ui, strip, cwd, files, eolmode) except PatchError, err: raise util.Abort(str(err)) @@ -1331,7 +1325,7 @@ opts = mdiff.defaultopts if not node1 and not node2: - node1 = repo.dirstate.parents()[0] + node1 = repo.dirstate.p1() def lrugetfilectx(): cache = {} diff -r 7b627923739f -r 680c3c6fcb48 mercurial/posix.py --- a/mercurial/posix.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/posix.py Fri May 06 14:29:53 2011 +0200 @@ -6,8 +6,7 @@ # GNU General Public License version 2 or any later version. from i18n import _ -import osutil -import os, sys, errno, stat, getpass, pwd, grp +import os, sys, errno, stat, getpass, pwd, grp, tempfile posixfile = open nulldev = '/dev/null' @@ -29,29 +28,6 @@ '''return number of hardlinks for the given file''' return os.lstat(name).st_nlink -def rcfiles(path): - rcs = [os.path.join(path, 'hgrc')] - rcdir = os.path.join(path, 'hgrc.d') - try: - rcs.extend([os.path.join(rcdir, f) - for f, kind in osutil.listdir(rcdir) - if f.endswith(".rc")]) - except OSError: - pass - return rcs - -def system_rcpath(): - path = [] - # old mod_python does not set sys.argv - if len(getattr(sys, 'argv', [])) > 0: - path.extend(rcfiles(os.path.dirname(sys.argv[0]) + - '/../etc/mercurial')) - path.extend(rcfiles('/etc/mercurial')) - return path - -def user_rcpath(): - return [os.path.expanduser('~/.hgrc')] - def parse_patch_output(output_line): """parses the output produced by patch and returns the filename""" pf = output_line[14:] @@ -83,7 +59,7 @@ os.unlink(f) try: os.symlink(data, f) - except: + except OSError: # failed to make a link, rewrite file fp = open(f, "w") fp.write(data) @@ -108,6 +84,50 @@ # Turn off all +x bits os.chmod(f, s & 0666) +def checkexec(path): + """ + Check whether the given path is on a filesystem with UNIX-like exec flags + + Requires a directory (like /foo/.hg) + """ + + # VFAT on some Linux versions can flip mode but it doesn't persist + # a FS remount. Frequently we can detect it if files are created + # with exec bit on. + + try: + EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + fh, fn = tempfile.mkstemp(dir=path, prefix='hg-checkexec-') + try: + os.close(fh) + m = os.stat(fn).st_mode & 0777 + new_file_has_exec = m & EXECFLAGS + os.chmod(fn, m ^ EXECFLAGS) + exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0777) == m) + finally: + os.unlink(fn) + except (IOError, OSError): + # we don't care, the user probably won't be able to commit anyway + return False + return not (new_file_has_exec or exec_flags_cannot_flip) + +def checklink(path): + """check whether the given path is on a symlink-capable filesystem""" + # mktemp is not racy because symlink creation will fail if the + # file already exists + name = tempfile.mktemp(dir=path, prefix='hg-checklink-') + try: + os.symlink(".", name) + os.unlink(name) + return True + except (OSError, AttributeError): + return False + +def checkosfilename(path): + '''Check that the base-relative path is a valid filename on this platform. + Returns None if the path is ok, or a UI string describing the problem.''' + pass # on posix platforms, every path is ok + def set_binary(fd): pass @@ -296,7 +316,9 @@ if not os.isatty(fd): continue arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8) - return array.array('h', arri)[1] + width = array.array('h', arri)[1] + if width > 0: + return width except ValueError: pass except IOError, e: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/pure/bdiff.py --- a/mercurial/pure/bdiff.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/pure/bdiff.py Fri May 06 14:29:53 2011 +0200 @@ -19,6 +19,7 @@ def _normalizeblocks(a, b, blocks): prev = None + r = [] for curr in blocks: if prev is None: prev = curr @@ -40,9 +41,10 @@ while (b1end + shift < b2end and a[a1end + shift] == b[b1end + shift]): shift += 1 - yield a1, b1, l1 + shift + r.append((a1, b1, l1 + shift)) prev = a2 + shift, b2 + shift, l2 - shift - yield prev + r.append(prev) + return r def bdiff(a, b): a = str(a).splitlines(True) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/pure/mpatch.py --- a/mercurial/pure/mpatch.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/pure/mpatch.py Fri May 06 14:29:53 2011 +0200 @@ -56,9 +56,9 @@ def pull(dst, src, l): # pull l bytes from src while l: - f = src.pop(0) + f = src.pop() if f[0] > l: # do we need to split? - src.insert(0, (f[0] - l, f[1] + l)) + src.append((f[0] - l, f[1] + l)) dst.append((l, f[1])) return dst.append(f) @@ -66,7 +66,7 @@ def collect(buf, list): start = buf - for l, p in list: + for l, p in reversed(list): move(buf, p, l) buf += l return (buf - start, start) @@ -88,7 +88,7 @@ new.append((l, pos + 12)) # what got added pos += l + 12 last = p2 - frags = new + frags # what was left at the end + frags.extend(reversed(new)) # what was left at the end t = collect(b2, frags) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/pure/parsers.py --- a/mercurial/pure/parsers.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/pure/parsers.py Fri May 06 14:29:53 2011 +0200 @@ -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. -from mercurial.node import bin, nullid, nullrev +from mercurial.node import bin, nullid from mercurial import util import struct, zlib diff -r 7b627923739f -r 680c3c6fcb48 mercurial/repair.py --- a/mercurial/repair.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/repair.py Fri May 06 14:29:53 2011 +0200 @@ -6,14 +6,14 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import changegroup, bookmarks -from node import nullrev, short -from i18n import _ +from mercurial import changegroup, bookmarks +from mercurial.node import short +from mercurial.i18n import _ import os -def _bundle(repo, bases, heads, node, suffix, extranodes=None, compress=True): +def _bundle(repo, bases, heads, node, suffix, compress=True): """create a bundle with the specified revisions as a backup""" - cg = repo.changegroupsubset(bases, heads, 'strip', extranodes) + cg = repo.changegroupsubset(bases, heads, 'strip') backupdir = repo.join("strip-backup") if not os.path.isdir(backupdir): os.mkdir(backupdir) @@ -33,40 +33,26 @@ return sorted(files) -def _collectextranodes(repo, files, link): - """return the nodes that have to be saved before the strip""" - def collectone(cl, revlog): - extra = [] - startrev = count = len(revlog) +def _collectbrokencsets(repo, files, striprev): + """return the changesets which will be broken by the truncation""" + s = set() + def collectone(revlog): + links = (revlog.linkrev(i) for i in revlog) # find the truncation point of the revlog - for i in xrange(count): - lrev = revlog.linkrev(i) - if lrev >= link: - startrev = i + 1 + for lrev in links: + if lrev >= striprev: break + # see if any revision after this point has a linkrev + # less than striprev (those will be broken by strip) + for lrev in links: + if lrev < striprev: + s.add(lrev) - # see if any revision after that point has a linkrev less than link - # (we have to manually save these guys) - for i in xrange(startrev, count): - node = revlog.node(i) - lrev = revlog.linkrev(i) - if lrev < link: - extra.append((node, cl.node(lrev))) - - return extra + collectone(repo.manifest) + for fname in files: + collectone(repo.file(fname)) - extranodes = {} - cl = repo.changelog - extra = collectone(cl, repo.manifest) - if extra: - extranodes[1] = extra - for fname in files: - f = repo.file(fname) - extra = collectone(cl, f) - if extra: - extranodes[fname] = extra - - return extranodes + return s def strip(ui, repo, node, backup="all"): cl = repo.changelog @@ -82,28 +68,26 @@ # the list of heads and bases of the set of interesting revisions. # (head = revision in the set that has no descendant in the set; # base = revision in the set that has no ancestor in the set) - tostrip = set((striprev,)) - saveheads = set() - savebases = [] + tostrip = set(cl.descendants(striprev)) + tostrip.add(striprev) + + files = _collectfiles(repo, striprev) + saverevs = _collectbrokencsets(repo, files, striprev) + + # compute heads + saveheads = set(saverevs) for r in xrange(striprev + 1, len(cl)): - parents = cl.parentrevs(r) - if parents[0] in tostrip or parents[1] in tostrip: - # r is a descendant of striprev - tostrip.add(r) - # if this is a merge and one of the parents does not descend - # from striprev, mark that parent as a savehead. - if parents[1] != nullrev: - for p in parents: - if p not in tostrip and p > striprev: - saveheads.add(p) - else: - # if no parents of this revision will be stripped, mark it as - # a savebase - if parents[0] < striprev and parents[1] < striprev: - savebases.append(cl.node(r)) + if r not in tostrip: + saverevs.add(r) + saveheads.difference_update(cl.parentrevs(r)) + saveheads.add(r) + saveheads = [cl.node(r) for r in saveheads] - saveheads.difference_update(parents) - saveheads.add(r) + # compute base nodes + if saverevs: + descendants = set(cl.descendants(*saverevs)) + saverevs.difference_update(descendants) + savebases = [cl.node(r) for r in saverevs] bm = repo._bookmarks updatebm = [] @@ -112,20 +96,15 @@ if rev in tostrip: updatebm.append(m) - saveheads = [cl.node(r) for r in saveheads] - files = _collectfiles(repo, striprev) - - extranodes = _collectextranodes(repo, files, striprev) - # create a changegroup for all the branches we need to keep backupfile = None if backup == "all": backupfile = _bundle(repo, [node], cl.heads(), node, 'backup') repo.ui.status(_("saved backup bundle to %s\n") % backupfile) - if saveheads or extranodes: + if saveheads or savebases: # do not compress partial bundle if we remove it from disk later chgrpfile = _bundle(repo, savebases, saveheads, node, 'temp', - extranodes=extranodes, compress=keeppartialbundle) + compress=keeppartialbundle) mfst = repo.manifest @@ -149,7 +128,7 @@ tr.abort() raise - if saveheads or extranodes: + if saveheads or savebases: ui.note(_("adding branch\n")) f = open(chgrpfile, "rb") gen = changegroup.readbundle(f, chgrpfile) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/revlog.py --- a/mercurial/revlog.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/revlog.py Fri May 06 14:29:53 2011 +0200 @@ -14,7 +14,7 @@ # import stuff from node for others to import from revlog from node import bin, hex, nullid, nullrev, short #@UnusedImport from i18n import _ -import changegroup, ancestor, mdiff, parsers, error, util +import ancestor, mdiff, parsers, error, util import struct, zlib, errno _pack = struct.pack @@ -34,9 +34,7 @@ REVLOGNG_FLAGS = REVLOGNGINLINEDATA | REVLOGSHALLOW # revlog index flags -REVIDX_PARENTDELTA = 1 -REVIDX_PUNCHED_FLAG = 2 -REVIDX_KNOWN_FLAGS = REVIDX_PUNCHED_FLAG | REVIDX_PARENTDELTA +REVIDX_KNOWN_FLAGS = 0 # max size of revlog with inline data _maxinline = 131072 @@ -223,7 +221,6 @@ self._chunkcache = (0, '') self.index = [] self._shallowroot = shallowroot - self._parentdelta = 0 self._pcache = {} self._nodecache = {nullid: nullrev} self._nodepos = None @@ -233,8 +230,6 @@ v = opener.options['defversion'] if v & REVLOGNG: v |= REVLOGNGINLINEDATA - if v & REVLOGNG and 'parentdelta' in opener.options: - self._parentdelta = 1 if shallowroot: v |= REVLOGSHALLOW @@ -288,7 +283,7 @@ @util.propertycache def nodemap(self): - n = self.rev(self.node(0)) + self.rev(self.node(0)) return self._nodecache def rev(self, node): @@ -399,11 +394,12 @@ yield i break - def findmissing(self, common=None, heads=None): - """Return the ancestors of heads that are not ancestors of common. + def findcommonmissing(self, common=None, heads=None): + """Return a tuple of the ancestors of common and the ancestors of heads + that are not ancestors of common. - More specifically, return a list of nodes N such that every N - satisfies the following constraints: + More specifically, the second element is a list of nodes N such that + every N satisfies the following constraints: 1. N is an ancestor of some node in 'heads' 2. N is not an ancestor of any node in 'common' @@ -441,7 +437,25 @@ visit.append(p) missing = list(missing) missing.sort() - return [self.node(r) for r in missing] + return has, [self.node(r) for r in missing] + + def findmissing(self, common=None, heads=None): + """Return the ancestors of heads that are not ancestors of common. + + More specifically, return a list of nodes N such that every N + satisfies the following constraints: + + 1. N is an ancestor of some node in 'heads' + 2. N is not an ancestor of any node in 'common' + + The list is sorted by revision number, meaning it is + topologically sorted. + + 'heads' and 'common' are both lists of node IDs. If heads is + not supplied, uses all of the revlog's heads. If common is not + supplied, uses nullid.""" + _common, missing = self.findcommonmissing(common, heads) + return missing def nodesbetween(self, roots=None, heads=None): """Return a topological path from 'roots' to 'heads'. @@ -492,7 +506,7 @@ # Turn heads into a dictionary so we can remove 'fake' heads. # Also, later we will be using it to filter out the heads we can't # find from roots. - heads = dict.fromkeys(heads, 0) + heads = dict.fromkeys(heads, False) # Start at the top and keep marking parents until we're done. nodestotag = set(heads) # Remember where the top was so we can use it as a limit later. @@ -582,22 +596,33 @@ # We're trying to figure out which heads are reachable # from roots. # Mark this head as having been reached - heads[n] = 1 + heads[n] = True elif ancestors is None: # Otherwise, we're trying to discover the heads. # Assume this is a head because if it isn't, the next step # will eventually remove it. - heads[n] = 1 + heads[n] = True # But, obviously its parents aren't. for p in self.parents(n): heads.pop(p, None) - heads = [n for n in heads.iterkeys() if heads[n] != 0] + heads = [n for n, flag in heads.iteritems() if flag] roots = list(roots) assert orderedout assert roots assert heads return (orderedout, roots, heads) + def headrevs(self): + count = len(self) + if not count: + return [nullrev] + ishead = [1] * (count + 1) + index = self.index + for r in xrange(count): + e = index[r] + ishead[e[5]] = ishead[e[6]] = 0 + return [r for r in xrange(count) if ishead[r]] + def heads(self, start=None, stop=None): """return the list of all nodes that have no children @@ -607,15 +632,9 @@ as if they had no children """ if start is None and stop is None: - count = len(self) - if not count: + if not len(self): return [nullid] - ishead = [1] * (count + 1) - index = self.index - for r in xrange(count): - e = index[r] - ishead[e[5]] = ishead[e[6]] = 0 - return [self.node(r) for r in xrange(count) if ishead[r]] + return [self.node(r) for r in self.headrevs()] if start is None: start = nullid @@ -803,19 +822,22 @@ def _chunk(self, rev): return decompress(self._chunkraw(rev, rev)) + def _chunkbase(self, rev): + return self._chunk(rev) + def _chunkclear(self): self._chunkcache = (0, '') def deltaparent(self, rev): - """return previous revision or parentrev according to flags""" - if self.flags(rev) & REVIDX_PARENTDELTA: - return self.parentrevs(rev)[0] + """return deltaparent of the given revision""" + if self.index[rev][3] == rev: + return nullrev else: return rev - 1 def revdiff(self, rev1, rev2): """return or calculate a delta between two revisions""" - if self.base(rev2) != rev2 and self.deltaparent(rev2) == rev1: + if rev1 != nullrev and self.deltaparent(rev2) == rev1: return self._chunk(rev2) return mdiff.textdiff(self.revision(self.node(rev1)), @@ -843,16 +865,10 @@ # build delta chain chain = [] - index = self.index # for performance iterrev = rev - e = index[iterrev] while iterrev != base and iterrev != cachedrev: chain.append(iterrev) - if e[0] & REVIDX_PARENTDELTA: - iterrev = e[5] - else: - iterrev -= 1 - e = index[iterrev] + iterrev -= 1 chain.reverse() base = iterrev @@ -865,7 +881,7 @@ self._chunkraw(base, rev) if text is None: - text = self._chunk(base) + text = self._chunkbase(base) bins = [self._chunk(r) for r in chain] text = mdiff.patches(text, bins) @@ -877,8 +893,7 @@ def _checkhash(self, text, node, rev): p1, p2 = self.parents(node) - if (node != hash(text, p1, p2) and - not (self.flags(rev) & REVIDX_PUNCHED_FLAG)): + if node != hash(text, p1, p2): raise RevlogError(_("integrity check failed on %s:%d") % (self.indexfile, rev)) return text @@ -932,8 +947,7 @@ cachedelta - an optional precomputed delta """ node = hash(text, p1, p2) - if (node in self.nodemap and - (not self.flags(self.rev(node)) & REVIDX_PUNCHED_FLAG)): + if node in self.nodemap: return node dfh = None @@ -991,23 +1005,16 @@ # should we try to build a delta? if prev != nullrev: d = builddelta(prev) - if self._parentdelta and prev != p1r: - d2 = builddelta(p1r) - if d2 < d: - d = d2 - flags = REVIDX_PARENTDELTA dist, l, data, base = d # full versions are inserted when the needed deltas # become comparable to the uncompressed text - # or the base revision is punched if text is None: textlen = mdiff.patchedsize(self.rawsize(cachedelta[0]), cachedelta[1]) else: textlen = len(text) - if (d is None or dist > textlen * 2 or - (self.flags(base) & REVIDX_PUNCHED_FLAG)): + if d is None or dist > textlen * 2: text = buildtext() data = compress(text) l = len(data[1]) + len(data[0]) @@ -1039,7 +1046,7 @@ self._cache = (node, curr, text) return node - def group(self, nodelist, lookup, infocollect=None, fullrev=False): + def group(self, nodelist, bundler): """Calculate a delta group, yielding a sequence of changegroup chunks (strings). @@ -1049,45 +1056,26 @@ guaranteed to have this parent as it has all history before these changesets. In the case firstparent is nullrev the changegroup starts with a full revision. - fullrev forces the insertion of the full revision, necessary - in the case of shallow clones where the first parent might - not exist at the reciever. """ - revs = [self.rev(n) for n in nodelist] + revs = sorted([self.rev(n) for n in nodelist]) # if we don't have any revisions touched by these changesets, bail if not revs: - yield changegroup.closechunk() + yield bundler.close() return # add the parent of the first rev p = self.parentrevs(revs[0])[0] revs.insert(0, p) - if p == nullrev: - fullrev = True # build deltas - for d in xrange(len(revs) - 1): - a, b = revs[d], revs[d + 1] - nb = self.node(b) - - if infocollect is not None: - infocollect(nb) + for r in xrange(len(revs) - 1): + prev, curr = revs[r], revs[r + 1] + for c in bundler.revchunk(self, curr, prev): + yield c - p = self.parents(nb) - meta = nb + p[0] + p[1] + lookup(nb) - if fullrev: - d = self.revision(nb) - meta += mdiff.trivialdiffheader(len(d)) - fullrev = False - else: - d = self.revdiff(a, b) - yield changegroup.chunkheader(len(meta) + len(d)) - yield meta - yield d - - yield changegroup.closechunk() + yield bundler.close() def addgroup(self, bundle, linkmapper, transaction): """ @@ -1119,52 +1107,34 @@ # loop through our set of deltas chain = None while 1: - chunkdata = bundle.parsechunk() + chunkdata = bundle.deltachunk(chain) if not chunkdata: break node = chunkdata['node'] p1 = chunkdata['p1'] p2 = chunkdata['p2'] cs = chunkdata['cs'] - delta = chunkdata['data'] + deltabase = chunkdata['deltabase'] + delta = chunkdata['delta'] link = linkmapper(cs) - if (node in self.nodemap and - (not self.flags(self.rev(node)) & REVIDX_PUNCHED_FLAG)): + if node in self.nodemap: # this can happen if two branches make the same change chain = node continue for p in (p1, p2): if not p in self.nodemap: - if self._shallow: - # add null entries for missing parents - # XXX FIXME - #if base == nullrev: - # base = len(self) - #e = (offset_type(end, REVIDX_PUNCHED_FLAG), - # 0, 0, base, nullrev, nullrev, nullrev, p) - #self.index.insert(-1, e) - #self.nodemap[p] = r - #entry = self._io.packentry(e, self.node, - # self.version, r) - #ifh.write(entry) - #t, r = r, r + 1 - raise LookupError(p, self.indexfile, - _('unknown parent')) - else: - raise LookupError(p, self.indexfile, - _('unknown parent')) + raise LookupError(p, self.indexfile, + _('unknown parent')) - if not chain: - # retrieve the parent revision of the delta chain - chain = p1 - if not chain in self.nodemap: - raise LookupError(chain, self.indexfile, _('unknown base')) + if deltabase not in self.nodemap: + raise LookupError(deltabase, self.indexfile, + _('unknown delta base')) - chainrev = self.rev(chain) + baserev = self.rev(deltabase) chain = self._addrevision(node, None, transaction, link, - p1, p2, (chainrev, delta), ifh, dfh) + p1, p2, (baserev, delta), ifh, dfh) if not dfh and not self._inline: # addrevision switched from inline to conventional # reopen the index diff -r 7b627923739f -r 680c3c6fcb48 mercurial/revset.py --- a/mercurial/revset.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/revset.py Fri May 06 14:29:53 2011 +0200 @@ -6,13 +6,15 @@ # GNU General Public License version 2 or any later version. import re -import parser, util, error, discovery +import parser, util, error, discovery, help, hbisect import bookmarks as bookmarksmod import match as matchmod -from i18n import _, gettext +from i18n import _ elements = { "(": (20, ("group", 1, ")"), ("func", 1, ")")), + "~": (18, None, ("ancestor", 18)), + "^": (18, None, ("parent", 18), ("parentpost", 18)), "-": (5, ("negate", 19), ("minus", 5)), "::": (17, ("dagrangepre", 17), ("dagrange", 17), ("dagrangepost", 17)), @@ -47,7 +49,7 @@ elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully yield ('..', None, pos) pos += 1 # skip ahead - elif c in "():,-|&+!": # handle simple operators + elif c in "():,-|&+!~^": # handle simple operators yield (c, None, pos) elif (c in '"\'' or c == 'r' and program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings @@ -123,7 +125,7 @@ x = repo[x].rev() if x == -1 and len(subset) == len(repo): return [-1] - if x in subset: + if len(subset) == len(repo) or x in subset: return [x] return [] @@ -156,9 +158,10 @@ return getset(repo, getset(repo, subset, x), y) def orset(repo, subset, x, y): - s = set(getset(repo, subset, x)) - s |= set(getset(repo, [r for r in subset if r not in s], y)) - return [r for r in subset if r in s] + xl = getset(repo, subset, x) + s = set(xl) + yl = getset(repo, [r for r in subset if r not in s], y) + return xl + yl def notset(repo, subset, x): s = set(getset(repo, subset, x)) @@ -174,6 +177,356 @@ # functions +def adds(repo, subset, x): + """``adds(pattern)`` + Changesets that add a file matching pattern. + """ + # i18n: "adds" is a keyword + pat = getstring(x, _("adds requires a pattern")) + return checkstatus(repo, subset, pat, 1) + +def ancestor(repo, subset, x): + """``ancestor(single, single)`` + Greatest common ancestor of the two changesets. + """ + # i18n: "ancestor" is a keyword + l = getargs(x, 2, 2, _("ancestor requires two arguments")) + r = range(len(repo)) + a = getset(repo, r, l[0]) + b = getset(repo, r, l[1]) + if len(a) != 1 or len(b) != 1: + # i18n: "ancestor" is a keyword + raise error.ParseError(_("ancestor arguments must be single revisions")) + an = [repo[a[0]].ancestor(repo[b[0]]).rev()] + + return [r for r in an if r in subset] + +def ancestors(repo, subset, x): + """``ancestors(set)`` + Changesets that are ancestors of a changeset in set. + """ + args = getset(repo, range(len(repo)), x) + if not args: + return [] + s = set(repo.changelog.ancestors(*args)) | set(args) + return [r for r in subset if r in s] + +def ancestorspec(repo, subset, x, n): + """``set~n`` + Changesets that are the Nth ancestor (first parents only) of a changeset in set. + """ + try: + n = int(n[1]) + except ValueError: + raise error.ParseError(_("~ expects a number")) + ps = set() + cl = repo.changelog + for r in getset(repo, subset, x): + for i in range(n): + r = cl.parentrevs(r)[0] + ps.add(r) + return [r for r in subset if r in ps] + +def author(repo, subset, x): + """``author(string)`` + Alias for ``user(string)``. + """ + # i18n: "author" is a keyword + n = getstring(x, _("author requires a string")).lower() + return [r for r in subset if n in repo[r].user().lower()] + +def bisected(repo, subset, x): + """``bisected(string)`` + Changesets marked in the specified bisect state (good, bad, skip). + """ + state = getstring(x, _("bisect requires a string")).lower() + if state not in ('good', 'bad', 'skip', 'unknown'): + raise error.ParseError(_('invalid bisect state')) + marked = set(repo.changelog.rev(n) for n in hbisect.load_state(repo)[state]) + return [r for r in subset if r in marked] + +def bookmark(repo, subset, x): + """``bookmark([name])`` + The named bookmark or all bookmarks. + """ + # i18n: "bookmark" is a keyword + args = getargs(x, 0, 1, _('bookmark takes one or no arguments')) + if args: + bm = getstring(args[0], + # i18n: "bookmark" is a keyword + _('the argument to bookmark must be a string')) + bmrev = bookmarksmod.listbookmarks(repo).get(bm, None) + if not bmrev: + raise util.Abort(_("bookmark '%s' does not exist") % bm) + bmrev = repo[bmrev].rev() + return [r for r in subset if r == bmrev] + bms = set([repo[r].rev() + for r in bookmarksmod.listbookmarks(repo).values()]) + return [r for r in subset if r in bms] + +def branch(repo, subset, x): + """``branch(string or set)`` + All changesets belonging to the given branch or the branches of the given + changesets. + """ + try: + b = getstring(x, '') + if b in repo.branchmap(): + return [r for r in subset if repo[r].branch() == b] + except error.ParseError: + # not a string, but another revspec, e.g. tip() + pass + + s = getset(repo, range(len(repo)), x) + b = set() + for r in s: + b.add(repo[r].branch()) + s = set(s) + return [r for r in subset if r in s or repo[r].branch() in b] + +def checkstatus(repo, subset, pat, field): + m = matchmod.match(repo.root, repo.getcwd(), [pat]) + s = [] + fast = (m.files() == [pat]) + for r in subset: + c = repo[r] + if fast: + if pat not in c.files(): + continue + else: + for f in c.files(): + if m(f): + break + else: + continue + files = repo.status(c.p1().node(), c.node())[field] + if fast: + if pat in files: + s.append(r) + else: + for f in files: + if m(f): + s.append(r) + break + return s + +def children(repo, subset, x): + """``children(set)`` + Child changesets of changesets in set. + """ + cs = set() + cl = repo.changelog + s = set(getset(repo, range(len(repo)), x)) + for r in xrange(0, len(repo)): + for p in cl.parentrevs(r): + if p in s: + cs.add(r) + return [r for r in subset if r in cs] + +def closed(repo, subset, x): + """``closed()`` + Changeset is closed. + """ + # i18n: "closed" is a keyword + getargs(x, 0, 0, _("closed takes no arguments")) + return [r for r in subset if repo[r].extra().get('close')] + +def contains(repo, subset, x): + """``contains(pattern)`` + Revision contains pattern. + """ + # i18n: "contains" is a keyword + pat = getstring(x, _("contains requires a pattern")) + m = matchmod.match(repo.root, repo.getcwd(), [pat]) + s = [] + if m.files() == [pat]: + for r in subset: + if pat in repo[r]: + s.append(r) + else: + for r in subset: + for f in repo[r].manifest(): + if m(f): + s.append(r) + break + return s + +def date(repo, subset, x): + """``date(interval)`` + Changesets within the interval, see :hg:`help dates`. + """ + # i18n: "date" is a keyword + ds = getstring(x, _("date requires a string")) + dm = util.matchdate(ds) + return [r for r in subset if dm(repo[r].date()[0])] + +def descendants(repo, subset, x): + """``descendants(set)`` + Changesets which are descendants of changesets in set. + """ + args = getset(repo, range(len(repo)), x) + if not args: + return [] + s = set(repo.changelog.descendants(*args)) | set(args) + return [r for r in subset if r in s] + +def follow(repo, subset, x): + """``follow()`` + An alias for ``::.`` (ancestors of the working copy's first parent). + """ + # i18n: "follow" is a keyword + getargs(x, 0, 0, _("follow takes no arguments")) + p = repo['.'].rev() + s = set(repo.changelog.ancestors(p)) | set([p]) + return [r for r in subset if r in s] + +def getall(repo, subset, x): + """``all()`` + All changesets, the same as ``0:tip``. + """ + # i18n: "all" is a keyword + getargs(x, 0, 0, _("all takes no arguments")) + return subset + +def grep(repo, subset, x): + """``grep(regex)`` + Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` + to ensure special escape characters are handled correctly. + """ + try: + # i18n: "grep" is a keyword + gr = re.compile(getstring(x, _("grep requires a string"))) + except re.error, e: + raise error.ParseError(_('invalid match pattern: %s') % e) + l = [] + for r in subset: + c = repo[r] + for e in c.files() + [c.user(), c.description()]: + if gr.search(e): + l.append(r) + break + return l + +def hasfile(repo, subset, x): + """``file(pattern)`` + Changesets affecting files matched by pattern. + """ + # i18n: "file" is a keyword + pat = getstring(x, _("file requires a pattern")) + m = matchmod.match(repo.root, repo.getcwd(), [pat]) + s = [] + for r in subset: + for f in repo[r].files(): + if m(f): + s.append(r) + break + return s + +def head(repo, subset, x): + """``head()`` + Changeset is a named branch head. + """ + # i18n: "head" is a keyword + getargs(x, 0, 0, _("head takes no arguments")) + hs = set() + for b, ls in repo.branchmap().iteritems(): + hs.update(repo[h].rev() for h in ls) + return [r for r in subset if r in hs] + +def heads(repo, subset, x): + """``heads(set)`` + Members of set with no children in set. + """ + s = getset(repo, subset, x) + ps = set(parents(repo, subset, x)) + return [r for r in s if r not in ps] + +def keyword(repo, subset, x): + """``keyword(string)`` + Search commit message, user name, and names of changed files for + string. + """ + # i18n: "keyword" is a keyword + kw = getstring(x, _("keyword requires a string")).lower() + l = [] + for r in subset: + c = repo[r] + t = " ".join(c.files() + [c.user(), c.description()]) + if kw in t.lower(): + l.append(r) + return l + +def limit(repo, subset, x): + """``limit(set, n)`` + First n members of set. + """ + # i18n: "limit" is a keyword + l = getargs(x, 2, 2, _("limit requires two arguments")) + try: + # i18n: "limit" is a keyword + lim = int(getstring(l[1], _("limit requires a number"))) + except ValueError: + # i18n: "limit" is a keyword + raise error.ParseError(_("limit expects a number")) + ss = set(subset) + os = getset(repo, range(len(repo)), l[0])[:lim] + return [r for r in os if r in ss] + +def last(repo, subset, x): + """``last(set, n)`` + Last n members of set. + """ + # i18n: "last" is a keyword + l = getargs(x, 2, 2, _("last requires two arguments")) + try: + # i18n: "last" is a keyword + lim = int(getstring(l[1], _("last requires a number"))) + except ValueError: + # i18n: "last" is a keyword + raise error.ParseError(_("last expects a number")) + ss = set(subset) + os = getset(repo, range(len(repo)), l[0])[-lim:] + return [r for r in os if r in ss] + +def maxrev(repo, subset, x): + """``max(set)`` + Changeset with highest revision number in set. + """ + os = getset(repo, range(len(repo)), x) + if os: + m = max(os) + if m in subset: + return [m] + return [] + +def merge(repo, subset, x): + """``merge()`` + Changeset is a merge changeset. + """ + # i18n: "merge" is a keyword + getargs(x, 0, 0, _("merge takes no arguments")) + cl = repo.changelog + return [r for r in subset if cl.parentrevs(r)[1] != -1] + +def minrev(repo, subset, x): + """``min(set)`` + Changeset with lowest revision number in set. + """ + os = getset(repo, range(len(repo)), x) + if os: + m = min(os) + if m in subset: + return [m] + return [] + +def modifies(repo, subset, x): + """``modifies(pattern)`` + Changesets modifying files matched by pattern. + """ + # i18n: "modifies" is a keyword + pat = getstring(x, _("modifies requires a pattern")) + return checkstatus(repo, subset, pat, 0) + def node(repo, subset, x): """``id(string)`` Revision non-ambiguously specified by the given hex string prefix. @@ -188,26 +541,35 @@ rn = repo.changelog.rev(repo.changelog._partialmatch(n)) return [r for r in subset if r == rn] -def rev(repo, subset, x): - """``rev(number)`` - Revision with the given numeric identifier. +def outgoing(repo, subset, x): + """``outgoing([path])`` + Changesets not found in the specified destination repository, or the + default push location. """ - # i18n: "rev" is a keyword - l = getargs(x, 1, 1, _("rev requires one argument")) - try: - # i18n: "rev" is a keyword - l = int(getstring(l[0], _("rev requires a number"))) - except ValueError: - # i18n: "rev" is a keyword - raise error.ParseError(_("rev expects a number")) - return [r for r in subset if r == l] + import hg # avoid start-up nasties + # i18n: "outgoing" is a keyword + l = getargs(x, 0, 1, _("outgoing requires a repository path")) + # i18n: "outgoing" is a keyword + dest = l and getstring(l[0], _("outgoing requires a repository path")) or '' + dest = repo.ui.expandpath(dest or 'default-push', dest or 'default') + dest, branches = hg.parseurl(dest) + revs, checkout = hg.addbranchrevs(repo, repo, branches, []) + if revs: + revs = [repo.lookup(rev) for rev in revs] + other = hg.repository(hg.remoteui(repo, {}), dest) + repo.ui.pushbuffer() + common, _anyinc, _heads = discovery.findcommonincoming(repo, other) + repo.ui.popbuffer() + cl = repo.changelog + o = set([cl.rev(r) for r in repo.changelog.findmissing(common, revs)]) + return [r for r in subset if r in o] def p1(repo, subset, x): """``p1([set])`` First parent of changesets in set, or the working directory. """ if x is None: - p = repo[x].parents()[0].rev() + p = repo[x].p1().rev() return [r for r in subset if r == p] ps = set() @@ -248,291 +610,30 @@ ps.update(cl.parentrevs(r)) return [r for r in subset if r in ps] -def maxrev(repo, subset, x): - """``max(set)`` - Changeset with highest revision number in set. - """ - s = getset(repo, subset, x) - if s: - m = max(s) - if m in subset: - return [m] - return [] - -def minrev(repo, subset, x): - """``min(set)`` - Changeset with lowest revision number in set. - """ - s = getset(repo, subset, x) - if s: - m = min(s) - if m in subset: - return [m] - return [] - -def limit(repo, subset, x): - """``limit(set, n)`` - First n members of set. - """ - # i18n: "limit" is a keyword - l = getargs(x, 2, 2, _("limit requires two arguments")) - try: - # i18n: "limit" is a keyword - lim = int(getstring(l[1], _("limit requires a number"))) - except ValueError: - # i18n: "limit" is a keyword - raise error.ParseError(_("limit expects a number")) - return getset(repo, subset, l[0])[:lim] - -def children(repo, subset, x): - """``children(set)`` - Child changesets of changesets in set. - """ - cs = set() - cl = repo.changelog - s = set(getset(repo, range(len(repo)), x)) - for r in xrange(0, len(repo)): - for p in cl.parentrevs(r): - if p in s: - cs.add(r) - return [r for r in subset if r in cs] - -def branch(repo, subset, x): - """``branch(set)`` - All changesets belonging to the branches of changesets in set. - """ - s = getset(repo, range(len(repo)), x) - b = set() - for r in s: - b.add(repo[r].branch()) - s = set(s) - return [r for r in subset if r in s or repo[r].branch() in b] - -def ancestor(repo, subset, x): - """``ancestor(single, single)`` - Greatest common ancestor of the two changesets. - """ - # i18n: "ancestor" is a keyword - l = getargs(x, 2, 2, _("ancestor requires two arguments")) - r = range(len(repo)) - a = getset(repo, r, l[0]) - b = getset(repo, r, l[1]) - if len(a) != 1 or len(b) != 1: - # i18n: "ancestor" is a keyword - raise error.ParseError(_("ancestor arguments must be single revisions")) - an = [repo[a[0]].ancestor(repo[b[0]]).rev()] - - return [r for r in an if r in subset] - -def ancestors(repo, subset, x): - """``ancestors(set)`` - Changesets that are ancestors of a changeset in set. - """ - args = getset(repo, range(len(repo)), x) - if not args: - return [] - s = set(repo.changelog.ancestors(*args)) | set(args) - return [r for r in subset if r in s] - -def descendants(repo, subset, x): - """``descendants(set)`` - Changesets which are descendants of changesets in set. - """ - args = getset(repo, range(len(repo)), x) - if not args: - return [] - s = set(repo.changelog.descendants(*args)) | set(args) - return [r for r in subset if r in s] - -def follow(repo, subset, x): - """``follow()`` - An alias for ``::.`` (ancestors of the working copy's first parent). - """ - # i18n: "follow" is a keyword - getargs(x, 0, 0, _("follow takes no arguments")) - p = repo['.'].rev() - s = set(repo.changelog.ancestors(p)) | set([p]) - return [r for r in subset if r in s] - -def date(repo, subset, x): - """``date(interval)`` - Changesets within the interval, see :hg:`help dates`. - """ - # i18n: "date" is a keyword - ds = getstring(x, _("date requires a string")) - dm = util.matchdate(ds) - return [r for r in subset if dm(repo[r].date()[0])] - -def keyword(repo, subset, x): - """``keyword(string)`` - Search commit message, user name, and names of changed files for - string. - """ - # i18n: "keyword" is a keyword - kw = getstring(x, _("keyword requires a string")).lower() - l = [] - for r in subset: - c = repo[r] - t = " ".join(c.files() + [c.user(), c.description()]) - if kw in t.lower(): - l.append(r) - return l - -def grep(repo, subset, x): - """``grep(regex)`` - Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')`` - to ensure special escape characters are handled correctly. +def parentspec(repo, subset, x, n): + """``set^0`` + The set. + ``set^1`` (or ``set^``), ``set^2`` + First or second parent, respectively, of all changesets in set. """ try: - # i18n: "grep" is a keyword - gr = re.compile(getstring(x, _("grep requires a string"))) - except re.error, e: - raise error.ParseError(_('invalid match pattern: %s') % e) - l = [] - for r in subset: - c = repo[r] - for e in c.files() + [c.user(), c.description()]: - if gr.search(e): - l.append(r) - continue - return l - -def author(repo, subset, x): - """``author(string)`` - Alias for ``user(string)``. - """ - # i18n: "author" is a keyword - n = getstring(x, _("author requires a string")).lower() - return [r for r in subset if n in repo[r].user().lower()] - -def user(repo, subset, x): - """``user(string)`` - User name is string. - """ - return author(repo, subset, x) - -def hasfile(repo, subset, x): - """``file(pattern)`` - Changesets affecting files matched by pattern. - """ - # i18n: "file" is a keyword - pat = getstring(x, _("file requires a pattern")) - m = matchmod.match(repo.root, repo.getcwd(), [pat]) - s = [] - for r in subset: - for f in repo[r].files(): - if m(f): - s.append(r) - continue - return s - -def contains(repo, subset, x): - """``contains(pattern)`` - Revision contains pattern. - """ - # i18n: "contains" is a keyword - pat = getstring(x, _("contains requires a pattern")) - m = matchmod.match(repo.root, repo.getcwd(), [pat]) - s = [] - if m.files() == [pat]: - for r in subset: - if pat in repo[r]: - s.append(r) - continue - else: - for r in subset: - for f in repo[r].manifest(): - if m(f): - s.append(r) - continue - return s - -def checkstatus(repo, subset, pat, field): - m = matchmod.match(repo.root, repo.getcwd(), [pat]) - s = [] - fast = (m.files() == [pat]) - for r in subset: - c = repo[r] - if fast: - if pat not in c.files(): - continue - else: - for f in c.files(): - if m(f): - break - else: - continue - files = repo.status(c.p1().node(), c.node())[field] - if fast: - if pat in files: - s.append(r) - continue - else: - for f in files: - if m(f): - s.append(r) - continue - return s - -def modifies(repo, subset, x): - """``modifies(pattern)`` - Changesets modifying files matched by pattern. - """ - # i18n: "modifies" is a keyword - pat = getstring(x, _("modifies requires a pattern")) - return checkstatus(repo, subset, pat, 0) - -def adds(repo, subset, x): - """``adds(pattern)`` - Changesets that add a file matching pattern. - """ - # i18n: "adds" is a keyword - pat = getstring(x, _("adds requires a pattern")) - return checkstatus(repo, subset, pat, 1) - -def removes(repo, subset, x): - """``removes(pattern)`` - Changesets which remove files matching pattern. - """ - # i18n: "removes" is a keyword - pat = getstring(x, _("removes requires a pattern")) - return checkstatus(repo, subset, pat, 2) - -def merge(repo, subset, x): - """``merge()`` - Changeset is a merge changeset. - """ - # i18n: "merge" is a keyword - getargs(x, 0, 0, _("merge takes no arguments")) + n = int(n[1]) + if n not in (0, 1, 2): + raise ValueError + except ValueError: + raise error.ParseError(_("^ expects a number 0, 1, or 2")) + ps = set() cl = repo.changelog - return [r for r in subset if cl.parentrevs(r)[1] != -1] - -def closed(repo, subset, x): - """``closed()`` - Changeset is closed. - """ - # i18n: "closed" is a keyword - getargs(x, 0, 0, _("closed takes no arguments")) - return [r for r in subset if repo[r].extra().get('close')] - -def head(repo, subset, x): - """``head()`` - Changeset is a named branch head. - """ - # i18n: "head" is a keyword - getargs(x, 0, 0, _("head takes no arguments")) - hs = set() - for b, ls in repo.branchmap().iteritems(): - hs.update(repo[h].rev() for h in ls) - return [r for r in subset if r in hs] - -def reverse(repo, subset, x): - """``reverse(set)`` - Reverse order of set. - """ - l = getset(repo, subset, x) - l.reverse() - return l + for r in getset(repo, subset, x): + if n == 0: + ps.add(r) + elif n == 1: + ps.add(cl.parentrevs(r)[0]) + elif n == 2: + parents = cl.parentrevs(r) + if len(parents) > 1: + ps.add(parents[1]) + return [r for r in subset if r in ps] def present(repo, subset, x): """``present(set)`` @@ -544,6 +645,44 @@ except error.RepoLookupError: return [] +def removes(repo, subset, x): + """``removes(pattern)`` + Changesets which remove files matching pattern. + """ + # i18n: "removes" is a keyword + pat = getstring(x, _("removes requires a pattern")) + return checkstatus(repo, subset, pat, 2) + +def rev(repo, subset, x): + """``rev(number)`` + Revision with the given numeric identifier. + """ + # i18n: "rev" is a keyword + l = getargs(x, 1, 1, _("rev requires one argument")) + try: + # i18n: "rev" is a keyword + l = int(getstring(l[0], _("rev requires a number"))) + except ValueError: + # i18n: "rev" is a keyword + raise error.ParseError(_("rev expects a number")) + return [r for r in subset if r == l] + +def reverse(repo, subset, x): + """``reverse(set)`` + Reverse order of set. + """ + l = getset(repo, subset, x) + l.reverse() + return l + +def roots(repo, subset, x): + """``roots(set)`` + Changesets with no parent changeset in set. + """ + s = getset(repo, subset, x) + cs = set(children(repo, subset, x)) + return [r for r in s if r not in cs] + def sort(repo, subset, x): """``sort(set[, [-]key...])`` Sort set by keys. The default sort order is ascending, specify a key @@ -599,53 +738,6 @@ l.sort() return [e[-1] for e in l] -def getall(repo, subset, x): - """``all()`` - All changesets, the same as ``0:tip``. - """ - # i18n: "all" is a keyword - getargs(x, 0, 0, _("all takes no arguments")) - return subset - -def heads(repo, subset, x): - """``heads(set)`` - Members of set with no children in set. - """ - s = getset(repo, subset, x) - ps = set(parents(repo, subset, x)) - return [r for r in s if r not in ps] - -def roots(repo, subset, x): - """``roots(set)`` - Changesets with no parent changeset in set. - """ - s = getset(repo, subset, x) - cs = set(children(repo, subset, x)) - return [r for r in s if r not in cs] - -def outgoing(repo, subset, x): - """``outgoing([path])`` - Changesets not found in the specified destination repository, or the - default push location. - """ - import hg # avoid start-up nasties - # i18n: "outgoing" is a keyword - l = getargs(x, 0, 1, _("outgoing requires a repository path")) - # i18n: "outgoing" is a keyword - dest = l and getstring(l[0], _("outgoing requires a repository path")) or '' - dest = repo.ui.expandpath(dest or 'default-push', dest or 'default') - dest, branches = hg.parseurl(dest) - revs, checkout = hg.addbranchrevs(repo, repo, branches, []) - if revs: - revs = [repo.lookup(rev) for rev in revs] - other = hg.repository(hg.remoteui(repo, {}), dest) - repo.ui.pushbuffer() - o = discovery.findoutgoing(repo, other) - repo.ui.popbuffer() - cl = repo.changelog - o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]]) - return [r for r in subset if r in o] - def tag(repo, subset, x): """``tag(name)`` The specified tag by name, or all tagged revisions if no name is given. @@ -657,6 +749,8 @@ tn = getstring(args[0], # i18n: "tag" is a keyword _('the argument to tag must be a string')) + if not repo.tags().get(tn, None): + raise util.Abort(_("tag '%s' does not exist") % tn) s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn]) else: s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip']) @@ -665,23 +759,11 @@ def tagged(repo, subset, x): return tag(repo, subset, x) -def bookmark(repo, subset, x): - """``bookmark([name])`` - The named bookmark or all bookmarks. +def user(repo, subset, x): + """``user(string)`` + User name is string. """ - # i18n: "bookmark" is a keyword - args = getargs(x, 0, 1, _('bookmark takes one or no arguments')) - if args: - bm = getstring(args[0], - # i18n: "bookmark" is a keyword - _('the argument to bookmark must be a string')) - bmrev = bookmarksmod.listbookmarks(repo).get(bm, None) - if bmrev: - bmrev = repo[bmrev].rev() - return [r for r in subset if r == bmrev] - bms = set([repo[r].rev() - for r in bookmarksmod.listbookmarks(repo).values()]) - return [r for r in subset if r in bms] + return author(repo, subset, x) symbols = { "adds": adds, @@ -689,6 +771,7 @@ "ancestor": ancestor, "ancestors": ancestors, "author": author, + "bisected": bisected, "bookmark": bookmark, "branch": branch, "children": children, @@ -702,6 +785,7 @@ "head": head, "heads": heads, "keyword": keyword, + "last": last, "limit": limit, "max": maxrev, "min": minrev, @@ -732,6 +816,9 @@ "not": notset, "list": listset, "func": func, + "ancestor": ancestorspec, + "parent": parentspec, + "parentpost": p1, } def optimize(x, small): @@ -777,16 +864,19 @@ elif op == 'not': o = optimize(x[1], not small) return o[0], (op, o[1]) + elif op == 'parentpost': + o = optimize(x[1], small) + return o[0], (op, o[1]) elif op == 'group': return optimize(x[1], small) - elif op in 'range list': + elif op in 'range list parent ancestorspec': wa, ta = optimize(x[1], small) wb, tb = optimize(x[2], small) return wa + wb, (op, ta, tb) elif op == 'func': f = getstring(x[1], _("not a symbol")) wa, ta = optimize(x[2], small) - if f in "grep date user author keyword branch file outgoing": + if f in "grep date user author keyword branch file outgoing closed": w = 10 # slow elif f in "modifies adds removes": w = 30 # slower @@ -803,31 +893,96 @@ return w + wa, (op, x[1], ta) return 1, x +class revsetalias(object): + funcre = re.compile('^([^(]+)\(([^)]+)\)$') + args = () + + def __init__(self, token, value): + '''Aliases like: + + h = heads(default) + b($1) = ancestors($1) - ancestors(default) + ''' + if isinstance(token, tuple): + self.type, self.name = token + else: + m = self.funcre.search(token) + if m: + self.type = 'func' + self.name = m.group(1) + self.args = [x.strip() for x in m.group(2).split(',')] + else: + self.type = 'symbol' + self.name = token + + if isinstance(value, str): + for arg in self.args: + value = value.replace(arg, repr(arg)) + self.replacement, pos = parse(value) + if pos != len(value): + raise error.ParseError('invalid token', pos) + else: + self.replacement = value + + def match(self, tree): + if not tree: + return False + if tree == (self.type, self.name): + return True + if tree[0] != self.type: + return False + if len(tree) > 1 and tree[1] != ('symbol', self.name): + return False + # 'func' + funcname + args + if ((self.args and len(tree) != 3) or + (len(self.args) == 1 and tree[2][0] == 'list') or + (len(self.args) > 1 and (tree[2][0] != 'list' or + len(tree[2]) - 1 != len(self.args)))): + raise error.ParseError('invalid amount of arguments', len(tree) - 2) + return True + + def replace(self, tree): + if tree == (self.type, self.name): + return self.replacement + result = self.replacement + def getsubtree(i): + if tree[2][0] == 'list': + return tree[2][i + 1] + return tree[i + 2] + for i, v in enumerate(self.args): + valalias = revsetalias(('string', v), getsubtree(i)) + result = valalias.process(result) + return result + + def process(self, tree): + if self.match(tree): + return self.replace(tree) + if isinstance(tree, tuple): + return tuple(map(self.process, tree)) + return tree + +def findaliases(ui, tree): + for k, v in ui.configitems('revsetalias'): + alias = revsetalias(k, v) + tree = alias.process(tree) + return tree + parse = parser.parser(tokenize, elements).parse -def match(spec): +def match(ui, spec): if not spec: raise error.ParseError(_("empty query")) - tree = parse(spec) + tree, pos = parse(spec) + if (pos != len(spec)): + raise error.ParseError("invalid token", pos) + tree = findaliases(ui, tree) weight, tree = optimize(tree, True) def mfunc(repo, subset): return getset(repo, subset, tree) return mfunc def makedoc(topic, doc): - """Generate and include predicates help in revsets topic.""" - predicates = [] - for name in sorted(symbols): - text = symbols[name].__doc__ - if not text: - continue - text = gettext(text.rstrip()) - lines = text.splitlines() - lines[1:] = [(' ' + l.strip()) for l in lines[1:]] - predicates.append('\n'.join(lines)) - predicates = '\n\n'.join(predicates) - doc = doc.replace('.. predicatesmarker', predicates) - return doc + return help.makeitemsdoc(topic, doc, '.. predicatesmarker', symbols) # tell hggettext to extract docstrings from these functions: i18nfunctions = symbols.values() diff -r 7b627923739f -r 680c3c6fcb48 mercurial/scmutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/scmutil.py Fri May 06 14:29:53 2011 +0200 @@ -0,0 +1,465 @@ +# scmutil.py - Mercurial core utility functions +# +# Copyright Matt Mackall +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from i18n import _ +import util, error, osutil +import os, errno, stat, sys + +def checkfilename(f): + '''Check that the filename f is an acceptable filename for a tracked file''' + if '\r' in f or '\n' in f: + raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f) + +def checkportable(ui, f): + '''Check if filename f is portable and warn or abort depending on config''' + checkfilename(f) + abort, warn = checkportabilityalert(ui) + if abort or warn: + msg = util.checkwinfilename(f) + if msg: + msg = "%s: %r" % (msg, f) + if abort: + raise util.Abort(msg) + ui.warn(_("warning: %s\n") % msg) + +def checkportabilityalert(ui): + '''check if the user's config requests nothing, a warning, or abort for + non-portable filenames''' + val = ui.config('ui', 'portablefilenames', 'warn') + lval = val.lower() + bval = util.parsebool(val) + abort = os.name == 'nt' or lval == 'abort' + warn = bval or lval == 'warn' + if bval is None and not (warn or abort or lval == 'ignore'): + raise error.ConfigError( + _("ui.portablefilenames value is invalid ('%s')") % val) + return abort, warn + +class casecollisionauditor(object): + def __init__(self, ui, abort, existingiter): + self._ui = ui + self._abort = abort + self._map = {} + for f in existingiter: + self._map[f.lower()] = f + + def __call__(self, f): + fl = f.lower() + map = self._map + if fl in map and map[fl] != f: + msg = _('possible case-folding collision for %s') % f + if self._abort: + raise util.Abort(msg) + self._ui.warn(_("warning: %s\n") % msg) + map[fl] = f + +class pathauditor(object): + '''ensure that a filesystem path contains no banned components. + the following properties of a path are checked: + + - ends with a directory separator + - under top-level .hg + - starts at the root of a windows drive + - contains ".." + - traverses a symlink (e.g. a/symlink_here/b) + - inside a nested repository (a callback can be used to approve + some nested repositories, e.g., subrepositories) + ''' + + def __init__(self, root, callback=None): + self.audited = set() + self.auditeddir = set() + self.root = root + self.callback = callback + + def __call__(self, path): + '''Check the relative path. + path may contain a pattern (e.g. foodir/**.txt)''' + + if path in self.audited: + return + # AIX ignores "/" at end of path, others raise EISDIR. + if util.endswithsep(path): + raise util.Abort(_("path ends in directory separator: %s") % path) + normpath = os.path.normcase(path) + parts = util.splitpath(normpath) + if (os.path.splitdrive(path)[0] + or parts[0].lower() in ('.hg', '.hg.', '') + or os.pardir in parts): + raise util.Abort(_("path contains illegal component: %s") % path) + if '.hg' in path.lower(): + lparts = [p.lower() for p in parts] + for p in '.hg', '.hg.': + if p in lparts[1:]: + pos = lparts.index(p) + base = os.path.join(*parts[:pos]) + raise util.Abort(_('path %r is inside nested repo %r') + % (path, base)) + + parts.pop() + prefixes = [] + while parts: + prefix = os.sep.join(parts) + if prefix in self.auditeddir: + break + curpath = os.path.join(self.root, prefix) + try: + st = os.lstat(curpath) + except OSError, err: + # EINVAL can be raised as invalid path syntax under win32. + # They must be ignored for patterns can be checked too. + if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL): + raise + else: + if stat.S_ISLNK(st.st_mode): + raise util.Abort( + _('path %r traverses symbolic link %r') + % (path, prefix)) + elif (stat.S_ISDIR(st.st_mode) and + os.path.isdir(os.path.join(curpath, '.hg'))): + if not self.callback or not self.callback(curpath): + raise util.Abort(_('path %r is inside nested repo %r') % + (path, prefix)) + prefixes.append(prefix) + parts.pop() + + self.audited.add(path) + # only add prefixes to the cache after checking everything: we don't + # want to add "foo/bar/baz" before checking if there's a "foo/.hg" + self.auditeddir.update(prefixes) + +class abstractopener(object): + """Abstract base class; cannot be instantiated""" + + def __init__(self, *args, **kwargs): + '''Prevent instantiation; don't call this from subclasses.''' + raise NotImplementedError('attempted instantiating ' + str(type(self))) + + def read(self, path): + fp = self(path, 'rb') + try: + return fp.read() + finally: + fp.close() + + def write(self, path, data): + fp = self(path, 'wb') + try: + return fp.write(data) + finally: + fp.close() + + def append(self, path, data): + fp = self(path, 'ab') + try: + return fp.write(data) + finally: + fp.close() + +class opener(abstractopener): + '''Open files relative to a base directory + + This class is used to hide the details of COW semantics and + remote file access from higher level code. + ''' + def __init__(self, base, audit=True): + self.base = base + if audit: + self.auditor = pathauditor(base) + else: + self.auditor = util.always + self.createmode = None + self._trustnlink = None + + @util.propertycache + def _can_symlink(self): + return util.checklink(self.base) + + def _fixfilemode(self, name): + if self.createmode is None: + return + os.chmod(name, self.createmode & 0666) + + def __call__(self, path, mode="r", text=False, atomictemp=False): + r = util.checkosfilename(path) + if r: + raise util.Abort("%s: %r" % (r, path)) + self.auditor(path) + f = os.path.join(self.base, path) + + if not text and "b" not in mode: + mode += "b" # for that other OS + + nlink = -1 + dirname, basename = os.path.split(f) + # If basename is empty, then the path is malformed because it points + # to a directory. Let the posixfile() call below raise IOError. + if basename and mode not in ('r', 'rb'): + if atomictemp: + if not os.path.isdir(dirname): + util.makedirs(dirname, self.createmode) + return util.atomictempfile(f, mode, self.createmode) + try: + if 'w' in mode: + util.unlink(f) + nlink = 0 + else: + # nlinks() may behave differently for files on Windows + # shares if the file is open. + fd = util.posixfile(f) + nlink = util.nlinks(f) + if nlink < 1: + nlink = 2 # force mktempcopy (issue1922) + fd.close() + except (OSError, IOError), e: + if e.errno != errno.ENOENT: + raise + nlink = 0 + if not os.path.isdir(dirname): + util.makedirs(dirname, self.createmode) + if nlink > 0: + if self._trustnlink is None: + self._trustnlink = nlink > 1 or util.checknlink(f) + if nlink > 1 or not self._trustnlink: + util.rename(util.mktempcopy(f), f) + fp = util.posixfile(f, mode) + if nlink == 0: + self._fixfilemode(f) + return fp + + def symlink(self, src, dst): + self.auditor(dst) + linkname = os.path.join(self.base, dst) + try: + os.unlink(linkname) + except OSError: + pass + + dirname = os.path.dirname(linkname) + if not os.path.exists(dirname): + util.makedirs(dirname, self.createmode) + + if self._can_symlink: + try: + os.symlink(src, linkname) + except OSError, err: + raise OSError(err.errno, _('could not symlink to %r: %s') % + (src, err.strerror), linkname) + else: + f = self(dst, "w") + f.write(src) + f.close() + self._fixfilemode(dst) + +class filteropener(abstractopener): + '''Wrapper opener for filtering filenames with a function.''' + + def __init__(self, opener, filter): + self._filter = filter + self._orig = opener + + def __call__(self, path, *args, **kwargs): + return self._orig(self._filter(path), *args, **kwargs) + +def canonpath(root, cwd, myname, auditor=None): + '''return the canonical path of myname, given cwd and root''' + if util.endswithsep(root): + rootsep = root + else: + rootsep = root + os.sep + name = myname + if not os.path.isabs(name): + name = os.path.join(root, cwd, name) + name = os.path.normpath(name) + if auditor is None: + auditor = pathauditor(root) + if name != rootsep and name.startswith(rootsep): + name = name[len(rootsep):] + auditor(name) + return util.pconvert(name) + elif name == root: + return '' + else: + # Determine whether `name' is in the hierarchy at or beneath `root', + # by iterating name=dirname(name) until that causes no change (can't + # check name == '/', because that doesn't work on windows). For each + # `name', compare dev/inode numbers. If they match, the list `rel' + # holds the reversed list of components making up the relative file + # name we want. + root_st = os.stat(root) + rel = [] + while True: + try: + name_st = os.stat(name) + except OSError: + break + if util.samestat(name_st, root_st): + if not rel: + # name was actually the same as root (maybe a symlink) + return '' + rel.reverse() + name = os.path.join(*rel) + auditor(name) + return util.pconvert(name) + dirname, basename = os.path.split(name) + rel.append(basename) + if dirname == name: + break + name = dirname + + raise util.Abort('%s not under root' % myname) + +def walkrepos(path, followsym=False, seen_dirs=None, recurse=False): + '''yield every hg repository under path, recursively.''' + def errhandler(err): + if err.filename == path: + raise err + if followsym and hasattr(os.path, 'samestat'): + def _add_dir_if_not_there(dirlst, dirname): + match = False + samestat = os.path.samestat + dirstat = os.stat(dirname) + for lstdirstat in dirlst: + if samestat(dirstat, lstdirstat): + match = True + break + if not match: + dirlst.append(dirstat) + return not match + else: + followsym = False + + if (seen_dirs is None) and followsym: + seen_dirs = [] + _add_dir_if_not_there(seen_dirs, path) + for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler): + dirs.sort() + if '.hg' in dirs: + yield root # found a repository + qroot = os.path.join(root, '.hg', 'patches') + if os.path.isdir(os.path.join(qroot, '.hg')): + yield qroot # we have a patch queue repo here + if recurse: + # avoid recursing inside the .hg directory + dirs.remove('.hg') + else: + dirs[:] = [] # don't descend further + elif followsym: + newdirs = [] + for d in dirs: + fname = os.path.join(root, d) + if _add_dir_if_not_there(seen_dirs, fname): + if os.path.islink(fname): + for hgname in walkrepos(fname, True, seen_dirs): + yield hgname + else: + newdirs.append(d) + dirs[:] = newdirs + +def os_rcpath(): + '''return default os-specific hgrc search path''' + path = system_rcpath() + path.extend(user_rcpath()) + path = [os.path.normpath(f) for f in path] + return path + +_rcpath = None + +def rcpath(): + '''return hgrc search path. if env var HGRCPATH is set, use it. + for each item in path, if directory, use files ending in .rc, + else use item. + make HGRCPATH empty to only look in .hg/hgrc of current repo. + if no HGRCPATH, use default os-specific path.''' + global _rcpath + if _rcpath is None: + if 'HGRCPATH' in os.environ: + _rcpath = [] + for p in os.environ['HGRCPATH'].split(os.pathsep): + if not p: + continue + p = util.expandpath(p) + if os.path.isdir(p): + for f, kind in osutil.listdir(p): + if f.endswith('.rc'): + _rcpath.append(os.path.join(p, f)) + else: + _rcpath.append(p) + else: + _rcpath = os_rcpath() + return _rcpath + +if os.name != 'nt': + + def rcfiles(path): + rcs = [os.path.join(path, 'hgrc')] + rcdir = os.path.join(path, 'hgrc.d') + try: + rcs.extend([os.path.join(rcdir, f) + for f, kind in osutil.listdir(rcdir) + if f.endswith(".rc")]) + except OSError: + pass + return rcs + + def system_rcpath(): + path = [] + # old mod_python does not set sys.argv + if len(getattr(sys, 'argv', [])) > 0: + path.extend(rcfiles(os.path.dirname(sys.argv[0]) + + '/../etc/mercurial')) + path.extend(rcfiles('/etc/mercurial')) + return path + + def user_rcpath(): + return [os.path.expanduser('~/.hgrc')] + +else: + + _HKEY_LOCAL_MACHINE = 0x80000002L + + def system_rcpath(): + '''return default os-specific hgrc search path''' + rcpath = [] + filename = util.executable_path() + # Use mercurial.ini found in directory with hg.exe + progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini') + if os.path.isfile(progrc): + rcpath.append(progrc) + return rcpath + # Use hgrc.d found in directory with hg.exe + progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d') + if os.path.isdir(progrcd): + for f, kind in osutil.listdir(progrcd): + if f.endswith('.rc'): + rcpath.append(os.path.join(progrcd, f)) + return rcpath + # else look for a system rcpath in the registry + value = util.lookup_reg('SOFTWARE\\Mercurial', None, + _HKEY_LOCAL_MACHINE) + if not isinstance(value, str) or not value: + return rcpath + value = value.replace('/', os.sep) + for p in value.split(os.pathsep): + if p.lower().endswith('mercurial.ini'): + rcpath.append(p) + elif os.path.isdir(p): + for f, kind in osutil.listdir(p): + if f.endswith('.rc'): + rcpath.append(os.path.join(p, f)) + return rcpath + + def user_rcpath(): + '''return os-specific hgrc search path to the user dir''' + home = os.path.expanduser('~') + path = [os.path.join(home, 'mercurial.ini'), + os.path.join(home, '.hgrc')] + userprofile = os.environ.get('USERPROFILE') + if userprofile: + path.append(os.path.join(userprofile, 'mercurial.ini')) + path.append(os.path.join(userprofile, '.hgrc')) + return path diff -r 7b627923739f -r 680c3c6fcb48 mercurial/setdiscovery.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/setdiscovery.py Fri May 06 14:29:53 2011 +0200 @@ -0,0 +1,178 @@ +# setdiscovery.py - improved discovery of common nodeset for mercurial +# +# Copyright 2010 Benoit Boissinot +# and Peter Arrenbrecht +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from node import nullid +from i18n import _ +import random, collections, util, dagutil + +def _updatesample(dag, nodes, sample, always, quicksamplesize=0): + # if nodes is empty we scan the entire graph + if nodes: + heads = dag.headsetofconnecteds(nodes) + else: + heads = dag.heads() + dist = {} + visit = collections.deque(heads) + seen = set() + factor = 1 + while visit: + curr = visit.popleft() + if curr in seen: + continue + d = dist.setdefault(curr, 1) + if d > factor: + factor *= 2 + if d == factor: + if curr not in always: # need this check for the early exit below + sample.add(curr) + if quicksamplesize and (len(sample) >= quicksamplesize): + return + seen.add(curr) + for p in dag.parents(curr): + if not nodes or p in nodes: + dist.setdefault(p, d + 1) + visit.append(p) + +def _setupsample(dag, nodes, size): + if len(nodes) <= size: + return set(nodes), None, 0 + always = set(dag.heads()) + desiredlen = size - len(always) + if desiredlen <= 0: + # This could be bad if there are very many heads, all unknown to the + # server. We're counting on long request support here. + return always, None, desiredlen + return always, set(), desiredlen + +def _takequicksample(dag, nodes, size, initial): + always, sample, desiredlen = _setupsample(dag, nodes, size) + if sample is None: + return always + if initial: + fromset = None + else: + fromset = nodes + _updatesample(dag, fromset, sample, always, quicksamplesize=desiredlen) + sample.update(always) + return sample + +def _takefullsample(dag, nodes, size): + always, sample, desiredlen = _setupsample(dag, nodes, size) + if sample is None: + return always + # update from heads + _updatesample(dag, nodes, sample, always) + # update from roots + _updatesample(dag.inverse(), nodes, sample, always) + assert sample + if len(sample) > desiredlen: + sample = set(random.sample(sample, desiredlen)) + elif len(sample) < desiredlen: + more = desiredlen - len(sample) + sample.update(random.sample(list(nodes - sample - always), more)) + sample.update(always) + return sample + +def findcommonheads(ui, local, remote, + initialsamplesize=100, + fullsamplesize=200, + abortwhenunrelated=True): + '''Return a tuple (common, anyincoming, remoteheads) used to identify + missing nodes from or in remote. + + shortcutlocal determines whether we try use direct access to localrepo if + remote is actually local. + ''' + roundtrips = 0 + cl = local.changelog + dag = dagutil.revlogdag(cl) + nodes = dag.nodeset() + + # early exit if we know all the specified server heads already + ui.debug("query 1; heads\n") + roundtrips += 1 + srvheadhashes = remote.heads() + + ## TODO We might want to request an additional random sample of the server's + ## nodes batched with the heads query here. + + if cl.tip() == nullid: + if srvheadhashes != [nullid]: + return [nullid], True, srvheadhashes + return [nullid], False, [] + + # start actual discovery (we note this before the next "if" for + # compatibility reasons) + ui.status(_("searching for changes\n")) + + srvheads = dag.internalizeall(srvheadhashes, filterunknown=True) + if len(srvheads) == len(srvheadhashes): + ui.note("all remote heads known locally\n") + return (srvheadhashes, False, srvheadhashes,) + + # full blown discovery + undecided = nodes # own nodes where I don't know if the server knows them + common = set() # own nodes I know we both know + missing = set() # own nodes I know the server lacks + + # treat remote heads as a first implicit sample response + common.update(dag.ancestorset(srvheads)) + undecided.difference_update(common) + # use cheapish initial sample + if common: + ui.debug("taking initial sample\n") + sample = _takefullsample(dag, undecided, size=fullsamplesize) + else: + ui.debug("taking quick initial sample\n") + sample = _takequicksample(dag, nodes, size=initialsamplesize, + initial=True) + + roundtrips += 1 + ui.progress(_('searching'), roundtrips, unit=_('queries')) + ui.debug("query %i; still undecided: %i, sample size is: %i\n" + % (roundtrips, len(undecided), len(sample))) + # indices between sample and externalized version must match + sample = list(sample) + yesno = remote.known(dag.externalizeall(sample)) + + while undecided: + commoninsample = set(n for i, n in enumerate(sample) if yesno[i]) + common.update(dag.ancestorset(commoninsample, common)) + + missinginsample = [n for i, n in enumerate(sample) if not yesno[i]] + missing.update(dag.descendantset(missinginsample, missing)) + + undecided.difference_update(missing) + undecided.difference_update(common) + + if not undecided: + break + + ui.note("sampling from both directions\n") + sample = _takefullsample(dag, undecided, size=fullsamplesize) + + roundtrips += 1 + ui.progress(_('searching'), roundtrips, unit=_('queries')) + ui.debug("query %i; still undecided: %i, sample size is: %i\n" + % (roundtrips, len(undecided), len(sample))) + # indices between sample and externalized version must match + sample = list(sample) + yesno = remote.known(dag.externalizeall(sample)) + + result = dag.headsetofconnecteds(common) + ui.progress(_('searching'), None) + ui.debug("%d total queries\n" % roundtrips) + + if not result and srvheadhashes != [nullid]: + if abortwhenunrelated: + raise util.Abort(_("repository is unrelated")) + else: + ui.warn(_("warning: repository is unrelated\n")) + return (set([nullid]), True, srvheadhashes,) + + return (dag.externalizeall(result), True, srvheadhashes,) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/simplemerge.py --- a/mercurial/simplemerge.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/simplemerge.py Fri May 06 14:29:53 2011 +0200 @@ -18,7 +18,7 @@ # s: "i hate that." from i18n import _ -import util, mdiff +import scmutil, util, mdiff import sys, os class CantReprocessAndShowBase(Exception): @@ -429,7 +429,7 @@ local = os.path.realpath(local) if not opts.get('print'): - opener = util.opener(os.path.dirname(local)) + opener = scmutil.opener(os.path.dirname(local)) out = opener(os.path.basename(local), "w", atomictemp=True) else: out = sys.stdout diff -r 7b627923739f -r 680c3c6fcb48 mercurial/sshrepo.py --- a/mercurial/sshrepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/sshrepo.py Fri May 06 14:29:53 2011 +0200 @@ -7,7 +7,6 @@ from i18n import _ import util, error, wireproto -import re class remotelock(object): def __init__(self, repo): @@ -24,16 +23,16 @@ self._url = path self.ui = ui - m = re.match(r'^ssh://(([^@]+)@)?([^:/]+)(:(\d+))?(/(.*))?$', path) - if not m: + u = util.url(path, parsequery=False, parsefragment=False) + if u.scheme != 'ssh' or not u.host or u.path is None: self._abort(error.RepoError(_("couldn't parse location %s") % path)) - self.user = m.group(2) - if self.user and ':' in self.user: + self.user = u.user + if u.passwd is not None: self._abort(error.RepoError(_("password in URL not supported"))) - self.host = m.group(3) - self.port = m.group(5) - self.path = m.group(7) or "." + self.host = u.host + self.port = u.port + self.path = u.path or "." sshcmd = self.ui.config("ui", "ssh", "ssh") remotecmd = self.ui.config("ui", "remotecmd", "hg") @@ -119,9 +118,24 @@ def _callstream(self, cmd, **args): self.ui.debug("sending %s command\n" % cmd) self.pipeo.write("%s\n" % cmd) - for k, v in sorted(args.iteritems()): + _func, names = wireproto.commands[cmd] + keys = names.split() + wireargs = {} + for k in keys: + if k == '*': + wireargs['*'] = args + break + else: + wireargs[k] = args[k] + del args[k] + for k, v in sorted(wireargs.iteritems()): self.pipeo.write("%s %d\n" % (k, len(v))) - self.pipeo.write(v) + if isinstance(v, dict): + for dk, dv in v.iteritems(): + self.pipeo.write("%s %d\n" % (dk, len(dv))) + self.pipeo.write(dv) + else: + self.pipeo.write(v) self.pipeo.flush() return self.pipei @@ -153,7 +167,7 @@ self.readerr() try: l = int(l) - except: + except ValueError: self._abort(error.ResponseError(_("unexpected response:"), l)) return self.pipei.read(l) @@ -194,7 +208,7 @@ return 1 try: return int(r) - except: + except ValueError: self._abort(error.ResponseError(_("unexpected response:"), r)) instance = sshrepository diff -r 7b627923739f -r 680c3c6fcb48 mercurial/sshserver.py --- a/mercurial/sshserver.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/sshserver.py Fri May 06 14:29:53 2011 +0200 @@ -27,21 +27,21 @@ def getargs(self, args): data = {} keys = args.split() - count = len(keys) for n in xrange(len(keys)): argline = self.fin.readline()[:-1] arg, l = argline.split() - val = self.fin.read(int(l)) if arg not in keys: raise util.Abort("unexpected parameter %r" % arg) if arg == '*': star = {} - for n in xrange(int(l)): + for k in xrange(int(l)): + argline = self.fin.readline()[:-1] arg, l = argline.split() val = self.fin.read(int(l)) star[arg] = val data['*'] = star else: + val = self.fin.read(int(l)) data[arg] = val return [data[k] for k in keys] diff -r 7b627923739f -r 680c3c6fcb48 mercurial/sslutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mercurial/sslutil.py Fri May 06 14:29:53 2011 +0200 @@ -0,0 +1,126 @@ +# sslutil.py - SSL handling for mercurial +# +# Copyright 2005, 2006, 2007, 2008 Matt Mackall +# Copyright 2006, 2007 Alexis S. L. Carvalho +# Copyright 2006 Vadim Gelfer +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +import os + +from mercurial import util +from mercurial.i18n import _ +try: + # avoid using deprecated/broken FakeSocket in python 2.6 + import ssl + ssl_wrap_socket = ssl.wrap_socket + CERT_REQUIRED = ssl.CERT_REQUIRED +except ImportError: + CERT_REQUIRED = 2 + + def ssl_wrap_socket(sock, key_file, cert_file, + cert_reqs=CERT_REQUIRED, ca_certs=None): + if ca_certs: + raise util.Abort(_( + 'certificate checking requires Python 2.6')) + + ssl = socket.ssl(sock, key_file, cert_file) + return httplib.FakeSocket(sock, ssl) + +def _verifycert(cert, hostname): + '''Verify that cert (in socket.getpeercert() format) matches hostname. + CRLs is not handled. + + Returns error message if any problems are found and None on success. + ''' + if not cert: + return _('no certificate received') + dnsname = hostname.lower() + def matchdnsname(certname): + return (certname == dnsname or + '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]) + + san = cert.get('subjectAltName', []) + if san: + certnames = [value.lower() for key, value in san if key == 'DNS'] + for name in certnames: + if matchdnsname(name): + return None + return _('certificate is for %s') % ', '.join(certnames) + + # subject is only checked when subjectAltName is empty + for s in cert.get('subject', []): + key, value = s[0] + if key == 'commonName': + try: + # 'subject' entries are unicode + certname = value.lower().encode('ascii') + except UnicodeEncodeError: + return _('IDN in certificate not supported') + if matchdnsname(certname): + return None + return _('certificate is for %s') % certname + return _('no commonName or subjectAltName found in certificate') + + +# CERT_REQUIRED means fetch the cert from the server all the time AND +# validate it against the CA store provided in web.cacerts. +# +# We COMPLETELY ignore CERT_REQUIRED on Python <= 2.5, as it's totally +# busted on those versions. + +def sslkwargs(ui, host): + cacerts = ui.config('web', 'cacerts') + hostfingerprint = ui.config('hostfingerprints', host) + if cacerts and not hostfingerprint: + cacerts = util.expandpath(cacerts) + if not os.path.exists(cacerts): + raise util.Abort(_('could not find web.cacerts: %s') % cacerts) + return {'ca_certs': cacerts, + 'cert_reqs': CERT_REQUIRED, + } + return {} + +class validator(object): + def __init__(self, ui, host): + self.ui = ui + self.host = host + + def __call__(self, sock): + host = self.host + cacerts = self.ui.config('web', 'cacerts') + hostfingerprint = self.ui.config('hostfingerprints', host) + if cacerts and not hostfingerprint: + msg = _verifycert(sock.getpeercert(), host) + if msg: + raise util.Abort(_('%s certificate error: %s ' + '(use --insecure to connect ' + 'insecurely)') % (host, msg)) + self.ui.debug('%s certificate successfully verified\n' % host) + else: + if getattr(sock, 'getpeercert', False): + peercert = sock.getpeercert(True) + peerfingerprint = util.sha1(peercert).hexdigest() + nicefingerprint = ":".join([peerfingerprint[x:x + 2] + for x in xrange(0, len(peerfingerprint), 2)]) + if hostfingerprint: + if peerfingerprint.lower() != \ + hostfingerprint.replace(':', '').lower(): + raise util.Abort(_('invalid certificate for %s ' + 'with fingerprint %s') % + (host, nicefingerprint)) + self.ui.debug('%s certificate matched fingerprint %s\n' % + (host, nicefingerprint)) + else: + self.ui.warn(_('warning: %s certificate ' + 'with fingerprint %s not verified ' + '(check hostfingerprints or web.cacerts ' + 'config setting)\n') % + (host, nicefingerprint)) + else: # python 2.5 ? + if hostfingerprint: + raise util.Abort(_('no certificate for %s with ' + 'configured hostfingerprint') % host) + self.ui.warn(_('warning: %s certificate not verified ' + '(check web.cacerts config setting)\n') % + host) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/statichttprepo.py --- a/mercurial/statichttprepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/statichttprepo.py Fri May 06 14:29:53 2011 +0200 @@ -9,7 +9,7 @@ from i18n import _ import changelog, byterange, url, error -import localrepo, manifest, util, store +import localrepo, manifest, util, scmutil, store import urllib, urllib2, errno class httprangereader(object): @@ -67,17 +67,17 @@ urlopener = url.opener(ui, authinfo) urlopener.add_handler(byterange.HTTPRangeHandler()) - def opener(base): - """return a function that opens files over http""" - p = base - def o(path, mode="r", atomictemp=None): - if 'a' in mode or 'w' in mode: + class statichttpopener(scmutil.abstractopener): + def __init__(self, base): + self.base = base + + def __call__(self, path, mode="r", atomictemp=None): + if mode not in ('r', 'rb'): raise IOError('Permission denied') - f = "/".join((p, urllib.quote(path))) + f = "/".join((self.base, urllib.quote(path))) return httprangereader(f, urlopener) - return o - return opener + return statichttpopener class statichttprepository(localrepo.localrepository): def __init__(self, ui, path): @@ -85,14 +85,15 @@ self.ui = ui self.root = path - self.path, authinfo = url.getauthinfo(path.rstrip('/') + "/.hg") + u = util.url(path.rstrip('/') + "/.hg") + self.path, authinfo = u.authinfo() opener = build_opener(ui, authinfo) self.opener = opener(self.path) # find requirements try: - requirements = self.opener("requires").read().splitlines() + requirements = self.opener.read("requires").splitlines() except IOError, inst: if inst.errno != errno.ENOENT: raise diff -r 7b627923739f -r 680c3c6fcb48 mercurial/store.py --- a/mercurial/store.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/store.py Fri May 06 14:29:53 2011 +0200 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. from i18n import _ -import osutil, util +import osutil, scmutil, util import os, stat _sha = util.sha1 @@ -14,6 +14,14 @@ # This avoids a collision between a file named foo and a dir named # foo.i or foo.d def encodedir(path): + ''' + >>> encodedir('data/foo.i') + 'data/foo.i' + >>> encodedir('data/foo.i/bla.i') + 'data/foo.i.hg/bla.i' + >>> encodedir('data/foo.i.hg/bla.i') + 'data/foo.i.hg.hg/bla.i' + ''' if not path.startswith('data/'): return path return (path @@ -22,6 +30,14 @@ .replace(".d/", ".d.hg/")) def decodedir(path): + ''' + >>> decodedir('data/foo.i') + 'data/foo.i' + >>> decodedir('data/foo.i.hg/bla.i') + 'data/foo.i/bla.i' + >>> decodedir('data/foo.i.hg.hg/bla.i') + 'data/foo.i.hg/bla.i' + ''' if not path.startswith('data/') or ".hg/" not in path: return path return (path @@ -30,6 +46,29 @@ .replace(".hg.hg/", ".hg/")) def _buildencodefun(): + ''' + >>> enc, dec = _buildencodefun() + + >>> enc('nothing/special.txt') + 'nothing/special.txt' + >>> dec('nothing/special.txt') + 'nothing/special.txt' + + >>> enc('HELLO') + '_h_e_l_l_o' + >>> dec('_h_e_l_l_o') + 'HELLO' + + >>> enc('hello:world?') + 'hello~3aworld~3f' + >>> dec('hello~3aworld~3f') + 'hello:world?' + + >>> enc('the\x07quick\xADshot') + 'the~07quick~adshot' + >>> dec('the~07quick~adshot') + 'the\\x07quick\\xadshot' + ''' e = '_' win_reserved = [ord(x) for x in '\\:*?"<>|'] cmap = dict([(chr(x), chr(x)) for x in xrange(127)]) @@ -58,6 +97,17 @@ encodefilename, decodefilename = _buildencodefun() def _build_lower_encodefun(): + ''' + >>> f = _build_lower_encodefun() + >>> f('nothing/special.txt') + 'nothing/special.txt' + >>> f('HELLO') + 'hello' + >>> f('hello:world?') + 'hello~3aworld~3f' + >>> f('the\x07quick\xADshot') + 'the~07quick~adshot' + ''' win_reserved = [ord(x) for x in '\\:*?"<>|'] cmap = dict([(chr(x), chr(x)) for x in xrange(127)]) for x in (range(32) + range(126, 256) + win_reserved): @@ -72,6 +122,23 @@ com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split() def _auxencode(path, dotencode): + ''' + Encodes filenames containing names reserved by Windows or which end in + period or space. Does not touch other single reserved characters c. + Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here. + Additionally encodes space or period at the beginning, if dotencode is + True. + path is assumed to be all lowercase. + + >>> _auxencode('.foo/aux.txt/txt.aux/con/prn/nul/foo.', True) + '~2efoo/au~78.txt/txt.aux/co~6e/pr~6e/nu~6c/foo~2e' + >>> _auxencode('.com1com2/lpt9.lpt4.lpt1/conprn/foo.', False) + '.com1com2/lp~749.lpt4.lpt1/conprn/foo~2e' + >>> _auxencode('foo. ', True) + 'foo.~20' + >>> _auxencode(' .foo', True) + '~20.foo' + ''' res = [] for n in path.split('/'): if n: @@ -169,12 +236,12 @@ class basicstore(object): '''base class for local repository stores''' - def __init__(self, path, opener): + def __init__(self, path, openertype): self.path = path self.createmode = _calcmode(path) - op = opener(self.path) + op = openertype(self.path) op.createmode = self.createmode - self.opener = lambda f, *args, **kw: op(encodedir(f), *args, **kw) + self.opener = scmutil.filteropener(op, encodedir) def join(self, f): return self.path + '/' + encodedir(f) @@ -218,12 +285,12 @@ pass class encodedstore(basicstore): - def __init__(self, path, opener): + def __init__(self, path, openertype): self.path = path + '/store' self.createmode = _calcmode(self.path) - op = opener(self.path) + op = openertype(self.path) op.createmode = self.createmode - self.opener = lambda f, *args, **kw: op(encodefilename(f), *args, **kw) + self.opener = scmutil.filteropener(op, encodefilename) def datafiles(self): for a, b, size in self._walk('data', True): @@ -298,21 +365,27 @@ self._load() return iter(self.entries) +class _fncacheopener(scmutil.abstractopener): + def __init__(self, op, fnc, encode): + self.opener = op + self.fncache = fnc + self.encode = encode + + def __call__(self, path, mode='r', *args, **kw): + if mode not in ('r', 'rb') and path.startswith('data/'): + self.fncache.add(path) + return self.opener(self.encode(path), mode, *args, **kw) + class fncachestore(basicstore): - def __init__(self, path, opener, encode): + def __init__(self, path, openertype, encode): self.encode = encode self.path = path + '/store' self.createmode = _calcmode(self.path) - op = opener(self.path) + op = openertype(self.path) op.createmode = self.createmode fnc = fncache(op) self.fncache = fnc - - def fncacheopener(path, mode='r', *args, **kw): - if mode not in ('r', 'rb') and path.startswith('data/'): - fnc.add(path) - return op(self.encode(path), mode, *args, **kw) - self.opener = fncacheopener + self.opener = _fncacheopener(op, fnc, encode) def join(self, f): return self.path + '/' + self.encode(f) @@ -344,11 +417,11 @@ def write(self): self.fncache.write() -def store(requirements, path, opener): +def store(requirements, path, openertype): if 'store' in requirements: if 'fncache' in requirements: auxencode = lambda f: _auxencode(f, 'dotencode' in requirements) encode = lambda f: _hybridencode(f, auxencode) - return fncachestore(path, opener, encode) - return encodedstore(path, opener) - return basicstore(path, opener) + return fncachestore(path, openertype, encode) + return encodedstore(path, openertype) + return basicstore(path, openertype) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/subrepo.py --- a/mercurial/subrepo.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/subrepo.py Fri May 06 14:29:53 2011 +0200 @@ -5,10 +5,10 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath +import errno, os, re, xml.dom.minidom, shutil, posixpath import stat, subprocess, tarfile from i18n import _ -import config, util, node, error, cmdutil +import config, scmutil, util, node, error, cmdutil, bookmarks hg = None propertycache = util.propertycache @@ -145,7 +145,7 @@ debug(s, "prompt remove") wctx.sub(s).remove() - for s, r in s2.items(): + for s, r in sorted(s2.items()): if s in s1: continue elif s not in sa: @@ -194,26 +194,16 @@ """return pull/push path of repo - either based on parent repo .hgsub info or on the top repo config. Abort or return None if no source found.""" if hasattr(repo, '_subparent'): - source = repo._subsource - if source.startswith('/') or '://' in source: - return source + source = util.url(repo._subsource) + source.path = posixpath.normpath(source.path) + if posixpath.isabs(source.path) or source.scheme: + return str(source) parent = _abssource(repo._subparent, push, abort=False) if parent: - if '://' in parent: - if parent[-1] == '/': - parent = parent[:-1] - r = urlparse.urlparse(parent + '/' + source) - if parent.startswith('ssh://') and not r[1]: - # Python before 2.6 parses ssh:// URLs wrong - host, path = r[2][2:].split('/', 1) - r2 = '//%s/%s' % (host, posixpath.normpath(path)) - else: - r2 = posixpath.normpath(r[2]) - r = urlparse.urlunparse((r[0], r[1], r2, - r[3], r[4], r[5])) - return r - else: # plain file system path - return posixpath.normpath(os.path.join(parent, repo._subsource)) + parent = util.url(parent) + parent.path = posixpath.join(parent.path, source.path) + parent.path = posixpath.normpath(parent.path) + return str(parent) else: # recursion reached top repo if hasattr(repo, '_subtoppath'): return repo._subtoppath @@ -245,7 +235,7 @@ import hg as h hg = h - util.path_auditor(ctx._repo.root)(path) + scmutil.pathauditor(ctx._repo.root)(path) state = ctx.substate.get(path, nullstate) if state[2] not in types: raise util.Abort(_('unknown subrepo type %s') % state[2]) @@ -438,15 +428,14 @@ def _get(self, state): source, revision, kind = state - try: - self._repo.lookup(revision) - except error.RepoError: + if revision not in self._repo: self._repo._subsource = source srcurl = _abssource(self._repo) self._repo.ui.status(_('pulling subrepo %s from %s\n') % (subrelpath(self), srcurl)) other = hg.repository(self._repo.ui, srcurl) self._repo.pull(other) + bookmarks.updatefromremote(self._repo.ui, self._repo, other) def get(self, state, overwrite=False): self._get(state) @@ -742,6 +731,12 @@ current = None return current + def _gitremote(self, remote): + out = self._gitcommand(['remote', 'show', '-n', remote]) + line = out.split('\n')[1] + i = line.index('URL: ') + len('URL: ') + return line[i:] + def _githavelocally(self, revision): out, code = self._gitdir(['cat-file', '-e', revision]) return code == 0 @@ -795,11 +790,14 @@ def _fetch(self, source, revision): if self._gitmissing(): - self._ui.status(_('cloning subrepo %s\n') % self._relpath) - self._gitnodir(['clone', self._abssource(source), self._abspath]) + source = self._abssource(source) + self._ui.status(_('cloning subrepo %s from %s\n') % + (self._relpath, source)) + self._gitnodir(['clone', source, self._abspath]) if self._githavelocally(revision): return - self._ui.status(_('pulling subrepo %s\n') % self._relpath) + self._ui.status(_('pulling subrepo %s from %s\n') % + (self._relpath, self._gitremote('origin'))) # try only origin: the originally cloned repo self._gitcommand(['fetch']) if not self._githavelocally(revision): @@ -827,7 +825,7 @@ return elif self._gitstate() == revision: if overwrite: - # first reset the index to unmark new files for commit, because + # first reset the index to unmark new files for commit, because # reset --hard will otherwise throw away files added for commit, # not just unmark them. self._gitcommand(['reset', 'HEAD']) diff -r 7b627923739f -r 680c3c6fcb48 mercurial/tags.py --- a/mercurial/tags.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/tags.py Fri May 06 14:29:53 2011 +0200 @@ -12,9 +12,9 @@ from node import nullid, bin, hex, short from i18n import _ -import os.path import encoding import error +import errno def findglobaltags(ui, repo, alltags, tagtypes): '''Find global tags in repo by reading .hgtags from every head that @@ -60,15 +60,18 @@ def readlocaltags(ui, repo, alltags, tagtypes): '''Read local tags in repo. Update alltags and tagtypes.''' try: - # localtags is in the local encoding; re-encode to UTF-8 on - # input for consistency with the rest of this module. - data = repo.opener("localtags").read() - filetags = _readtags( - ui, repo, data.splitlines(), "localtags", - recode=encoding.fromlocal) - _updatetags(filetags, "local", alltags, tagtypes) - except IOError: - pass + data = repo.opener.read("localtags") + except IOError, inst: + if inst.errno != errno.ENOENT: + raise + return + + # localtags is in the local encoding; re-encode to UTF-8 on + # input for consistency with the rest of this module. + filetags = _readtags( + ui, repo, data.splitlines(), "localtags", + recode=encoding.fromlocal) + _updatetags(filetags, "local", alltags, tagtypes) def _readtags(ui, repo, lines, fn, recode=None): '''Read tag definitions from a file (or any source of lines). @@ -185,7 +188,7 @@ if len(line) == 3: fnode = bin(line[2]) cachefnode[headnode] = fnode - except (ValueError, TypeError): + except Exception: # corruption of the tags cache, just recompute it ui.warn(_('.hg/cache/tags is corrupt, rebuilding it\n')) cacheheads = [] diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templatefilters.py --- a/mercurial/templatefilters.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templatefilters.py Fri May 06 14:29:53 2011 +0200 @@ -6,13 +6,13 @@ # GNU General Public License version 2 or any later version. import cgi, re, os, time, urllib -import encoding, node, util +import encoding, node, util, help -def stringify(thing): - '''turn nested template iterator into string.''' - if hasattr(thing, '__iter__') and not isinstance(thing, str): - return "".join([stringify(t) for t in thing if t is not None]) - return str(thing) +def addbreaks(text): + """:addbreaks: Any text. Add an XHTML "
" tag before the end of + every line except the last. + """ + return text.replace('\n', '
\n') agescales = [("year", 3600 * 24 * 365), ("month", 3600 * 24 * 30), @@ -23,7 +23,9 @@ ("second", 1)] def age(date): - '''turn a (timestamp, tzoff) tuple into an age string.''' + """:age: Date. Returns a human-readable date/time difference between the + given date/time and the current date/time. + """ def plural(t, c): if c == 1: @@ -34,18 +36,65 @@ now = time.time() then = date[0] + future = False if then > now: - return 'in the future' - - delta = max(1, int(now - then)) - if delta > agescales[0][1] * 2: - return util.shortdate(date) + future = True + delta = max(1, int(then - now)) + if delta > agescales[0][1] * 30: + return 'in the distant future' + else: + delta = max(1, int(now - then)) + if delta > agescales[0][1] * 2: + return util.shortdate(date) for t, s in agescales: n = delta // s if n >= 2 or s == 1: + if future: + return '%s from now' % fmt(t, n) return '%s ago' % fmt(t, n) +def basename(path): + """:basename: Any text. Treats the text as a path, and returns the last + component of the path after splitting by the path separator + (ignoring trailing separators). For example, "foo/bar/baz" becomes + "baz" and "foo/bar//" becomes "bar". + """ + return os.path.basename(path) + +def datefilter(text): + """:date: Date. Returns a date in a Unix date format, including the + timezone: "Mon Sep 04 15:13:13 2006 0700". + """ + return util.datestr(text) + +def domain(author): + """:domain: Any text. Finds the first string that looks like an email + address, and extracts just the domain component. Example: ``User + `` becomes ``example.com``. + """ + f = author.find('@') + if f == -1: + return '' + author = author[f + 1:] + f = author.find('>') + if f >= 0: + author = author[:f] + return author + +def email(text): + """:email: Any text. Extracts the first string that looks like an email + address. Example: ``User `` becomes + ``user@example.com``. + """ + return util.email(text) + +def escape(text): + """:escape: Any text. Replaces the special XML/XHTML characters "&", "<" + and ">" with XML entities. + """ + return cgi.escape(text, True) + para_re = None space_re = None @@ -74,40 +123,45 @@ return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest for para, rest in findparas()]) +def fill68(text): + """:fill68: Any text. Wraps the text to fit in 68 columns.""" + return fill(text, 68) + +def fill76(text): + """:fill76: Any text. Wraps the text to fit in 76 columns.""" + return fill(text, 76) + def firstline(text): - '''return the first line of text''' + """:firstline: Any text. Returns the first line of text.""" try: return text.splitlines(True)[0].rstrip('\r\n') except IndexError: return '' -def nl2br(text): - '''replace raw newlines with xhtml line breaks.''' - return text.replace('\n', '
\n') +def hexfilter(text): + """:hex: Any text. Convert a binary Mercurial node identifier into + its long hexadecimal representation. + """ + return node.hex(text) -def obfuscate(text): - text = unicode(text, encoding.encoding, 'replace') - return ''.join(['&#%d;' % ord(c) for c in text]) +def hgdate(text): + """:hgdate: Date. Returns the date as a pair of numbers: "1157407993 + 25200" (Unix timestamp, timezone offset). + """ + return "%d %d" % text -def domain(author): - '''get domain of author, or empty string if none.''' - f = author.find('@') - if f == -1: - return '' - author = author[f + 1:] - f = author.find('>') - if f >= 0: - author = author[:f] - return author +def isodate(text): + """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00 + +0200". + """ + return util.datestr(text, '%Y-%m-%d %H:%M %1%2') -def person(author): - '''get name of author, or else username.''' - if not '@' in author: - return author - f = author.find('<') - if f == -1: - return util.shortuser(author) - return author[:f].rstrip() +def isodatesec(text): + """:isodatesec: Date. Returns the date in ISO 8601 format, including + seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date + filter. + """ + return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2') def indent(text, prefix): '''indent each non-empty line of text after first with prefix.''' @@ -124,38 +178,6 @@ yield '\n' return "".join(indenter()) -def permissions(flags): - if "l" in flags: - return "lrwxrwxrwx" - if "x" in flags: - return "-rwxr-xr-x" - return "-rw-r--r--" - -def xmlescape(text): - text = (text - .replace('&', '&') - .replace('<', '<') - .replace('>', '>') - .replace('"', '"') - .replace("'", ''')) # ' invalid in HTML - return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text) - -def uescape(c): - if ord(c) < 0x80: - return c - else: - return '\\u%04x' % ord(c) - -_escapes = [ - ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'), - ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'), -] - -def jsonescape(s): - for k, v in _escapes: - s = s.replace(k, v) - return ''.join(uescape(c) for c in s) - def json(obj): if obj is None or obj is False or obj is True: return {None: 'null', False: 'false', True: 'true'}[obj] @@ -180,49 +202,164 @@ else: raise TypeError('cannot encode type %s' % obj.__class__.__name__) +def _uescape(c): + if ord(c) < 0x80: + return c + else: + return '\\u%04x' % ord(c) + +_escapes = [ + ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'), + ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'), +] + +def jsonescape(s): + for k, v in _escapes: + s = s.replace(k, v) + return ''.join(_uescape(c) for c in s) + +def localdate(text): + """:localdate: Date. Converts a date to local date.""" + return (text[0], util.makedate()[1]) + +def nonempty(str): + """:nonempty: Any text. Returns '(none)' if the string is empty.""" + return str or "(none)" + +def obfuscate(text): + """:obfuscate: Any text. Returns the input text rendered as a sequence of + XML entities. + """ + text = unicode(text, encoding.encoding, 'replace') + return ''.join(['&#%d;' % ord(c) for c in text]) + +def permissions(flags): + if "l" in flags: + return "lrwxrwxrwx" + if "x" in flags: + return "-rwxr-xr-x" + return "-rw-r--r--" + +def person(author): + """:person: Any text. Returns the text before an email address.""" + if not '@' in author: + return author + f = author.find('<') + if f != -1: + return author[:f].rstrip() + f = author.find('@') + return author[:f].replace('.', ' ') + +def rfc3339date(text): + """:rfc3339date: Date. Returns a date using the Internet date format + specified in RFC 3339: "2009-08-18T13:00:13+02:00". + """ + return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2") + +def rfc822date(text): + """:rfc822date: Date. Returns a date using the same format used in email + headers: "Tue, 18 Aug 2009 13:00:13 +0200". + """ + return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2") + +def short(text): + """:short: Changeset hash. Returns the short form of a changeset hash, + i.e. a 12 hexadecimal digit string. + """ + return text[:12] + +def shortdate(text): + """:shortdate: Date. Returns a date like "2006-09-18".""" + return util.shortdate(text) + +def stringescape(text): + return text.encode('string_escape') + +def stringify(thing): + """:stringify: Any type. Turns the value into text by converting values into + text and concatenating them. + """ + if hasattr(thing, '__iter__') and not isinstance(thing, str): + return "".join([stringify(t) for t in thing if t is not None]) + return str(thing) + +def strip(text): + """:strip: Any text. Strips all leading and trailing whitespace.""" + return text.strip() + def stripdir(text): - '''Treat the text as path and strip a directory level, if possible.''' + """:stripdir: Treat the text as path and strip a directory level, if + possible. For example, "foo" and "foo/bar" becomes "foo". + """ dir = os.path.dirname(text) if dir == "": return os.path.basename(text) else: return dir -def nonempty(str): - return str or "(none)" +def tabindent(text): + """:tabindent: Any text. Returns the text, with every line except the + first starting with a tab character. + """ + return indent(text, '\t') + +def urlescape(text): + """:urlescape: Any text. Escapes all "special" characters. For example, + "foo bar" becomes "foo%20bar". + """ + return urllib.quote(text) + +def userfilter(text): + """:user: Any text. Returns the user portion of an email address.""" + return util.shortuser(text) + +def xmlescape(text): + text = (text + .replace('&', '&') + .replace('<', '<') + .replace('>', '>') + .replace('"', '"') + .replace("'", ''')) # ' invalid in HTML + return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text) filters = { - "addbreaks": nl2br, - "basename": os.path.basename, - "stripdir": stripdir, + "addbreaks": addbreaks, "age": age, - "date": lambda x: util.datestr(x), + "basename": basename, + "date": datefilter, "domain": domain, - "email": util.email, - "escape": lambda x: cgi.escape(x, True), - "fill68": lambda x: fill(x, width=68), - "fill76": lambda x: fill(x, width=76), + "email": email, + "escape": escape, + "fill68": fill68, + "fill76": fill76, "firstline": firstline, - "tabindent": lambda x: indent(x, '\t'), - "hgdate": lambda x: "%d %d" % x, - "isodate": lambda x: util.datestr(x, '%Y-%m-%d %H:%M %1%2'), - "isodatesec": lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2'), + "hex": hexfilter, + "hgdate": hgdate, + "isodate": isodate, + "isodatesec": isodatesec, "json": json, "jsonescape": jsonescape, - "localdate": lambda x: (x[0], util.makedate()[1]), + "localdate": localdate, "nonempty": nonempty, "obfuscate": obfuscate, "permissions": permissions, "person": person, - "rfc822date": lambda x: util.datestr(x, "%a, %d %b %Y %H:%M:%S %1%2"), - "rfc3339date": lambda x: util.datestr(x, "%Y-%m-%dT%H:%M:%S%1:%2"), - "hex": node.hex, - "short": lambda x: x[:12], - "shortdate": util.shortdate, + "rfc3339date": rfc3339date, + "rfc822date": rfc822date, + "short": short, + "shortdate": shortdate, + "stringescape": stringescape, "stringify": stringify, - "strip": lambda x: x.strip(), - "urlescape": lambda x: urllib.quote(x), - "user": lambda x: util.shortuser(x), - "stringescape": lambda x: x.encode('string_escape'), + "strip": strip, + "stripdir": stripdir, + "tabindent": tabindent, + "urlescape": urlescape, + "user": userfilter, "xmlescape": xmlescape, } + +def makedoc(topic, doc): + return help.makeitemsdoc(topic, doc, '.. filtersmarker', filters) + +# tell hggettext to extract docstrings from these functions: +i18nfunctions = filters.values() diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templatekw.py --- a/mercurial/templatekw.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templatekw.py Fri May 06 14:29:53 2011 +0200 @@ -6,7 +6,7 @@ # GNU General Public License version 2 or any later version. from node import hex -import encoding, patch, util, error +import patch, util, error, help def showlist(name, values, plural=None, **args): '''expand set of values. @@ -73,8 +73,7 @@ def getfiles(repo, ctx, revcache): if 'files' not in revcache: - revcache['files'] = repo.status(ctx.parents()[0].node(), - ctx.node())[:3] + revcache['files'] = repo.status(ctx.p1().node(), ctx.node())[:3] return revcache['files'] def getlatesttags(repo, ctx, cache): @@ -143,32 +142,49 @@ def showauthor(repo, ctx, templ, **args): + """:author: String. The unmodified author of the changeset.""" return ctx.user() def showbranch(**args): + """:branch: String. The name of the branch on which the changeset was + committed. + """ return args['ctx'].branch() def showbranches(**args): + """:branches: List of strings. The name of the branch on which the + changeset was committed. Will be empty if the branch name was + default. + """ branch = args['ctx'].branch() if branch != 'default': return showlist('branch', [branch], plural='branches', **args) def showbookmarks(**args): + """:bookmarks: List of strings. Any bookmarks associated with the + changeset. + """ bookmarks = args['ctx'].bookmarks() return showlist('bookmark', bookmarks, **args) def showchildren(**args): + """:children: List of strings. The children of the changeset.""" ctx = args['ctx'] childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()] return showlist('children', childrevs, **args) def showdate(repo, ctx, templ, **args): + """:date: Date information. The date when the changeset was committed.""" return ctx.date() def showdescription(repo, ctx, templ, **args): + """:desc: String. The text of the changeset description.""" return ctx.description().strip() def showdiffstat(repo, ctx, templ, **args): + """:diffstat: String. Statistics of changes with the following format: + "modified files: +added/-removed lines" + """ files, adds, removes = 0, 0, 0 for i in patch.diffstatdata(util.iterlines(ctx.diff())): files += 1 @@ -184,10 +200,14 @@ yield templ('extra', **args) def showfileadds(**args): + """:file_adds: List of strings. Files added by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args) def showfilecopies(**args): + """:file_copies: List of strings. Files copied in this changeset with + their sources. + """ cache, ctx = args['cache'], args['ctx'] copies = args['revcache'].get('copies') if copies is None: @@ -207,25 +227,37 @@ # provided before calling the templater, usually with a --copies # command line switch. def showfilecopiesswitch(**args): + """:file_copies_switch: List of strings. Like "file_copies" but displayed + only if the --copied switch is set. + """ copies = args['revcache'].get('copies') or [] c = [{'name': x[0], 'source': x[1]} for x in copies] return showlist('file_copy', c, plural='file_copies', **args) def showfiledels(**args): + """:file_dels: List of strings. Files removed by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args) def showfilemods(**args): + """:file_mods: List of strings. Files modified by this changeset.""" repo, ctx, revcache = args['repo'], args['ctx'], args['revcache'] return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args) def showfiles(**args): + """:files: List of strings. All files modified, added, or removed by this + changeset. + """ return showlist('file', args['ctx'].files(), **args) def showlatesttag(repo, ctx, templ, cache, **args): + """:latesttag: String. Most recent global tag in the ancestors of this + changeset. + """ return getlatesttags(repo, ctx, cache)[2] def showlatesttagdistance(repo, ctx, templ, cache, **args): + """:latesttagdistance: Integer. Longest path to the latest tag.""" return getlatesttags(repo, ctx, cache)[1] def showmanifest(**args): @@ -236,12 +268,17 @@ return templ('manifest', **args) def shownode(repo, ctx, templ, **args): + """:node: String. The changeset identification hash, as a 40 hexadecimal + digit string. + """ return ctx.hex() def showrev(repo, ctx, templ, **args): + """:rev: Integer. The repository-local changeset revision number.""" return ctx.rev() def showtags(**args): + """:tags: List of strings. Any tags associated with the changeset.""" return showlist('tag', args['ctx'].tags(), **args) # keywords are callables like: @@ -276,3 +313,8 @@ 'tags': showtags, } +def makedoc(topic, doc): + return help.makeitemsdoc(topic, doc, '.. keywordsmarker', keywords) + +# tell hggettext to extract docstrings from these functions: +i18nfunctions = keywords.values() diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templater.py --- a/mercurial/templater.py Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templater.py Fri May 06 14:29:53 2011 +0200 @@ -69,7 +69,6 @@ else: raise error.ParseError(_("syntax error"), pos) pos += 1 - data[2] = pos yield ('end', None, pos) def compiletemplate(tmpl, context): @@ -91,8 +90,8 @@ parsed.append(("string", tmpl[pos:n])) pd = [tmpl, n + 1, stop] - parsed.append(p.parse(pd)) - pos = pd[2] + parseres, pos = p.parse(pd) + parsed.append(parseres) return [compileexp(e, context) for e in parsed] @@ -312,7 +311,7 @@ '''Get the template for the given template name. Use a local cache.''' if not t in self.cache: try: - self.cache[t] = open(self.map[t][1]).read() + self.cache[t] = util.readfile(self.map[t][1]) except KeyError, inst: raise util.Abort(_('"%s" not in template map') % inst.args[0]) except IOError, inst: diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templates/coal/header.tmpl --- a/mercurial/templates/coal/header.tmpl Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templates/coal/header.tmpl Fri May 06 14:29:53 2011 +0200 @@ -4,3 +4,4 @@ + diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templates/coal/map --- a/mercurial/templates/coal/map Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templates/coal/map Fri May 06 14:29:53 2011 +0200 @@ -94,14 +94,12 @@ filerename = '{file|escape}@' filelogrename = ' - - base: - - - {file|escape}@{node|short} - - - ' + + base + + {file|escape}@{node|short} + + ' fileannotateparent = ' parent: @@ -199,7 +197,7 @@ {name|escape} {description} {contact|obfuscate} - {lastchange|age} + {lastchange|date} {archives%indexarchiveentry} \n' indexarchiveentry = ' ↓{type|escape}' diff -r 7b627923739f -r 680c3c6fcb48 mercurial/templates/gitweb/bookmarks.tmpl --- a/mercurial/templates/gitweb/bookmarks.tmpl Thu May 05 16:01:09 2011 +0200 +++ b/mercurial/templates/gitweb/bookmarks.tmpl Fri May 06 14:29:53 2011 +0200 @@ -8,7 +8,7 @@