diff --git a/contrib/buildrpm b/contrib/buildrpm --- a/contrib/buildrpm +++ b/contrib/buildrpm @@ -1,19 +1,19 @@ #!/bin/sh # # Build a Mercurial RPM in place. -# Known to work on: -# - Fedora 9 -# - Fedora 10 # # Bryan O'Sullivan +# +# Tested on +# - Fedora 10 +# - Fedora 11 +# - Centos 5.3 (with Fedora EPEL repo for asciidoc) -if hg --version > /dev/null 2>&1; then : -else - echo 'hg command not available!' 1>&2 - exit 1 -fi +HG="`dirname $0`/../hg" +PYTHONPATH="`dirname $0`/../mercurial/pure" +export PYTHONPATH -root="`hg root 2>/dev/null`" +root="`$HG root 2>/dev/null`" specfile=contrib/mercurial.spec if [ -z "$root" ]; then @@ -26,7 +26,7 @@ rpmdir=/tmp/"`basename $root | sed 's/ / cd "$root" rm -rf $rpmdir mkdir -p $rpmdir/RPMS -hg clone "$root" $rpmdir/BUILD +$HG clone "$root" $rpmdir/BUILD if [ ! -f $specfile ]; then echo "Cannot find $specfile!" 1>&2 @@ -35,11 +35,11 @@ fi tmpspec=/tmp/`basename "$specfile"`.$$ # FIXME: Insecure /tmp handling # Use the most recent tag as the version. -version=`hg tags | perl -e 'while(){if(/^(\d\S+)/){print$1;exit}}'` +version=`$HG tags | python -c 'import sys; print [l for l in sys.stdin.readlines() if l[0].isdigit()][0].split()[0]'` # Compute the release number as the difference in revision numbers # between the tip and the most recent tag. -release=`hg tags | perl -e 'while(){($tag,$id)=/^(\S+)\s+(\d+)/;if($tag eq "tip"){$tip = $id}elsif($tag=~/^\d/){print $tip-$id+1;exit}}'` -tip=`hg -q tip` +release=`$HG tags | python -c 'import sys; l = sys.stdin.readlines(); print int(l[0].split()[1].split(":")[0]) - int([x for x in l if x[0].isdigit()][0].split()[1].split(":")[0])'` +tip=`$HG -q tip` # Beat up the spec file sed -e 's,^Source:.*,Source: /dev/null,' \ @@ -51,11 +51,11 @@ sed -e 's,^Source:.*,Source: /dev/null,' cat <> $tmpspec %changelog -* `date +'%a %b %d %Y'` `hg showconfig ui.username` $version-$release +* `date +'%a %b %d %Y'` `$HG showconfig ui.username` $version-$release - Automatically built via $0 EOF -hg log \ +$HG log \ --template '* {date|rfc822date} {author}\n- {desc|firstline}\n\n' \ .hgtags \ | sed -e 's/^\(\* [MTWFS][a-z][a-z]\), \([0-3][0-9]\) \([A-Z][a-z][a-z]\) /\1 \3 \2 /' \ diff --git a/contrib/mercurial.spec b/contrib/mercurial.spec --- a/contrib/mercurial.spec +++ b/contrib/mercurial.spec @@ -71,6 +71,8 @@ rm -rf $RPM_BUILD_ROOT %{_bindir}/hg-viz %{_bindir}/git-rev-tree %{_bindir}/mercurial-convert-repo -%{_libdir}/python%{pythonver}/site-packages/%{name}-*-py2.5.egg-info +%if "%{?pythonver}" != "2.4" +%{_libdir}/python%{pythonver}/site-packages/%{name}-*-py%{pythonver}.egg-info +%endif %{pythonlib} %{hgext} diff --git a/contrib/perf.py b/contrib/perf.py --- a/contrib/perf.py +++ b/contrib/perf.py @@ -1,4 +1,5 @@ # perf.py - performance test routines +'''helper extension to measure performance''' from mercurial import cmdutil, match, commands import time, os, sys diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -142,6 +142,11 @@ Example: foo.password = bar foo.schemes = http https + bar.prefix = secure.example.org + bar.key = path/to/file.key + bar.cert = path/to/file.cert + bar.schemes = https + Supported arguments: prefix;; @@ -152,10 +157,17 @@ Supported arguments: against the URI with its scheme stripped as well, and the schemes argument, q.v., is then subsequently consulted. username;; - Username to authenticate with. + Optional. Username to authenticate with. If not given, and the + remote site requires basic or digest authentication, the user + will be prompted for it. password;; - Optional. Password to authenticate with. If not given the user + Optional. Password to authenticate with. If not given, and the + remote site requires basic or digest authentication, the user will be prompted for it. + key;; + Optional. PEM encoded client certificate key file. + cert;; + Optional. PEM encoded client certificate chain file. schemes;; Optional. Space separated list of URI schemes to use this authentication entry with. Only used if the prefix doesn't include diff --git a/hgext/acl.py b/hgext/acl.py --- a/hgext/acl.py +++ b/hgext/acl.py @@ -5,49 +5,49 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. # -# this hook allows to allow or deny access to parts of a repo when -# taking incoming changesets. -# -# authorization is against local user name on system where hook is -# run, not committer of original changeset (since that is easy to -# spoof). -# -# acl hook is best to use if you use hgsh to set up restricted shells -# for authenticated users to only push to / pull from. not safe if -# user has interactive shell access, because they can disable hook. -# also not safe if remote users share one local account, because then -# no way to tell remote users apart. -# -# to use, configure acl extension in hgrc like this: -# -# [extensions] -# hgext.acl = -# -# [hooks] -# pretxnchangegroup.acl = python:hgext.acl.hook -# -# [acl] -# sources = serve # check if source of incoming changes in this list -# # ("serve" == ssh or http, "push", "pull", "bundle") -# -# allow and deny lists have subtree pattern (default syntax is glob) -# on left, user names on right. deny list checked before allow list. -# -# [acl.allow] -# # if acl.allow not present, all users allowed by default -# # empty acl.allow = no users allowed -# docs/** = doc_writer -# .hgtags = release_engineer -# -# [acl.deny] -# # if acl.deny not present, no users denied by default -# # empty acl.deny = all users allowed -# glob pattern = user4, user5 -# ** = user6 + +'''provide simple hooks for access control + +Authorization is against local user name on system where hook is run, not +committer of original changeset (since that is easy to spoof). + +The acl hook is best to use if you use hgsh to set up restricted shells for +authenticated users to only push to / pull from. It's not safe if user has +interactive shell access, because they can disable the hook. It's also not +safe if remote users share one local account, because then there's no way to +tell remote users apart. + +To use, configure the acl extension in hgrc like this: + + [extensions] + hgext.acl = + + [hooks] + pretxnchangegroup.acl = python:hgext.acl.hook + + [acl] + sources = serve # check if source of incoming changes in this list + # ("serve" == ssh or http, "push", "pull", "bundle") + +Allow and deny lists have a subtree pattern (default syntax is glob) on the +left and user names on right. The deny list is checked before the allow list. + + [acl.allow] + # if acl.allow not present, all users allowed by default + # empty acl.allow = no users allowed + docs/** = doc_writer + .hgtags = release_engineer + + [acl.deny] + # if acl.deny not present, no users denied by default + # empty acl.deny = all users allowed + glob pattern = user4, user5 + ** = user6 +''' from mercurial.i18n import _ from mercurial import util, match -import getpass +import getpass, urllib def buildmatch(ui, repo, user, key): '''return tuple of (match function, list enabled).''' @@ -72,7 +72,15 @@ def hook(ui, repo, hooktype, node=None, ui.debug(_('acl: changes have source "%s" - skipping\n') % source) return - user = getpass.getuser() + user = None + if source == 'serve' and 'url' in kwargs: + url = kwargs['url'].split(':') + if url[0] == 'remote' and url[1].startswith('http'): + user = urllib.unquote(url[2]) + + if user is None: + user = getpass.getuser() + cfg = ui.config('acl', 'config') if cfg: ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny']) diff --git a/hgext/bookmarks.py b/hgext/bookmarks.py --- a/hgext/bookmarks.py +++ b/hgext/bookmarks.py @@ -64,10 +64,14 @@ def write(repo, refs): util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks')) if current(repo) not in refs: setcurrent(repo, None) - file = repo.opener('bookmarks', 'w+') - for refspec, node in refs.iteritems(): - file.write("%s %s\n" % (hex(node), refspec)) - file.close() + wlock = repo.wlock() + try: + file = repo.opener('bookmarks', 'w', atomictemp=True) + for refspec, node in refs.iteritems(): + file.write("%s %s\n" % (hex(node), refspec)) + file.rename() + finally: + wlock.release() def current(repo): '''Get the current bookmark @@ -106,9 +110,13 @@ def setcurrent(repo, mark): return if mark not in refs: mark = '' - file = repo.opener('bookmarks.current', 'w+') - file.write(mark) - file.close() + wlock = repo.wlock() + try: + file = repo.opener('bookmarks.current', 'w', atomictemp=True) + file.write(mark) + file.rename() + finally: + wlock.release() repo._bookmarkcurrent = mark def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None): @@ -242,26 +250,30 @@ def reposetup(ui, repo): def commit(self, *k, **kw): """Add a revision to the repository and move the bookmark""" - node = super(bookmark_repo, self).commit(*k, **kw) - if node is None: - return None - parents = repo.changelog.parents(node) - if parents[1] == nullid: - parents = (parents[0],) - marks = parse(repo) - update = False - for mark, n in marks.items(): - if ui.configbool('bookmarks', 'track.current'): - if mark == current(repo) and n in parents: - marks[mark] = node - update = True - else: - if n in parents: - marks[mark] = node - update = True - if update: - write(repo, marks) - return node + wlock = self.wlock() # do both commit and bookmark with lock held + try: + node = super(bookmark_repo, self).commit(*k, **kw) + if node is None: + return None + parents = repo.changelog.parents(node) + if parents[1] == nullid: + parents = (parents[0],) + marks = parse(repo) + update = False + for mark, n in marks.items(): + if ui.configbool('bookmarks', 'track.current'): + if mark == current(repo) and n in parents: + marks[mark] = node + update = True + else: + if n in parents: + marks[mark] = node + update = True + if update: + write(repo, marks) + return node + finally: + wlock.release() def addchangegroup(self, source, srctype, url, emptyok=False): parents = repo.dirstate.parents() diff --git a/hgext/children.py b/hgext/children.py --- a/hgext/children.py +++ b/hgext/children.py @@ -8,6 +8,8 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. +'''provides children command to show children changesets''' + from mercurial import cmdutil from mercurial.commands import templateopts from mercurial.i18n import _ diff --git a/hgext/churn.py b/hgext/churn.py --- a/hgext/churn.py +++ b/hgext/churn.py @@ -120,7 +120,7 @@ def churn(ui, repo, *pats, **opts): It is possible to map alternate email addresses to a main address by providing a file using the following format: - + Such a file may be specified with the --aliases option, otherwise a diff --git a/hgext/color.py b/hgext/color.py --- a/hgext/color.py +++ b/hgext/color.py @@ -29,10 +29,6 @@ also available. Effects are rendered wit function (aka ANSI escape codes). This module also provides the render_text function, which can be used to add effects to any text. -To enable this extension, add this to your .hgrc file: -[extensions] -color = - Default effects may be overridden from the .hgrc file: [color] diff --git a/hgext/convert/convcmd.py b/hgext/convert/convcmd.py --- a/hgext/convert/convcmd.py +++ b/hgext/convert/convcmd.py @@ -80,7 +80,7 @@ class converter(object): self.authorfile = None # Record converted revisions persistently: maps source revision - # ID to target revision ID (both strings). (This is how + # ID to target revision ID (both strings). (This is how # incremental conversions work.) self.map = mapfile(ui, revmapfile) @@ -297,7 +297,7 @@ class converter(object): parents = [self.map.get(p, p) for p in parents] except KeyError: parents = [b[0] for b in pbranches] - newnode = self.dest.putcommit(files, copies, parents, commit, + newnode = self.dest.putcommit(files, copies, parents, commit, self.source, self.map) self.source.converted(rev, newnode) self.map[rev] = newnode diff --git a/hgext/convert/p4.py b/hgext/convert/p4.py --- a/hgext/convert/p4.py +++ b/hgext/convert/p4.py @@ -159,7 +159,7 @@ class p4_source(converter_source): if code == "error": raise IOError(d["generic"], data) - + elif code == "stat": p4type = self.re_type.match(d["type"]) if p4type: @@ -173,7 +173,7 @@ class p4_source(converter_source): keywords = self.re_keywords_old elif "k" in flags: keywords = self.re_keywords - + elif code == "text" or code == "binary": contents += data diff --git a/hgext/convert/subversion.py b/hgext/convert/subversion.py --- a/hgext/convert/subversion.py +++ b/hgext/convert/subversion.py @@ -472,7 +472,7 @@ class svn_source(converter_source): # Here/tags/tag.1 discarded as well as its children. # It happens with tools like cvs2svn. Such tags cannot # be represented in mercurial. - addeds = dict((p, e.copyfrom_path) for p,e + addeds = dict((p, e.copyfrom_path) for p, e in origpaths.iteritems() if e.action == 'A') badroots = set() for destroot in addeds: @@ -484,7 +484,7 @@ class svn_source(converter_source): break for badroot in badroots: - pendings = [p for p in pendings if p[2] != badroot + pendings = [p for p in pendings if p[2] != badroot and not p[2].startswith(badroot + '/')] # Tell tag renamings from tag creations @@ -497,7 +497,7 @@ class svn_source(converter_source): if tagname in tags: # Keep the latest tag value continue - # From revision may be fake, get one with changes + # From revision may be fake, get one with changes try: tagid = self.latest(source, sourcerev) if tagid and tagname not in tags: diff --git a/hgext/extdiff.py b/hgext/extdiff.py --- a/hgext/extdiff.py +++ b/hgext/extdiff.py @@ -5,18 +5,14 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. -''' +'''allow external programs to compare revisions + The `extdiff' Mercurial extension allows you to use external programs to compare revisions, or revision with working directory. The external diff programs are called with a configurable set of options and two non-option arguments: paths to directories containing snapshots of files to compare. -To enable this extension: - - [extensions] - hgext.extdiff = - The `extdiff' extension also allows to configure new diff commands, so you do not need to type "hg extdiff -p kdiff3" always. diff --git a/hgext/gpg.py b/hgext/gpg.py --- a/hgext/gpg.py +++ b/hgext/gpg.py @@ -1,10 +1,10 @@ -# GnuPG signing extension for Mercurial -# # Copyright 2005, 2006 Benoit Boissinot # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. +'''GnuPG signing extension for Mercurial''' + import os, tempfile, binascii from mercurial import util, commands, match from mercurial import node as hgnode diff --git a/hgext/graphlog.py b/hgext/graphlog.py --- a/hgext/graphlog.py +++ b/hgext/graphlog.py @@ -12,59 +12,32 @@ commands. When this options is given, an revision graph is also shown. ''' -import os +import os, sys from mercurial.cmdutil import revrange, show_changeset from mercurial.commands import templateopts from mercurial.i18n import _ from mercurial.node import nullrev from mercurial import bundlerepo, changegroup, cmdutil, commands, extensions -from mercurial import hg, url, util - -def revisions(repo, start, stop): - """cset DAG generator yielding (rev, node, [parents]) tuples +from mercurial import hg, url, util, graphmod - This generator function walks through the revision history from revision - start to revision stop (which must be less than or equal to start). - """ - assert start >= stop - cur = start - while cur >= stop: - ctx = repo[cur] - parents = [p.rev() for p in ctx.parents() if p.rev() != nullrev] - parents.sort() - yield (ctx, parents) - cur -= 1 - -def filerevs(repo, path, start, stop): - """file cset DAG generator yielding (rev, node, [parents]) tuples +ASCIIDATA = 'ASC' - This generator function walks through the revision history of a single - file from revision start to revision stop (which must be less than or - equal to start). - """ - assert start >= stop - filerev = len(repo.file(path)) - 1 - while filerev >= 0: - fctx = repo.filectx(path, fileid=filerev) - parents = [f.linkrev() for f in fctx.parents() if f.path() == path] - parents.sort() - if fctx.rev() <= start: - yield (fctx, parents) - if fctx.rev() <= stop: - break - filerev -= 1 +def asciiformat(ui, repo, revdag, opts): + """formats a changelog DAG walk for ASCII output""" + showparents = [ctx.node() for ctx in repo[None].parents()] + displayer = show_changeset(ui, repo, opts, buffered=True) + for (id, type, ctx, parentids) in revdag: + if type != graphmod.CHANGESET: + continue + displayer.show(ctx) + lines = displayer.hunk.pop(ctx.rev()).split('\n')[:-1] + char = ctx.node() in showparents and '@' or 'o' + yield (id, ASCIIDATA, (char, lines), parentids) -def grapher(nodes): - """grapher for asciigraph on a list of nodes and their parents - - nodes must generate tuples (node, parents, char, lines) where - - parents must generate the parents of node, in sorted order, - and max length 2, - - char is the char to print as the node symbol, and - - lines are the lines to display next to the node. - """ +def asciiedges(nodes): + """adds edge info to changelog DAG walk suitable for ascii()""" seen = [] - for node, parents, char, lines in nodes: + for node, type, data, parents in nodes: if node not in seen: seen.append(node) nodeidx = seen.index(node) @@ -88,7 +61,7 @@ def grapher(nodes): edges.append((nodeidx, nodeidx + 1)) nmorecols = len(nextseen) - ncols seen = nextseen - yield (char, lines, nodeidx, edges, ncols, nmorecols) + yield (nodeidx, type, data, edges, ncols, nmorecols) def fix_long_right_edges(edges): for (i, (start, end)) in enumerate(edges): @@ -142,14 +115,16 @@ def get_padding_line(ni, n_columns, edge line.extend(["|", " "] * (n_columns - ni - 1)) return line -def ascii(ui, grapher): - """prints an ASCII graph of the DAG returned by the grapher +def ascii(ui, dag): + """prints an ASCII graph of the DAG + + dag is a generator that emits tuples with the following elements: - grapher is a generator that emits tuples with the following elements: - - - Character to use as node's symbol. - - List of lines to display as the node's text. - Column of the current node in the set of ongoing edges. + - Type indicator of node data == ASCIIDATA. + - Payload: (char, lines): + - Character to use as node's symbol. + - List of lines to display as the node's text. - Edges; a list of (col, next_col) indicating the edges between the current node and its parents. - Number of columns (ongoing edges) in the current revision. @@ -160,7 +135,7 @@ def ascii(ui, grapher): """ prev_n_columns_diff = 0 prev_node_index = 0 - for (node_ch, node_lines, node_index, edges, n_columns, n_columns_diff) in grapher: + for (node_index, type, (node_ch, node_lines), edges, n_columns, n_columns_diff) in dag: assert -2 < n_columns_diff < 2 if n_columns_diff == -1: @@ -278,34 +253,19 @@ def graphlog(ui, repo, path=None, **opts if path: path = util.canonpath(repo.root, os.getcwd(), path) if path: # could be reset in canonpath - revdag = filerevs(repo, path, start, stop) + revdag = graphmod.filerevs(repo, path, start, stop) else: - revdag = revisions(repo, start, stop) + revdag = graphmod.revisions(repo, start, stop) - graphdag = graphabledag(ui, repo, revdag, opts) - ascii(ui, grapher(graphdag)) + fmtdag = asciiformat(ui, repo, revdag, opts) + ascii(ui, asciiedges(fmtdag)) def graphrevs(repo, nodes, opts): - include = set(nodes) limit = cmdutil.loglimit(opts) - count = 0 - for node in reversed(nodes): - if count >= limit: - break - ctx = repo[node] - parents = [p.rev() for p in ctx.parents() if p.node() in include] - parents.sort() - yield (ctx, parents) - count += 1 - -def graphabledag(ui, repo, revdag, opts): - showparents = [ctx.node() for ctx in repo[None].parents()] - displayer = show_changeset(ui, repo, opts, buffered=True) - for (ctx, parents) in revdag: - displayer.show(ctx) - lines = displayer.hunk.pop(ctx.rev()).split('\n')[:-1] - char = ctx.node() in showparents and '@' or 'o' - yield (ctx.rev(), parents, char, lines) + nodes.reverse() + if limit < sys.maxint: + nodes = nodes[:limit] + return graphmod.nodes(repo, nodes) def goutgoing(ui, repo, dest=None, **opts): """show the outgoing changesets alongside an ASCII revision graph @@ -332,8 +292,8 @@ def goutgoing(ui, repo, dest=None, **opt o = repo.changelog.nodesbetween(o, revs)[0] revdag = graphrevs(repo, o, opts) - graphdag = graphabledag(ui, repo, revdag, opts) - ascii(ui, grapher(graphdag)) + fmtdag = asciiformat(ui, repo, revdag, opts) + ascii(ui, asciiedges(fmtdag)) def gincoming(ui, repo, source="default", **opts): """show the incoming changesets alongside an ASCII revision graph @@ -381,8 +341,8 @@ def gincoming(ui, repo, source="default" chlist = other.changelog.nodesbetween(incoming, revs)[0] revdag = graphrevs(other, chlist, opts) - graphdag = graphabledag(ui, repo, revdag, opts) - ascii(ui, grapher(graphdag)) + fmtdag = asciiformat(ui, repo, revdag, opts) + ascii(ui, asciiedges(fmtdag)) finally: if hasattr(other, 'close'): diff --git a/hgext/hgk.py b/hgext/hgk.py --- a/hgext/hgk.py +++ b/hgext/hgk.py @@ -14,20 +14,8 @@ distributed with Mercurial.) hgk consists of two parts: a Tcl script that does the displaying and querying of information, and an extension to Mercurial named hgk.py, which provides hooks for hgk to get information. hgk can be found in -the contrib directory, and hgk.py can be found in the hgext directory. - -To load the hgext.py extension, add it to your .hgrc file (you have to -use your global $HOME/.hgrc file, not one in a repository). You can -specify an absolute path: - - [extensions] - hgk=/usr/local/lib/hgk.py - -Mercurial can also scan the default python library path for a file -named 'hgk.py' if you set hgk empty: - - [extensions] - hgk= +the contrib directory, and the extension is shipped in the hgext +repository, and needs to be enabled. The hg view command will launch the hgk Tcl script. For this command to work, hgk must be in your search path. Alternately, you can specify diff --git a/hgext/highlight/__init__.py b/hgext/highlight/__init__.py --- a/hgext/highlight/__init__.py +++ b/hgext/highlight/__init__.py @@ -13,11 +13,6 @@ It depends on the Pygments syntax highlighting library: http://pygments.org/ -To enable the extension add this to hgrc: - -[extensions] -hgext.highlight = - There is a single configuration option: [web] @@ -30,10 +25,10 @@ The default is 'colorful'. import highlight from mercurial.hgweb import webcommands, webutil, common -from mercurial import extensions +from mercurial import extensions, encoding def filerevision_highlight(orig, web, tmpl, fctx): - mt = ''.join(tmpl('mimetype', encoding=web.encoding)) + mt = ''.join(tmpl('mimetype', encoding=encoding.encoding)) # only pygmentize for mimetype containing 'html' so we both match # 'text/html' and possibly 'application/xhtml+xml' in the future # so that we don't have to touch the extension when the mimetype @@ -47,7 +42,7 @@ def filerevision_highlight(orig, web, tm return orig(web, tmpl, fctx) def annotate_highlight(orig, web, req, tmpl): - mt = ''.join(tmpl('mimetype', encoding=web.encoding)) + mt = ''.join(tmpl('mimetype', encoding=encoding.encoding)) if 'html' in mt: fctx = webutil.filectx(web.repo, req) style = web.config('web', 'pygments_style', 'colorful') diff --git a/hgext/interhg.py b/hgext/interhg.py --- a/hgext/interhg.py +++ b/hgext/interhg.py @@ -14,12 +14,8 @@ This extension allows the use of a speci which will be automatically expanded into links or any other arbitrary expression, much like InterWiki does. -To enable this extension, add the following lines to your hgrc: - - [extensions] - interhg = - -A few example patterns (link to bug tracking, etc.): +A few example patterns (link to bug tracking, etc.) that may +be used in your hgrc: [interhg] issues = s!issue(\\d+)!issue\\1! diff --git a/hgext/keyword.py b/hgext/keyword.py --- a/hgext/keyword.py +++ b/hgext/keyword.py @@ -21,12 +21,6 @@ # # Binary files are not touched. # -# Setup in hgrc: -# -# [extensions] -# # enable extension -# hgext.keyword = -# # Files to act upon/ignore are specified in the [keyword] section. # Customized keyword template mappings in the [keywordmaps] section. # diff --git a/hgext/mq.py b/hgext/mq.py --- a/hgext/mq.py +++ b/hgext/mq.py @@ -543,6 +543,8 @@ class queue(object): def _apply(self, repo, series, list=False, update_status=True, strict=False, patchdir=None, merge=None, all_files={}): + '''returns (error, hash) + error = 1 for unable to read, 2 for patch failed, 3 for patch fuzz''' # TODO unify with commands.py if not patchdir: patchdir = self.path @@ -559,7 +561,7 @@ class queue(object): try: ph = patchheader(self.join(patchname)) except: - self.ui.warn(_("Unable to read %s\n") % patchname) + self.ui.warn(_("unable to read %s\n") % patchname) err = 1 break @@ -607,46 +609,60 @@ class queue(object): if patcherr: self.ui.warn(_("patch failed, rejects left in working dir\n")) - err = 1 + err = 2 break if fuzz and strict: self.ui.warn(_("fuzz found when applying patch, stopping\n")) - err = 1 + err = 3 break return (err, n) - def _clean_series(self, patches): + def _cleanup(self, patches, numrevs, keep=False): + if not keep: + r = self.qrepo() + if r: + r.remove(patches, True) + else: + for p in patches: + os.unlink(self.join(p)) + + if 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] self.parse_series() self.series_dirty = 1 - def finish(self, repo, revs): + def _revpatches(self, repo, revs): firstrev = repo[self.applied[0].rev].rev() - appliedbase = 0 patches = [] - for rev in sorted(revs): + for i, rev in enumerate(revs): + if rev < firstrev: raise util.Abort(_('revision %d is not managed') % rev) - base = bin(self.applied[appliedbase].rev) - node = repo.changelog.node(rev) - if node != base: - raise util.Abort(_('cannot delete revision %d above ' - 'applied patches') % rev) - patches.append(self.applied[appliedbase].name) - appliedbase += 1 + + ctx = repo[rev] + base = bin(self.applied[i].rev) + if ctx.node() != base: + msg = _('cannot delete revision %d above applied patches') + raise util.Abort(msg % rev) - r = self.qrepo() - if r: - r.remove(patches, True) - else: - for p in patches: - os.unlink(self.join(p)) + patch = self.applied[i].name + for fmt in ('[mq]: %s', 'imported patch %s'): + if ctx.description() == fmt % patch: + msg = _('patch %s finalized without changeset message\n') + repo.ui.status(msg % patch) + break - del self.applied[:appliedbase] - self.applied_dirty = 1 - self._clean_series(patches) + patches.append(patch) + return patches + + def finish(self, repo, revs): + patches = self._revpatches(repo, sorted(revs)) + self._cleanup(patches, len(patches)) def delete(self, repo, patches, opts): if not patches and not opts.get('rev'): @@ -663,37 +679,18 @@ class queue(object): raise util.Abort(_("patch %s not in series file") % patch) realpatches.append(patch) - appliedbase = 0 + numrevs = 0 if opts.get('rev'): if not self.applied: raise util.Abort(_('no patches applied')) revs = cmdutil.revrange(repo, opts['rev']) if len(revs) > 1 and revs[0] > revs[1]: revs.reverse() - for rev in revs: - if appliedbase >= len(self.applied): - raise util.Abort(_("revision %d is not managed") % rev) - - base = bin(self.applied[appliedbase].rev) - node = repo.changelog.node(rev) - if node != base: - raise util.Abort(_("cannot delete revision %d above " - "applied patches") % rev) - realpatches.append(self.applied[appliedbase].name) - appliedbase += 1 + revpatches = self._revpatches(repo, revs) + realpatches += revpatches + numrevs = len(revpatches) - if not opts.get('keep'): - r = self.qrepo() - if r: - r.remove(realpatches, True) - else: - for p in realpatches: - os.unlink(self.join(p)) - - if appliedbase: - del self.applied[:appliedbase] - self.applied_dirty = 1 - self._clean_series(realpatches) + self._cleanup(realpatches, numrevs, opts.get('keep')) def check_toppatch(self, repo): if len(self.applied) > 0: @@ -958,6 +955,7 @@ class queue(object): end = start + 1 else: end = self.series.index(patch, start) + 1 + s = self.series[start:end] all_files = {} try: @@ -977,13 +975,15 @@ class queue(object): util.unlink(repo.wjoin(f)) self.ui.warn(_('done\n')) raise + top = self.applied[-1].name - if ret[0]: - self.ui.write(_("errors during apply, please fix and " - "refresh %s\n") % top) + if ret[0] and ret[0] > 1: + msg = _("errors during apply, please fix and refresh %s\n") + self.ui.write(msg % top) else: self.ui.write(_("now at: %s\n") % top) return ret[0] + finally: wlock.release() diff --git a/hgext/parentrevspec.py b/hgext/parentrevspec.py --- a/hgext/parentrevspec.py +++ b/hgext/parentrevspec.py @@ -5,8 +5,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. -'''\ -use suffixes to refer to ancestor revisions +'''use suffixes to refer to ancestor revisions This extension allows you to use git-style suffixes to refer to the ancestors of a specific revision. diff --git a/hgext/patchbomb.py b/hgext/patchbomb.py --- a/hgext/patchbomb.py +++ b/hgext/patchbomb.py @@ -28,11 +28,6 @@ With the -d/--diffstat option, you will with a diffstat summary and the changeset summary, so you can be sure you are sending the right changes. -To enable this extension: - - [extensions] - hgext.patchbomb = - To configure other defaults, add a section like this to your hgrc file: diff --git a/hgext/purge.py b/hgext/purge.py --- a/hgext/purge.py +++ b/hgext/purge.py @@ -6,10 +6,6 @@ # This program was inspired by the "cvspurge" script contained in CVS utilities # (http://www.red-bean.com/cvsutils/). # -# To enable the "purge" extension put these lines in your ~/.hgrc: -# [extensions] -# hgext.purge = -# # For help on the usage of "hg purge" use: # hg help purge # @@ -27,6 +23,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +'''enable removing untracked files only''' + from mercurial import util, commands, cmdutil from mercurial.i18n import _ import os, stat diff --git a/hgext/share.py b/hgext/share.py --- a/hgext/share.py +++ b/hgext/share.py @@ -1,10 +1,10 @@ -# Mercurial extension to provide the 'hg share' command -# # Copyright 2006, 2007 Matt Mackall # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. +'''provides the hg share command''' + import os from mercurial.i18n import _ from mercurial import hg, commands diff --git a/hgext/win32mbcs.py b/hgext/win32mbcs.py --- a/hgext/win32mbcs.py +++ b/hgext/win32mbcs.py @@ -33,11 +33,6 @@ Note that there are some limitations on * You should set same encoding for the repository by locale or HGENCODING. -To use this extension, enable the extension in .hg/hgrc or ~/.hgrc: - - [extensions] - hgext.win32mbcs = - Path encoding conversion are done between Unicode and encoding.encoding which is decided by Mercurial from current locale setting or HGENCODING. diff --git a/hgext/win32text.py b/hgext/win32text.py --- a/hgext/win32text.py +++ b/hgext/win32text.py @@ -4,31 +4,34 @@ # # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. -# -# To perform automatic newline conversion, use: -# -# [extensions] -# hgext.win32text = -# [encode] -# ** = cleverencode: -# # or ** = macencode: -# [decode] -# ** = cleverdecode: -# # or ** = macdecode: -# -# If not doing conversion, to make sure you do not commit CRLF/CR by -# accident: -# -# [hooks] -# pretxncommit.crlf = python:hgext.win32text.forbidcrlf -# # or pretxncommit.cr = python:hgext.win32text.forbidcr -# -# To do the same check on a server to prevent CRLF/CR from being -# pushed or pulled: -# -# [hooks] -# pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf -# # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr + +'''LF <-> CRLF/CR translation utilities + +To perform automatic newline conversion, use: + +[extensions] +hgext.win32text = +[encode] +** = cleverencode: +# or ** = macencode: + +[decode] +** = cleverdecode: +# or ** = macdecode: + +If not doing conversion, to make sure you do not commit CRLF/CR by accident: + +[hooks] +pretxncommit.crlf = python:hgext.win32text.forbidcrlf +# or pretxncommit.cr = python:hgext.win32text.forbidcr + +To do the same check on a server to prevent CRLF/CR from being +pushed or pulled: + +[hooks] +pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf +# or pretxnchangegroup.cr = python:hgext.win32text.forbidcr +''' from mercurial.i18n import _ from mercurial.node import short diff --git a/hgext/zeroconf/__init__.py b/hgext/zeroconf/__init__.py --- a/hgext/zeroconf/__init__.py +++ b/hgext/zeroconf/__init__.py @@ -11,12 +11,6 @@ Zeroconf enabled repositories will be an the need to configure a server or a service. They can be discovered without knowing their actual IP address. -To use the zeroconf extension add the following entry to your hgrc -file: - -[extensions] -hgext.zeroconf = - To allow other people to discover your repository using run "hg serve" in your repository. diff --git a/mercurial/bdiff.c b/mercurial/bdiff.c --- a/mercurial/bdiff.c +++ b/mercurial/bdiff.c @@ -18,6 +18,10 @@ # define inline #endif +#ifdef __linux +# define inline __inline +#endif + #ifdef _WIN32 #ifdef _MSC_VER #define inline __inline diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -1230,16 +1230,14 @@ def grep(ui, repo, pattern, *pats, **opt for i in xrange(blo, bhi): yield ('+', b[i]) - prev = {} - def display(fn, rev, states, prevstates): + def display(fn, r, pstates, states): datefunc = ui.quiet and util.shortdate or util.datestr found = False filerevmatches = {} - r = prev.get(fn, -1) if opts.get('all'): - iter = difflinestates(states, prevstates) + iter = difflinestates(pstates, states) else: - iter = [('', l) for l in prevstates] + iter = [('', l) for l in states] for change, l in iter: cols = [fn, str(r)] if opts.get('line_number'): @@ -1261,8 +1259,8 @@ def grep(ui, repo, pattern, *pats, **opt found = True return found - fstate = {} skip = {} + revfiles = {} get = util.cachefunc(lambda r: repo[r].changeset()) changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts) found = False @@ -1270,46 +1268,58 @@ def grep(ui, repo, pattern, *pats, **opt for st, rev, fns in changeiter: if st == 'window': matches.clear() + revfiles.clear() elif st == 'add': ctx = repo[rev] - matches[rev] = {} + pctx = ctx.parents()[0] + parent = pctx.rev() + matches.setdefault(rev, {}) + matches.setdefault(parent, {}) + files = revfiles.setdefault(rev, []) for fn in fns: - if fn in skip: - continue + flog = getfile(fn) try: - grepbody(fn, rev, getfile(fn).read(ctx.filenode(fn))) - fstate.setdefault(fn, []) - if follow: - copied = getfile(fn).renamed(ctx.filenode(fn)) - if copied: - copies.setdefault(rev, {})[fn] = copied[0] + fnode = ctx.filenode(fn) except error.LookupError: - pass + continue + + copied = flog.renamed(fnode) + copy = follow and copied and copied[0] + if copy: + copies.setdefault(rev, {})[fn] = copy + if fn in skip: + if copy: + skip[copy] = True + continue + files.append(fn) + + if not matches[rev].has_key(fn): + grepbody(fn, rev, flog.read(fnode)) + + pfn = copy or fn + if not matches[parent].has_key(pfn): + try: + fnode = pctx.filenode(pfn) + grepbody(pfn, parent, flog.read(fnode)) + except error.LookupError: + pass elif st == 'iter': - for fn, m in sorted(matches[rev].items()): + parent = repo[rev].parents()[0].rev() + for fn in sorted(revfiles.get(rev, [])): + states = matches[rev][fn] copy = copies.get(rev, {}).get(fn) if fn in skip: if copy: skip[copy] = True continue - if fn in prev or fstate[fn]: - r = display(fn, rev, m, fstate[fn]) + pstates = matches.get(parent, {}).get(copy or fn, []) + if pstates or states: + r = display(fn, rev, pstates, states) found = found or r if r and not opts.get('all'): skip[fn] = True if copy: skip[copy] = True - fstate[fn] = m - if copy: - fstate[copy] = m - prev[fn] = rev - - for fn, state in sorted(fstate.items()): - if fn in skip: - continue - if fn not in copies.get(prev[fn], {}): - found = display(fn, rev, {}, state) or found - return (not found and 1) or 0 def heads(ui, repo, *branchrevs, **opts): """show current repository heads or show branch heads @@ -1473,18 +1483,9 @@ def help_(ui, name=None, with_version=Fa else: ui.write(' %-*s %s\n' % (m, f, h[f])) - exts = list(extensions.extensions()) - if exts and name != 'shortlist': - ui.write(_('\nenabled extensions:\n\n')) - maxlength = 0 - exthelps = [] - for ename, ext in exts: - doc = (gettext(ext.__doc__) or _('(no help text available)')) - ename = ename.split('.')[-1] - maxlength = max(len(ename), maxlength) - exthelps.append((ename, doc.splitlines(0)[0].strip())) - for ename, text in exthelps: - ui.write(_(' %s %s\n') % (ename.ljust(maxlength), text)) + if name != 'shortlist': + exts, maxlength = extensions.enabled() + ui.write(help.listexts(_('enabled extensions:'), exts, maxlength)) if not ui.quiet: addglobalopts(True) @@ -2108,7 +2109,7 @@ def merge(ui, repo, node=None, **opts): 'use "hg update" or merge with an explicit rev')) node = parent == bheads[0] and bheads[-1] or bheads[0] - if opts.get('show'): + if opts.get('preview'): p1 = repo['.'] p2 = repo[node] common = p1.ancestor(p2) @@ -2670,7 +2671,8 @@ def rollback(ui, repo): This command should be used with care. There is only one level of rollback, and there is no way to undo a rollback. It will also restore the dirstate at the time of the last transaction, losing - any dirstate changes since that time. + any dirstate changes since that time. This command does not alter + the working directory. Transactions are used to encapsulate the effects of all commands that create new changesets or propagate existing changesets into a @@ -2718,9 +2720,9 @@ def serve(ui, repo, **opts): baseui = repo and repo.baseui or ui optlist = ("name templates style address port prefix ipv6" - " accesslog errorlog webdir_conf certificate") + " accesslog errorlog webdir_conf certificate encoding") for o in optlist.split(): - if opts[o]: + if opts.get(o, None): baseui.setconfig("web", o, str(opts[o])) if (repo is not None) and (repo.ui != baseui): repo.ui.setconfig("web", o, str(opts[o])) @@ -2965,7 +2967,7 @@ def unbundle(ui, repo, fname1, *fnames, return postincoming(ui, repo, modheads, opts.get('update'), None) -def update(ui, repo, node=None, rev=None, clean=False, date=None): +def update(ui, repo, node=None, rev=None, clean=False, date=None, check=False): """update working directory Update the repository's working directory to the specified @@ -2981,7 +2983,8 @@ def update(ui, repo, node=None, rev=None When there are uncommitted changes, use option -C/--clean to discard them, forcibly replacing the state of the working - directory with the requested revision. + directory with the requested revision. Alternately, use -c/--check + to abort. When there are uncommitted changes and option -C/--clean is not used, and the parent revision and requested revision are on the @@ -3001,6 +3004,12 @@ def update(ui, repo, node=None, rev=None if not rev: rev = node + if not clean and check: + # we could use dirty() but we can ignore merge and branch trivia + c = repo[None] + if c.modified() or c.added() or c.removed(): + raise util.Abort(_("uncommitted local changes")) + if date: if rev: raise util.Abort(_("you can't specify a revision and a date")) @@ -3358,7 +3367,7 @@ table = { (merge, [('f', 'force', None, _('force a merge with outstanding changes')), ('r', 'rev', '', _('revision to merge')), - ('S', 'show', None, + ('P', 'preview', None, _('review revisions to merge (no merge is performed)'))], _('[-f] [[-r] REV]')), "outgoing|out": @@ -3492,6 +3501,7 @@ table = { "^update|up|checkout|co": (update, [('C', 'clean', None, _('overwrite locally modified files (no backup)')), + ('c', 'check', None, _('check for uncommitted changes')), ('d', 'date', '', _('tipmost revision matching date')), ('r', 'rev', '', _('revision'))], _('[-C] [-d DATE] [[-r] REV]')), diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py --- a/mercurial/dispatch.py +++ b/mercurial/dispatch.py @@ -224,7 +224,6 @@ def addaliases(ui, cmdtable): # but only if they have been defined prior to the current definition. for alias, definition in ui.configitems('alias'): aliasdef = cmdalias(alias, definition, cmdtable) - cmdtable[alias] = (aliasdef, aliasdef.opts, aliasdef.help) if aliasdef.norepo: commands.norepo += ' %s' % alias diff --git a/mercurial/extensions.py b/mercurial/extensions.py --- a/mercurial/extensions.py +++ b/mercurial/extensions.py @@ -5,9 +5,9 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. -import imp, os -import util, cmdutil -from i18n import _ +import imp, os, sys +import util, cmdutil, help +from i18n import _, gettext _extensions = {} _order = [] @@ -117,3 +117,61 @@ def wrapfunction(container, funcname, wr origfn = getattr(container, funcname) setattr(container, funcname, wrap) return origfn + +def disabled(): + '''find disabled extensions from hgext + returns a dict of {name: desc}, and the max name length''' + + import hgext + extpath = os.path.dirname(os.path.abspath(hgext.__file__)) + + exts = {} + maxlength = 0 + for e in os.listdir(extpath): + + if e.endswith('.py'): + name = e.rsplit('.', 1)[0] + path = os.path.join(extpath, e) + else: + name = e + path = os.path.join(extpath, e, '__init__.py') + if not os.path.exists(path): + continue + + if name in exts or name in _order or name == '__init__': + continue + + try: + file = open(path) + except IOError: + continue + else: + doc = help.moduledoc(file) + file.close() + + if doc: # extracting localized synopsis + exts[name] = gettext(doc).splitlines()[0] + else: + exts[name] = _('(no help text available)') + + if len(name) > maxlength: + maxlength = len(name) + + return exts, maxlength + +def enabled(): + '''return a dict of {name: desc} of extensions, and the max name length''' + + if not enabled: + return {}, 0 + + exts = {} + maxlength = 0 + exthelps = [] + for ename, ext in extensions(): + doc = (gettext(ext.__doc__) or _('(no help text available)')) + ename = ename.split('.')[-1] + maxlength = max(len(ename), maxlength) + exts[ename] = doc.splitlines(0)[0].strip() + + return exts, maxlength diff --git a/mercurial/filemerge.py b/mercurial/filemerge.py --- a/mercurial/filemerge.py +++ b/mercurial/filemerge.py @@ -195,8 +195,8 @@ def filemerge(repo, mynode, orig, fcd, f elif tool == 'internal:dump': a = repo.wjoin(fd) util.copyfile(a, a + ".local") - repo.wwrite(a + ".other", fco.data(), fco.flags()) - repo.wwrite(a + ".base", fca.data(), fca.flags()) + repo.wwrite(fd + ".other", fco.data(), fco.flags()) + repo.wwrite(fd + ".base", fca.data(), fca.flags()) return 1 # unresolved else: args = _toolstr(ui, tool, "args", '$local $base $other') diff --git a/mercurial/graphmod.py b/mercurial/graphmod.py --- a/mercurial/graphmod.py +++ b/mercurial/graphmod.py @@ -6,70 +6,114 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2, incorporated herein by reference. -from node import nullrev +"""supports walking the history as DAGs suitable for graphical output -def graph(repo, start_rev, stop_rev): - """incremental revision grapher +The most basic format we use is that of:: + + (id, type, data, [parentids]) - This generator function walks through the revision history from - revision start_rev to revision stop_rev (which must be less than - or equal to start_rev) and for each revision emits tuples with the - following elements: +The node and parent ids are arbitrary integers which identify a node in the +context of the graph returned. Type is a constant specifying the node type. +Data depends on type. +""" + +from mercurial.node import nullrev + +CHANGESET = 'C' - - Current node - - Column and color for the current node - - Edges; a list of (col, next_col, color) indicating the edges between - the current node and its parents. - - First line of the changeset description - - The changeset author - - The changeset date/time +def revisions(repo, start, stop): + """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. """ + cur = start + while cur >= stop: + ctx = repo[cur] + parents = [p.rev() for p in ctx.parents() if p.rev() != nullrev] + yield (cur, CHANGESET, ctx, sorted(parents)) + cur -= 1 - if start_rev == nullrev and not stop_rev: - return +def filerevs(repo, path, start, stop): + """file cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples - assert start_rev >= stop_rev - assert stop_rev >= 0 - curr_rev = start_rev - revs = [] - cl = repo.changelog - colors = {} - new_color = 1 + 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 + while filerev >= 0: + fctx = repo.filectx(path, fileid=filerev) + parents = [f.linkrev() for f in fctx.parents() if f.path() == path] + rev = fctx.rev() + if rev <= start: + yield (rev, CHANGESET, fctx, sorted(parents)) + if rev <= stop: + break + filerev -= 1 + +def nodes(repo, nodes): + """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples + + This generator function walks the given nodes. It only returns parents + that are in nodes, too. + """ + include = set(nodes) + for node in nodes: + ctx = repo[node] + parents = [p.rev() for p in ctx.parents() if p.node() in include] + yield (ctx.rev(), CHANGESET, ctx, sorted(parents)) + +def colored(dag): + """annotates a DAG with colored edge information - while curr_rev >= stop_rev: - # Compute revs and next_revs - if curr_rev not in revs: - revs.append(curr_rev) # new head - colors[curr_rev] = new_color - new_color += 1 + For each DAG node this function emits tuples:: + + (id, type, data, (col, color), [(col, nextcol, color)]) + + with the following new elements: - idx = revs.index(curr_rev) - color = colors.pop(curr_rev) - next = revs[:] + - Tuple (col, color) with column and color index for the current node + - A list of tuples indicating the edges between the current node and its + parents. + """ + seen = [] + colors = {} + newcolor = 1 + for (cur, type, data, parents) in dag: - # Add parents to next_revs - parents = [x for x in cl.parentrevs(curr_rev) if x != nullrev] + # Compute seen and next + if cur not in seen: + seen.append(cur) # new head + colors[cur] = newcolor + newcolor += 1 + + col = seen.index(cur) + color = colors.pop(cur) + next = seen[:] + + # Add parents to next addparents = [p for p in parents if p not in next] - next[idx:idx + 1] = addparents + next[col:col + 1] = addparents # Set colors for the parents for i, p in enumerate(addparents): if not i: colors[p] = color else: - colors[p] = new_color - new_color += 1 + colors[p] = newcolor + newcolor += 1 # Add edges to the graph edges = [] - for col, r in enumerate(revs): - if r in next: - edges.append((col, next.index(r), colors[r])) - elif r == curr_rev: + for ecol, eid in enumerate(seen): + if eid in next: + edges.append((ecol, next.index(eid), colors[eid])) + elif eid == cur: for p in parents: - edges.append((col, next.index(p), colors[p])) + edges.append((ecol, next.index(p), colors[p])) # Yield and move on - yield (repo[curr_rev], (idx, color), edges) - revs = next - curr_rev -= 1 + yield (cur, type, data, (col, color), edges) + seen = next diff --git a/mercurial/help.py b/mercurial/help.py --- a/mercurial/help.py +++ b/mercurial/help.py @@ -6,6 +6,90 @@ # GNU General Public License version 2, incorporated herein by reference. from i18n import _ +import extensions + + +def moduledoc(file): + '''return the top-level python documentation for the given file + + Loosely inspired by pydoc.source_synopsis(), but rewritten to handle \''' + as well as """ and to return the whole text instead of just the synopsis''' + result = [] + + line = file.readline() + while line[:1] == '#' or not line.strip(): + line = file.readline() + if not line: break + + start = line[:3] + if start == '"""' or start == "'''": + line = line[3:] + while line: + if line.rstrip().endswith(start): + line = line.split(start)[0] + if line: + result.append(line) + break + elif not line: + return None # unmatched delimiter + result.append(line) + line = file.readline() + else: + return None + + return ''.join(result) + +def listexts(header, exts, maxlength): + '''return a text listing of the given extensions''' + if not exts: + return '' + result = '\n%s\n\n' % header + for name, desc in sorted(exts.iteritems()): + result += ' %s %s\n' % (name.ljust(maxlength), desc) + return result + +def extshelp(): + doc = _(r''' + Mercurial has a mechanism for adding new features through the + use of extensions. Extensions may bring new commands, or new + hooks, or change Mercurial's behavior. + + Extensions are not loaded by default for a variety of reasons, + they may be meant for advanced users or provide potentially + dangerous commands (e.g. mq and rebase allow history to be + rewritten), they might not be ready for prime-time yet, or + they may alter Mercurial's behavior. It is thus up to the user + to activate extensions as desired. + + To enable the "foo" extension, either shipped with Mercurial + or in the Python search path, create an entry for it in your + hgrc, like this: + + [extensions] + foo = + + You may also specify the full path to an extension: + + [extensions] + myfeature = ~/.hgext/myfeature.py + + To explicitly disable an extension enabled in an hgrc of broader + scope, prepend its path with !: + + [extensions] + # disabling extension bar residing in /ext/path + hgext.bar = !/path/to/extension/bar.py + # ditto, but no path was supplied for extension baz + hgext.baz = ! + ''') + + exts, maxlength = extensions.enabled() + doc += listexts(_('enabled extensions:'), exts, maxlength) + + exts, maxlength = extensions.disabled() + doc += listexts(_('disabled extensions:'), exts, maxlength) + + return doc helptable = ( (["dates"], _("Date Formats"), @@ -418,4 +502,5 @@ PYTHONPATH:: The push command will look for a path named 'default-push', and prefer it over 'default' if both are defined. ''')), + (["extensions"], _("Using additional features"), extshelp), ) diff --git a/mercurial/hgweb/hgweb_mod.py b/mercurial/hgweb/hgweb_mod.py --- a/mercurial/hgweb/hgweb_mod.py +++ b/mercurial/hgweb/hgweb_mod.py @@ -64,7 +64,8 @@ class hgweb(object): self.maxshortchanges = int(self.config("web", "maxshortchanges", 60)) self.maxfiles = int(self.config("web", "maxfiles", 10)) self.allowpull = self.configbool("web", "allowpull", True) - self.encoding = self.config("web", "encoding", encoding.encoding) + encoding.encoding = self.config("web", "encoding", + encoding.encoding) def run(self): if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."): @@ -81,28 +82,6 @@ class hgweb(object): self.refresh() - # process this if it's a protocol request - # protocol bits don't need to create any URLs - # and the clients always use the old URL structure - - cmd = req.form.get('cmd', [''])[0] - if cmd and cmd in protocol.__all__: - try: - if cmd in perms: - try: - self.check_perm(req, perms[cmd]) - except ErrorResponse, inst: - if cmd == 'unbundle': - req.drain() - raise - method = getattr(protocol, cmd) - return method(self.repo, req) - except ErrorResponse, inst: - req.respond(inst, protocol.HGTYPE) - if not inst.message: - return [] - return '0\n%s\n' % inst.message, - # work with CGI variables to create coherent structure # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME @@ -122,6 +101,30 @@ class hgweb(object): query = req.env['QUERY_STRING'].split('&', 1)[0] query = query.split(';', 1)[0] + # process this if it's a protocol request + # protocol bits don't need to create any URLs + # and the clients always use the old URL structure + + cmd = req.form.get('cmd', [''])[0] + if cmd and cmd in protocol.__all__: + if query: + raise ErrorResponse(HTTP_NOT_FOUND) + try: + if cmd in perms: + try: + self.check_perm(req, perms[cmd]) + except ErrorResponse, inst: + if cmd == 'unbundle': + req.drain() + raise + method = getattr(protocol, cmd) + return method(self.repo, req) + except ErrorResponse, inst: + req.respond(inst, protocol.HGTYPE) + if not inst.message: + return [] + return '0\n%s\n' % inst.message, + # translate user-visible url structure to internal structure args = query.split('/', 2) @@ -160,7 +163,7 @@ class hgweb(object): try: tmpl = self.templater(req) - ctype = tmpl('mimetype', encoding=self.encoding) + ctype = tmpl('mimetype', encoding=encoding.encoding) ctype = templater.stringify(ctype) # check read permissions non-static content @@ -219,7 +222,7 @@ class hgweb(object): # some functions for the templater def header(**map): - yield tmpl('header', encoding=self.encoding, **map) + yield tmpl('header', encoding=encoding.encoding, **map) def footer(**map): yield tmpl("footer", **map) diff --git a/mercurial/hgweb/hgwebdir_mod.py b/mercurial/hgweb/hgwebdir_mod.py --- a/mercurial/hgweb/hgwebdir_mod.py +++ b/mercurial/hgweb/hgwebdir_mod.py @@ -70,6 +70,8 @@ class hgwebdir(object): elif isinstance(self.conf, dict): paths = self.conf.items() + encoding.encoding = self.ui.config('web', 'encoding', + encoding.encoding) self.motd = self.ui.config('web', 'motd') self.style = self.ui.config('web', 'style', 'paper') self.stripecount = self.ui.config('web', 'stripes', 1) diff --git a/mercurial/hgweb/protocol.py b/mercurial/hgweb/protocol.py --- a/mercurial/hgweb/protocol.py +++ b/mercurial/hgweb/protocol.py @@ -162,8 +162,10 @@ def unbundle(repo, req): sys.stderr = sys.stdout = cStringIO.StringIO() try: - url = 'remote:%s:%s' % (proto, - req.env.get('REMOTE_HOST', '')) + url = 'remote:%s:%s:%s' % ( + proto, + urllib.quote(req.env.get('REMOTE_HOST', '')), + urllib.quote(req.env.get('REMOTE_USER', ''))) try: ret = repo.addchangegroup(gen, 'serve', url) except util.Abort, inst: diff --git a/mercurial/hgweb/webcommands.py b/mercurial/hgweb/webcommands.py --- a/mercurial/hgweb/webcommands.py +++ b/mercurial/hgweb/webcommands.py @@ -668,10 +668,13 @@ def graph(web, req, tmpl): count = len(web.repo) changenav = webutil.revnavgen(rev, revcount, count, web.repo.changectx) - tree = list(graphmod.graph(web.repo, rev, downrev)) + dag = graphmod.revisions(web.repo, rev, downrev) + tree = list(graphmod.colored(dag)) canvasheight = (len(tree) + 1) * bg_height - 27; data = [] - for (ctx, vtx, edges) in tree: + for (id, type, ctx, vtx, edges) in tree: + if type != graphmod.CHANGESET: + continue node = short(ctx.node()) age = templatefilters.age(ctx.date()) desc = templatefilters.firstline(ctx.description()) diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -262,7 +262,7 @@ class localrepository(repo.repository): warn(_("node '%s' is not well formed") % node) continue if bin_n not in self.changelog.nodemap: - warn(_("tag '%s' refers to unknown node") % key) + # silently ignore as pull -r might cause this continue h = [] @@ -290,11 +290,24 @@ class localrepository(repo.repository): globaltags[k] = an, ah tagtypes[k] = tagtype - # read the tags file from each head, ending with the tip + seen = set() f = None - for rev, node, fnode in self._hgtagsnodes(): - f = (f and f.filectx(fnode) or - self.filectx('.hgtags', fileid=fnode)) + ctxs = [] + for node in self.heads(): + try: + fnode = self[node].filenode('.hgtags') + except error.LookupError: + continue + if fnode not in seen: + seen.add(fnode) + if not f: + f = self.filectx('.hgtags', fileid=fnode) + else: + f = f.filectx(fnode) + ctxs.append(f) + + # read the tags file from each head, ending with the tip + for f in reversed(ctxs): readtags(f.data().splitlines(), f, "global") try: @@ -328,22 +341,6 @@ class localrepository(repo.repository): return self._tagstypecache.get(tagname) - def _hgtagsnodes(self): - last = {} - ret = [] - for node in reversed(self.heads()): - c = self[node] - rev = c.rev() - try: - fnode = c.filenode('.hgtags') - except error.LookupError: - continue - ret.append((rev, node, fnode)) - if fnode in last: - ret[last[fnode]] = None - last[fnode] = len(ret) - 1 - return [item for item in ret if item] - def tagslist(self): '''return a list of tags ordered by revision''' l = [] diff --git a/mercurial/patch.py b/mercurial/patch.py --- a/mercurial/patch.py +++ b/mercurial/patch.py @@ -972,7 +972,7 @@ def iterhunks(ui, fp, sourcefile=None, t def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, eol=None): """ - Reads a patch from fp and tries to apply it. + Reads a patch from fp and tries to apply it. The dict 'changed' is filled in with all of the filenames changed by the patch. Returns 0 for a clean patch, -1 if any rejects were @@ -1137,7 +1137,7 @@ def internalpatch(patchobj, ui, strip, c eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()] except KeyError: raise util.Abort(_('Unsupported line endings type: %s') % eolmode) - + try: fp = file(patchobj, 'rb') except TypeError: diff --git a/mercurial/url.py b/mercurial/url.py --- a/mercurial/url.py +++ b/mercurial/url.py @@ -109,7 +109,9 @@ class passwordmgr(urllib2.HTTPPasswordMg return (user, passwd) if not user: - user, passwd = self._readauthtoken(authuri) + auth = self.readauthtoken(authuri) + if auth: + user, passwd = auth.get('username'), auth.get('password') if not user or not passwd: if not self.ui.interactive(): raise util.Abort(_('http authorization required')) @@ -132,7 +134,7 @@ class passwordmgr(urllib2.HTTPPasswordMg msg = _('http auth: user %s, password %s\n') self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set')) - def _readauthtoken(self, uri): + def readauthtoken(self, uri): # Read configuration config = dict() for key, val in self.ui.configitems('auth'): @@ -143,7 +145,7 @@ class passwordmgr(urllib2.HTTPPasswordMg # Find the best match scheme, hostpath = uri.split('://', 1) bestlen = 0 - bestauth = None, None + bestauth = None for auth in config.itervalues(): prefix = auth.get('prefix') if not prefix: continue @@ -155,7 +157,7 @@ class passwordmgr(urllib2.HTTPPasswordMg if (prefix == '*' or hostpath.startswith(prefix)) and \ len(prefix) > bestlen and scheme in schemes: bestlen = len(prefix) - bestauth = auth.get('username'), auth.get('password') + bestauth = auth return bestauth class proxyhandler(urllib2.ProxyHandler): @@ -411,8 +413,38 @@ if has_https: send = _gen_sendfile(httplib.HTTPSConnection) class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): + def __init__(self, ui): + keepalive.KeepAliveHandler.__init__(self) + urllib2.HTTPSHandler.__init__(self) + self.ui = ui + self.pwmgr = passwordmgr(self.ui) + def https_open(self, req): - return self.do_open(httpsconnection, req) + self.auth = self.pwmgr.readauthtoken(req.get_full_url()) + return self.do_open(self._makeconnection, req) + + def _makeconnection(self, host, port=443, *args, **kwargs): + keyfile = None + certfile = None + + if args: # key_file + keyfile = args.pop(0) + if args: # cert_file + certfile = args.pop(0) + + # if the user has specified different key/cert files in + # hgrc, we prefer these + if self.auth and 'key' in self.auth and 'cert' in self.auth: + keyfile = self.auth['key'] + certfile = self.auth['cert'] + + # let host port take precedence + if ':' in host and '[' not in host or ']:' in host: + host, port = host.rsplit(':', 1) + if '[' in host: + host = host[1:-1] + + return httpsconnection(host, port, keyfile, certfile, *args, **kwargs) # In python < 2.5 AbstractDigestAuthHandler raises a ValueError if # it doesn't know about the auth type requested. This can happen if @@ -460,7 +492,7 @@ def opener(ui, authinfo=None): ''' handlers = [httphandler()] if has_https: - handlers.append(httpshandler()) + handlers.append(httpshandler(ui)) handlers.append(proxyhandler(ui)) diff --git a/tests/run-tests.py b/tests/run-tests.py --- a/tests/run-tests.py +++ b/tests/run-tests.py @@ -16,7 +16,7 @@ # If you change this script, it is recommended that you ensure you # haven't broken it by running it in various modes with a representative # sample of test scripts. For example: -# +# # 1) serial, no coverage, temp install: # ./run-tests.py test-s* # 2) serial, no coverage, local hg: diff --git a/tests/test-convert.out b/tests/test-convert.out --- a/tests/test-convert.out +++ b/tests/test-convert.out @@ -271,5 +271,5 @@ emptydir does not look like a darcs repo emptydir does not look like a monotone repo emptydir does not look like a GNU Arch repo emptydir does not look like a Bazaar repo -emptydir does not look like a P4 repo +cannot find required "p4" tool abort: emptydir: missing or unsupported repository diff --git a/tests/test-debugcomplete.out b/tests/test-debugcomplete.out --- a/tests/test-debugcomplete.out +++ b/tests/test-debugcomplete.out @@ -169,14 +169,14 @@ diff: rev, change, text, git, nodates, s export: output, switch-parent, text, git, nodates init: ssh, remotecmd log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, prune, patch, git, limit, no-merges, style, template, include, exclude -merge: force, rev, show +merge: force, rev, preview parents: rev, style, template pull: update, force, rev, ssh, remotecmd push: force, rev, ssh, remotecmd remove: after, force, include, exclude serve: accesslog, daemon, daemon-pipefds, errorlog, port, address, prefix, name, webdir-conf, pid-file, stdio, templates, style, ipv6, certificate status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, include, exclude -update: clean, date, rev +update: clean, check, date, rev addremove: similarity, include, exclude, dry-run archive: no-decode, prefix, rev, type, include, exclude backout: merge, parent, rev, include, exclude, message, logfile, date, user diff --git a/tests/test-double-merge b/tests/test-double-merge --- a/tests/test-double-merge +++ b/tests/test-double-merge @@ -20,7 +20,7 @@ hg cat foo >> foo hg ci -m 'change foo' -d "1000000 0" # we get conflicts that shouldn't be there -hg merge -S +hg merge -P hg merge --debug echo "-- foo --" diff --git a/tests/test-globalopts.out b/tests/test-globalopts.out --- a/tests/test-globalopts.out +++ b/tests/test-globalopts.out @@ -208,6 +208,7 @@ additional help topics: diffs Diff Formats templating Template Usage urls URL Paths + extensions Using additional features use "hg -v help" to show aliases and global options Mercurial Distributed SCM @@ -273,6 +274,7 @@ additional help topics: diffs Diff Formats templating Template Usage urls URL Paths + extensions Using additional features use "hg -v help" to show aliases and global options %% not tested: --debugger diff --git a/tests/test-grep b/tests/test-grep --- a/tests/test-grep +++ b/tests/test-grep @@ -73,3 +73,25 @@ hg grep octarine # Used to crash here hg grep -r 1 octarine +# Issue337: grep did not compared changesets by their revision numbers +# instead of following parent-child relationships. +cd .. +echo % issue 337 +hg init issue337 +cd issue337 + +echo white > color +hg commit -A -m "0 white" + +echo red > color +hg commit -A -m "1 red" + +hg update 0 +echo black > color +hg commit -A -m "2 black" + +hg update --clean 1 +echo blue > color +hg commit -A -m "3 blue" + +hg grep --all red diff --git a/tests/test-grep.out b/tests/test-grep.out --- a/tests/test-grep.out +++ b/tests/test-grep.out @@ -38,6 +38,13 @@ adding noeol noeol:4:no infinite loo % issue 685 adding color +colour:1:octarine color:0:octarine colour:1:octarine -colour:1:octarine +% issue 337 +adding color +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +created new head +1 files updated, 0 files merged, 0 files removed, 0 files unresolved +color:3:-:red +color:1:+:red diff --git a/tests/test-hardlinks-safety.out b/tests/test-hardlinks-safety.out --- a/tests/test-hardlinks-safety.out +++ b/tests/test-hardlinks-safety.out @@ -15,6 +15,8 @@ bar 430ed4828a74fa4047bc816a25500f7472ab4bfe:foo % foo +patch foo finalized without changeset message +patch bar finalized without changeset message %%% 4e7abb4840c46a910f6d7b4d3c3fc7e5209e684c foo 430ed4828a74fa4047bc816a25500f7472ab4bfe bar diff --git a/tests/test-help.out b/tests/test-help.out --- a/tests/test-help.out +++ b/tests/test-help.out @@ -99,6 +99,7 @@ additional help topics: diffs Diff Formats templating Template Usage urls URL Paths + extensions Using additional features use "hg -v help" to show aliases and global options add add the specified files on the next commit @@ -160,6 +161,7 @@ additional help topics: diffs Diff Formats templating Template Usage urls URL Paths + extensions Using additional features hg add [OPTION]... [FILE]... add the specified files on the next commit diff --git a/tests/test-merge-default b/tests/test-merge-default --- a/tests/test-merge-default +++ b/tests/test-merge-default @@ -31,7 +31,7 @@ hg merge 2 hg commit -mm1 echo % should succeed - 2 heads -hg merge -S +hg merge -P hg merge hg commit -mm2 diff --git a/tests/test-merge1 b/tests/test-merge1 --- a/tests/test-merge1 +++ b/tests/test-merge1 @@ -30,7 +30,7 @@ hg add c hg commit -m "commit #2" -d "1000000 0" echo This is file b1 > b echo %% no merges expected -hg merge -S 1 +hg merge -P 1 hg merge 1 hg diff --nodates hg status diff --git a/tests/test-mq-qdelete.out b/tests/test-mq-qdelete.out --- a/tests/test-mq-qdelete.out +++ b/tests/test-mq-qdelete.out @@ -13,9 +13,12 @@ a b series status +patch a finalized without changeset message 1 [mq]: a 0 base abort: cannot delete revision 3 above applied patches +patch d finalized without changeset message +patch e finalized without changeset message f 4 [mq]: f 3 [mq]: e @@ -32,11 +35,14 @@ abort: unknown revision 'c'! applying c patch c is empty now at: c +patch a finalized without changeset message +patch b finalized without changeset message c 3 imported patch c 2 [mq]: b 1 [mq]: a 0 base +patch c finalized without changeset message 3 imported patch c 2 [mq]: b 1 [mq]: a diff --git a/tests/test-mq-qimport.out b/tests/test-mq-qimport.out --- a/tests/test-mq-qimport.out +++ b/tests/test-mq-qimport.out @@ -27,3 +27,5 @@ 1 files updated, 0 files merged, 0 files adding another.diff to series file applying another.diff now at: another.diff +patch b.diff finalized without changeset message +patch another.diff finalized without changeset message diff --git a/tests/test-mq-qpush-fail b/tests/test-mq-qpush-fail --- a/tests/test-mq-qpush-fail +++ b/tests/test-mq-qpush-fail @@ -45,3 +45,12 @@ hg parents echo '% bar should be gone; other unknown/ignored files should still be around' hg status -A + +echo '% preparing qpush of a missing patch' +hg qpop -a +hg qpush +rm .hg/patches/patch2 +echo '% now we expect the push to fail, but it should NOT complain about patch1' +hg qpush + +true # happy ending diff --git a/tests/test-mq-qpush-fail.out b/tests/test-mq-qpush-fail.out --- a/tests/test-mq-qpush-fail.out +++ b/tests/test-mq-qpush-fail.out @@ -19,3 +19,11 @@ summary: add foo ? untracked-file I .hgignore C foo +% preparing qpush of a missing patch +no patches applied +applying patch1 +now at: patch1 +% now we expect the push to fail, but it should NOT complain about patch1 +applying patch2 +unable to read patch2 +now at: patch1 diff --git a/tests/test-tags.out b/tests/test-tags.out --- a/tests/test-tags.out +++ b/tests/test-tags.out @@ -26,14 +26,12 @@ created new head .hgtags@c071f74ab5eb, line 2: cannot parse entry .hgtags@c071f74ab5eb, line 4: node 'foo' is not well formed .hgtags@4ca6f1b1a68c, line 2: node 'x' is not well formed -localtags, line 1: tag 'invalid' refers to unknown node tip 8:4ca6f1b1a68c first 0:0acdaf898367 changeset: 8:4ca6f1b1a68c .hgtags@c071f74ab5eb, line 2: cannot parse entry .hgtags@c071f74ab5eb, line 4: node 'foo' is not well formed .hgtags@4ca6f1b1a68c, line 2: node 'x' is not well formed -localtags, line 1: tag 'invalid' refers to unknown node tag: tip parent: 3:b2ef3841386b user: test diff --git a/tests/test-up-issue1456 b/tests/test-up-issue1456 --- a/tests/test-up-issue1456 +++ b/tests/test-up-issue1456 @@ -10,7 +10,7 @@ chmod +x foo hg ci -m1 hg co -q 0 echo dirty > foo -sleep 1 +hg up -c hg up -q cat foo hg st -A diff --git a/tests/test-up-issue1456.out b/tests/test-up-issue1456.out --- a/tests/test-up-issue1456.out +++ b/tests/test-up-issue1456.out @@ -1,3 +1,4 @@ +abort: uncommitted local changes dirty M foo % validate update of standalone execute bit change