# HG changeset patch # User Thomas Arendsen Hein # Date 1140534287 -3600 # Node ID 982fb022a16a46930e48c9adfc6b220b6c824bcb # Parent b9fac31f34c9381252f00739b4b97b2e1d1ffddd# Parent f79afc26ae3b347000ced2d9469213b4a1fbe4e9 Merged RSS feed for tags from Peter van Dijk diff --git a/contrib/bash_completion b/contrib/bash_completion --- a/contrib/bash_completion +++ b/contrib/bash_completion @@ -2,36 +2,36 @@ shopt -s extglob _hg_command_list() { - hg --debug help 2>/dev/null | \ + "$hg" --debug help 2>/dev/null | \ awk 'function command_line(line) { - gsub(/,/, "", line) - gsub(/:.*/, "", line) - split(line, aliases) - command = aliases[1] - delete aliases[1] - print command - for (i in aliases) - if (index(command, aliases[i]) != 1) - print aliases[i] - } - /^list of commands:/ {commands=1} - commands && /^ debug/ {a[i++] = $0; next;} - commands && /^ [^ ]/ {command_line($0)} - /^global options:/ {exit 0} - END {for (i in a) command_line(a[i])}' + gsub(/,/, "", line) + gsub(/:.*/, "", line) + split(line, aliases) + command = aliases[1] + delete aliases[1] + print command + for (i in aliases) + if (index(command, aliases[i]) != 1) + print aliases[i] + } + /^list of commands:/ {commands=1} + commands && /^ debug/ {a[i++] = $0; next;} + commands && /^ [^ ]/ {command_line($0)} + /^global options:/ {exit 0} + END {for (i in a) command_line(a[i])}' } _hg_option_list() { - hg -v help $1 2> /dev/null | \ - awk '/^ *-/ { - for (i = 1; i <= NF; i ++) { + "$hg" -v help $1 2>/dev/null | \ + awk '/^ *-/ { + for (i = 1; i <= NF; i ++) { if (index($i, "-") != 1) - break; + break; print $i; - } - }' + } + }' } @@ -56,29 +56,29 @@ shopt -s extglob _hg_paths() { - local paths="$(hg paths 2> /dev/null | sed -e 's/ = .*$//')" - COMPREPLY=(${COMPREPLY[@]:-} $( compgen -W '$paths' -- "$cur" )) + local paths="$("$hg" paths 2>/dev/null | sed -e 's/ = .*$//')" + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$paths' -- "$cur")) } _hg_repos() { local i - for i in $( compgen -d -- "$cur" ); do - test ! -d "$i"/.hg || COMPREPLY=(${COMPREPLY[@]:-} "$i") + for i in $(compgen -d -- "$cur"); do + test ! -d "$i"/.hg || COMPREPLY=(${COMPREPLY[@]:-} "$i") done } _hg_status() { - local files="$( hg status -n$1 . 2> /dev/null)" - COMPREPLY=(${COMPREPLY[@]:-} $( compgen -W '$files' -- "$cur" )) + local files="$("$hg" status -n$1 . 2>/dev/null)" + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$files' -- "$cur")) } _hg_tags() { - local tags="$(hg tags 2> /dev/null | - sed -e 's/[0-9]*:[a-f0-9]\{40\}$//; s/ *$//')" - COMPREPLY=( ${COMPREPLY[@]:-} $(compgen -W '$tags' -- "$cur") ) + local tags="$("$hg" tags 2>/dev/null | + sed -e 's/[0-9]*:[a-f0-9]\{40\}$//; s/ *$//')" + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$tags' -- "$cur")) } # this is "kind of" ugly... @@ -87,7 +87,7 @@ shopt -s extglob local i count=0 local filters="$1" - for (( i=1; $i<=$COMP_CWORD; i++ )); do + for ((i=1; $i<=$COMP_CWORD; i++)); do if [[ "${COMP_WORDS[i]}" != -* ]]; then if [[ ${COMP_WORDS[i-1]} == @($filters|$global_args) ]]; then continue @@ -104,6 +104,7 @@ shopt -s extglob local cur prev cmd opts i # global options that receive an argument local global_args='--cwd|-R|--repository' + local hg="$1" COMPREPLY=() cur="$2" @@ -112,7 +113,7 @@ shopt -s extglob # searching for the command # (first non-option argument that doesn't follow a global option that # receives an argument) - for (( i=1; $i<=$COMP_CWORD; i++ )); do + for ((i=1; $i<=$COMP_CWORD; i++)); do if [[ ${COMP_WORDS[i]} != -* ]]; then if [[ ${COMP_WORDS[i-1]} != @($global_args) ]]; then cmd="${COMP_WORDS[i]}" @@ -124,7 +125,7 @@ shopt -s extglob if [[ "$cur" == -* ]]; then opts=$(_hg_option_list $cmd) - COMPREPLY=( ${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur") ) + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -W '$opts' -- "$cur")) return fi @@ -146,7 +147,7 @@ shopt -s extglob fi # canonicalize command name - cmd=$(hg -q help "$cmd" 2> /dev/null | sed -e 's/^hg //; s/ .*//; 1q') + cmd=$("$hg" -q help "$cmd" 2>/dev/null | sed -e 's/^hg //; s/ .*//; 1q') if [ "$cmd" != status ] && [ "$prev" = -r ] || [ "$prev" = --rev ]; then _hg_tags @@ -190,17 +191,17 @@ shopt -s extglob if [ $count = 1 ]; then _hg_paths fi - _hg_repos + _hg_repos ;; debugindex|debugindexdot) - COMPREPLY=(${COMPREPLY[@]:-} $( compgen -f -X "!*.i" -- "$cur" )) + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.i" -- "$cur")) ;; debugdata) - COMPREPLY=(${COMPREPLY[@]:-} $( compgen -f -X "!*.d" -- "$cur" )) + COMPREPLY=(${COMPREPLY[@]:-} $(compgen -f -X "!*.d" -- "$cur")) ;; esac } -complete -o bashdefault -o default -F _hg hg 2> /dev/null \ +complete -o bashdefault -o default -F _hg hg 2>/dev/null \ || complete -o default -F _hg hg diff --git a/contrib/convert-repo b/contrib/convert-repo --- a/contrib/convert-repo +++ b/contrib/convert-repo @@ -21,7 +21,7 @@ # interrupted and can be run repeatedly to copy new commits. import sys, os, zlib, sha, time -from mercurial import hg, ui, util, commands +from mercurial import hg, ui, util class convert_git: def __init__(self, path): @@ -113,7 +113,7 @@ class convert_mercurial: except: pass - def putcommit(self, files, parents, author, date, text): + def putcommit(self, files, parents, author, dest, text): seen = {} pl = [] for p in parents: @@ -129,13 +129,8 @@ class convert_mercurial: while parents: p1 = p2 p2 = parents.pop(0) - self.repo.dirstate.setparents(hg.bin(p1), hg.bin(p2)) - if len(files) > 0: - olddir = os.getcwd() - os.chdir(self.path) - commands.addremove(self.repo.ui, self.repo, *files) - os.chdir(olddir) - self.repo.commit(files, text, author, date) + self.repo.rawcommit(files, text, author, dest, + hg.bin(p1), hg.bin(p2)) text = "(octopus merge fixup)\n" p2 = hg.hex(self.repo.changelog.tip()) @@ -265,6 +260,7 @@ class convert: t = self.toposort(parents) t = [n for n in t if n not in self.map] num = len(t) + c = None for c in t: num -= 1 @@ -279,7 +275,7 @@ class convert: if v in self.map: ctags[k] = self.map[v] - if ctags: + if c and ctags: nrev = self.dest.puttags(ctags) # write another hash correspondence to override the previous # one so we don't end up with extra tag heads diff --git a/contrib/hbisect.py b/contrib/hbisect.py --- a/contrib/hbisect.py +++ b/contrib/hbisect.py @@ -187,7 +187,7 @@ class bisect(object): check_clean(self.ui, self.repo) rev = self.next() self.ui.write("Now testing %s\n" % hg.hex(rev)) - return self.repo.update(rev, allow=True, force=True) + return self.repo.update(rev, force=True) def good(self, rev): self.goodrevs.append(rev) @@ -232,7 +232,7 @@ def test(ui, repo, rev): b.good(new_rev) ui.write("it is good\n") anc = b.ancestors() - repo.update(new_rev, allow=True, force=True) + repo.update(new_rev, force=True) for v in anc: if v != rev: ui.warn("fail to found cset! :(\n") diff --git a/contrib/macosx/Readme.html b/contrib/macosx/Readme.html new file mode 100644 --- /dev/null +++ b/contrib/macosx/Readme.html @@ -0,0 +1,38 @@ + + + + + + + + + +

Before you install

+


+

This is not a stand-alone version of Mercurial.

+


+

To use it, you must have the “official unofficial” MacPython 2.4.1 installed.

+


+

You can download MacPython 2.4.1 from here:

+

http://python.org/ftp/python/2.4.1/MacPython-OSX-2.4.1-1.dmg

+


+

For more information on MacPython, go here:

+

http://undefined.org/python

+


+

After you install

+


+

This package installs the hg executable in /usr/local/bin. This directory may not be in your shell's search path. Don't forget to check.

+


+

Reporting problems

+


+

If you run into any problems, please file a bug online:

+

http://www.selenic.com/mercurial/bts

+ + diff --git a/contrib/macosx/Welcome.html b/contrib/macosx/Welcome.html new file mode 100644 --- /dev/null +++ b/contrib/macosx/Welcome.html @@ -0,0 +1,17 @@ + + + + + + + + + +

This is a prepackaged release of Mercurial for Mac OS X.

+


+

It is based on Mercurial 0.8.

+ + diff --git a/contrib/macosx/macosx-build.txt b/contrib/macosx/macosx-build.txt new file mode 100644 --- /dev/null +++ b/contrib/macosx/macosx-build.txt @@ -0,0 +1,11 @@ +to build a new macosx binary package: + +install macpython from http://undefined.org/python/ + +install py2app from http://pythonmac.org/packages/ + +make sure /usr/local/bin is in your path + +run bdist_mpkg in top-level hg directory + +find packaged stuff in dist directory diff --git a/contrib/win32/ReadMe.html b/contrib/win32/ReadMe.html --- a/contrib/win32/ReadMe.html +++ b/contrib/win32/ReadMe.html @@ -5,7 +5,7 @@ -

Mercurial version 0.7 for Windows

+

Mercurial version 0.8 for Windows

Welcome to Mercurial for Windows!

diff --git a/contrib/win32/mercurial.iss b/contrib/win32/mercurial.iss --- a/contrib/win32/mercurial.iss +++ b/contrib/win32/mercurial.iss @@ -4,7 +4,7 @@ [Setup] AppCopyright=Copyright 2005 Matt Mackall and others AppName=Mercurial -AppVerName=Mercurial version 0.7 +AppVerName=Mercurial version 0.8 InfoAfterFile=contrib/win32/postinstall.txt LicenseFile=COPYING ShowLanguageDialog=yes @@ -14,10 +14,10 @@ AppSupportURL=http://www.selenic.com/mer AppUpdatesURL=http://www.selenic.com/mercurial AppID={{4B95A5F1-EF59-4B08-BED8-C891C46121B3} AppContact=mercurial@selenic.com -OutputBaseFilename=Mercurial-0.7 +OutputBaseFilename=Mercurial-0.8 DefaultDirName={sd}\Mercurial SourceDir=C:\hg\hg-release -VersionInfoVersion=0.7 +VersionInfoVersion=0.8 VersionInfoDescription=Mercurial distributed SCM VersionInfoCopyright=Copyright 2005 Matt Mackall and others VersionInfoCompany=Matt Mackall and others diff --git a/contrib/win32/postinstall.txt b/contrib/win32/postinstall.txt --- a/contrib/win32/postinstall.txt +++ b/contrib/win32/postinstall.txt @@ -8,6 +8,27 @@ file that comes with this package. Release Notes ------------- +2006-01-29 v0.8 + +* Upgrade notes: + + - diff and status command are now repo-wide by default + (use 'hg diff .' for the old behavior) + - GPG signing is now done with the gpg extension + - the --text option for commit, rawcommit, and tag has been removed + - the copy/rename --parents option has been removed + +* Major changes from 0.7 to 0.8: + + - faster status, diff, and commit + - reduced memory usage for push and pull + - improved extension API + - new bisect, gpg, hgk, and win32text extensions + - short URLs, binary file handling, and optional gitweb skin for hgweb + - numerous new command options including log --keyword and pull --rev + - improved hooks and file filtering + + 2005-09-21 v0.7 with modifications * New INI files have been added to control Mercurial's behaviour: diff --git a/doc/Makefile b/doc/Makefile --- a/doc/Makefile +++ b/doc/Makefile @@ -15,7 +15,7 @@ html: $(HTML) asciidoc -d manpage -b docbook $*.txt %.html: %.txt - asciidoc -b html4 $*.txt + asciidoc -b html4 $*.txt || asciidoc -b html $*.txt clean: $(RM) $(MAN) $(MAN:%=%.xml) $(MAN:%=%.html) diff --git a/doc/hg.1.txt b/doc/hg.1.txt --- a/doc/hg.1.txt +++ b/doc/hg.1.txt @@ -223,9 +223,11 @@ diff [-a] [-r revision] [-r revision] [f probably with undesirable results. options: - -a, --text treat all files as text - -I, --include include names matching the given patterns - -X, --exclude exclude names matching the given patterns + -a, --text treat all files as text + -I, --include include names matching the given patterns + -p, --show-function show which function each change is in + -X, --exclude exclude names matching the given patterns + -w, --ignore-all-space ignore white space when comparing lines export [-o filespec] [revision] ...:: Print the changeset header and diffs for one or more revisions. @@ -374,6 +376,11 @@ log [-r revision ...] [-p] [files]:: options: -I, --include include names matching the given patterns -X, --exclude exclude names matching the given patterns + -b, --branch show branches + -k, --keyword search for keywords + -l, --limit print no more than this many changes + -M, --no-merges do not show merges + -m, --only-merges only show merges -r, --rev show the specified revision or range -p, --patch show patch @@ -541,10 +548,12 @@ serve [options]:: options: -A, --accesslog name of access log file to write to + -d, --daemon run server in background, as a daemon -E, --errorlog name of error log file to write to -a, --address address to use -p, --port port to use (default: 8000) -n, --name name to show in web pages (default: working dir) + --pid-file write server process ID to given file -t, --templatedir web templates to use -6, --ipv6 use IPv6 in addition to IPv4 @@ -600,9 +609,12 @@ tags:: This lists both regular and local tags. -tip:: +tip [-p]:: Show the tip revision. + options: + -p, --patch show patch + unbundle :: (EXPERIMENTAL) diff --git a/doc/hgrc.5.txt b/doc/hgrc.5.txt --- a/doc/hgrc.5.txt +++ b/doc/hgrc.5.txt @@ -141,20 +141,75 @@ hooks:: [hooks] # do not use the site-wide hook - commit = - commit.email = /my/email/hook - commit.autobuild = /my/build/hook + incoming = + incoming.email = /my/email/hook + incoming.autobuild = /my/build/hook + + Most hooks are run with environment variables set that give added + useful information. For each hook below, the environment variables + it is passed are listed with names of the form "$HG_foo". changegroup;; - Run after a changegroup has been added via push or pull. Passed - the ID of the first new changeset in $NODE. + Run after a changegroup has been added via push, pull or + unbundle. ID of the first new changeset is in $HG_NODE. commit;; - Run after a changeset has been created or for each changeset - pulled. Passed the ID of the newly created changeset in - environment variable $NODE. + Run after a changeset has been created in the local repository. + ID of the newly created changeset is in $HG_NODE. Parent + changeset IDs are in $HG_PARENT1 and $HG_PARENT2. + incoming;; + Run after a changeset has been pulled, pushed, or unbundled into + the local repository. The ID of the newly arrived changeset is in + $HG_NODE. + outgoing;; + Run after sending changes from local repository to another. ID of + first changeset sent is in $HG_NODE. Source of operation is in + $HG_SOURCE; see "preoutgoing" hook for description. + prechangegroup;; + Run before a changegroup is added via push, pull or unbundle. + Exit status 0 allows the changegroup to proceed. Non-zero status + will cause the push, pull or unbundle to fail. precommit;; - Run before starting a commit. Exit status 0 allows the commit to - proceed. Non-zero status will cause the commit to fail. + 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. + preoutgoing;; + Run before computing changes to send from the local repository to + another. Non-zero status will cause failure. This lets you + prevent pull over http or ssh. Also prevents against local pull, + push (outbound) or bundle commands, but not effective, since you + can just copy files instead then. Source of operation is in + $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. + 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 + changeset to tag is in $HG_NODE. Name of tag is in $HG_TAG. Tag + is local if $HG_LOCAL=1, in repo if $HG_LOCAL=0. + pretxnchangegroup;; + Run after a changegroup has been added via push, pull or unbundle, + but before the transaction has been committed. Changegroup is + visible to hook program. This lets you validate incoming changes + before accepting them. Passed the ID of the first new changeset + in $HG_NODE. Exit status 0 allows the transaction to commit. + Non-zero status will cause the transaction to be rolled back and + the push, pull or unbundle will fail. + pretxncommit;; + Run after a changeset has been created but the transaction not yet + committed. Changeset is visible to hook program. This lets you + validate commit message and changes. Exit status 0 allows the + commit to proceed. Non-zero status will cause the transaction to + be rolled back. ID of changeset is in $HG_NODE. Parent changeset + IDs are in $HG_PARENT1 and $HG_PARENT2. + 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 repo if $HG_LOCAL=0. + + In earlier releases, the names of hook environment variables did not + have a "HG_" prefix. These unprefixed names are still provided in + the environment for backwards compatibility, but their use is + deprecated, and they will be removed in a future release. http_proxy:: Used to access web-based Mercurial repositories through a HTTP diff --git a/hg b/hg --- a/hg +++ b/hg @@ -1,9 +1,8 @@ #!/usr/bin/env python # -# mercurial - a minimal scalable distributed SCM -# v0.6 "paola" +# mercurial - scalable distributed SCM # -# Copyright 2005 Matt Mackall +# Copyright 2005, 2006 Matt Mackall # # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. diff --git a/hgeditor b/hgeditor --- a/hgeditor +++ b/hgeditor @@ -34,7 +34,6 @@ HGTMP="${TMPDIR-/tmp}/hgeditor.$RANDOM.$ } ( - cd "`hg root`" grep '^HG: changed' "$1" | cut -b 13- | while read changed; do hg diff "$changed" >> "$HGTMP/diff" done diff --git a/hgext/gpg.py b/hgext/gpg.py --- a/hgext/gpg.py +++ b/hgext/gpg.py @@ -1,6 +1,14 @@ -import os, tempfile, binascii, errno +# 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, incorporated herein by reference. + +import os, tempfile, binascii from mercurial import util from mercurial import node as hgnode +from mercurial.i18n import gettext as _ class gpg: def __init__(self, path, key=None): @@ -14,6 +22,7 @@ class gpg: def verify(self, data, sig): """ returns of the good and bad signatures""" try: + # create temporary files fd, sigfile = tempfile.mkstemp(prefix="hggpgsig") fp = os.fdopen(fd, 'wb') fp.write(sig) @@ -22,8 +31,8 @@ class gpg: fp = os.fdopen(fd, 'wb') fp.write(data) fp.close() - gpgcmd = "%s --logger-fd 1 --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile) - #gpgcmd = "%s --status-fd 1 --verify \"%s\" \"%s\"" % (self.path, sigfile, datafile) + gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify " + "\"%s\" \"%s\"" % (self.path, sigfile, datafile)) ret = util.filter("", gpgcmd) except: for f in (sigfile, datafile): @@ -41,7 +50,7 @@ class gpg: continue l = l[9:] if l.startswith("ERRSIG"): - err = "error while verifying signature" + err = _("error while verifying signature") break elif l.startswith("VALIDSIG"): # fingerprint of the primary key @@ -61,12 +70,97 @@ class gpg: return err, keys def newgpg(ui, **opts): + """create a new gpg instance""" gpgpath = ui.config("gpg", "cmd", "gpg") gpgkey = opts.get('key') if not gpgkey: gpgkey = ui.config("gpg", "key", None) return gpg(gpgpath, gpgkey) +def sigwalk(repo): + """ + walk over every sigs, yields a couple + ((node, version, sig), (filename, linenumber)) + """ + def parsefile(fileiter, context): + ln = 1 + for l in fileiter: + if not l: + continue + yield (l.split(" ", 2), (context, ln)) + ln +=1 + + fl = repo.file(".hgsigs") + h = fl.heads() + h.reverse() + # read the heads + for r in h: + fn = ".hgsigs|%s" % hgnode.short(r) + for item in parsefile(fl.read(r).splitlines(), fn): + yield item + try: + # read local signatures + fn = "localsigs" + for item in parsefile(repo.opener(fn), fn): + yield item + except IOError: + pass + +def getkeys(ui, repo, mygpg, sigdata, context): + """get the keys who signed a data""" + fn, ln = context + node, version, sig = sigdata + prefix = "%s:%d" % (fn, ln) + node = hgnode.bin(node) + + data = node2txt(repo, node, version) + sig = binascii.a2b_base64(sig) + err, keys = mygpg.verify(data, sig) + if err: + ui.warn("%s:%d %s\n" % (fn, ln , err)) + return None + + validkeys = [] + # warn for expired key and/or sigs + for key in keys: + if key[0] == "BADSIG": + ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2])) + continue + if key[0] == "EXPSIG": + ui.write(_("%s Note: Signature has expired" + " (signed by: \"%s\")\n") % (prefix, key[2])) + elif key[0] == "EXPKEYSIG": + ui.write(_("%s Note: This key has expired" + " (signed by: \"%s\")\n") % (prefix, key[2])) + validkeys.append((key[1], key[2], key[3])) + return validkeys + +def sigs(ui, repo): + """list signed changesets""" + mygpg = newgpg(ui) + revs = {} + + for data, context in sigwalk(repo): + node, version, sig = data + fn, ln = context + try: + n = repo.lookup(node) + except KeyError: + ui.warn(_("%s:%d node does not exist\n") % (fn, ln)) + continue + r = repo.changelog.rev(n) + keys = getkeys(ui, repo, mygpg, data, context) + if not keys: + continue + revs.setdefault(r, []) + revs[r].extend(keys) + nodes = list(revs) + nodes.reverse() + for rev in nodes: + for k in revs[rev]: + r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev))) + ui.write("%-30s %s\n" % (keystr(ui, k), r)) + def check(ui, repo, rev): """verify all the signatures there may be for a particular revision""" mygpg = newgpg(ui) @@ -74,63 +168,30 @@ def check(ui, repo, rev): hexrev = hgnode.hex(rev) keys = [] - def addsig(fn, ln, l): - if not l: return - n, v, sig = l.split(" ", 2) - if n == hexrev: - data = node2txt(repo, rev, v) - sig = binascii.a2b_base64(sig) - err, k = mygpg.verify(data, sig) - if not err: - keys.append((k, fn, ln)) - else: - ui.warn("%s:%d %s\n" % (fn, ln , err)) - - fl = repo.file(".hgsigs") - h = fl.heads() - h.reverse() - # read the heads - for r in h: - ln = 1 - for l in fl.read(r).splitlines(): - addsig(".hgsigs|%s" % hgnode.short(r), ln, l) - ln +=1 - try: - # read local signatures - ln = 1 - f = repo.opener("localsigs") - for l in f: - addsig("localsigs", ln, l) - ln +=1 - except IOError: - pass + for data, context in sigwalk(repo): + node, version, sig = data + if node == hexrev: + k = getkeys(ui, repo, mygpg, data, context) + if k: + keys.extend(k) if not keys: - ui.write("%s not signed\n" % hgnode.short(rev)) + ui.write(_("No valid signature for %s\n") % hgnode.short(rev)) return - valid = [] - # warn for expired key and/or sigs - for k, fn, ln in keys: - prefix = "%s:%d" % (fn, ln) - for key in k: - if key[0] == "BADSIG": - ui.write("%s Bad signature from \"%s\"\n" % (prefix, key[2])) - continue - if key[0] == "EXPSIG": - ui.write("%s Note: Signature has expired" - " (signed by: \"%s\")\n" % (prefix, key[2])) - elif key[0] == "EXPKEYSIG": - ui.write("%s Note: This key has expired" - " (signed by: \"%s\")\n" % (prefix, key[2])) - valid.append((key[1], key[2], key[3])) + # print summary ui.write("%s is signed by:\n" % hgnode.short(rev)) - for keyid, user, fingerprint in valid: - role = getrole(ui, fingerprint) - ui.write(" %s (%s)\n" % (user, role)) + for key in keys: + ui.write(" %s\n" % keystr(ui, key)) -def getrole(ui, fingerprint): - return ui.config("gpg", fingerprint, "no role defined") +def keystr(ui, key): + """associate a string to a key (username, comment)""" + keyid, user, fingerprint = key + comment = ui.config("gpg", fingerprint, None) + if comment: + return "%s (%s)" % (user, comment) + else: + return user def sign(ui, repo, *revs, **opts): """add a signature for the current tip or a given revision""" @@ -150,7 +211,7 @@ def sign(ui, repo, *revs, **opts): data = node2txt(repo, n, sigver) sig = mygpg.sign(data) if not sig: - raise util.Abort("Error while signing") + raise util.Abort(_("Error while signing")) sig = binascii.b2a_base64(sig) sig = sig.replace("\n", "") sigmessage += "%s %s %s\n" % (hexnode, sigver, sig) @@ -162,9 +223,9 @@ def sign(ui, repo, *revs, **opts): for x in repo.changes(): if ".hgsigs" in x and not opts["force"]: - raise util.Abort("working copy of .hgsigs is changed " - "(please commit .hgsigs manually" - "or use --force)") + raise util.Abort(_("working copy of .hgsigs is changed " + "(please commit .hgsigs manually " + "or use --force)")) repo.wfile(".hgsigs", "ab").write(sigmessage) @@ -176,7 +237,8 @@ def sign(ui, repo, *revs, **opts): message = opts['message'] if not message: - message = "\n".join(["Added signature for changeset %s" % hgnode.hex(n) + message = "\n".join([_("Added signature for changeset %s") + % hgnode.hex(n) for n in nodes]) try: repo.commit([".hgsigs"], message, opts['user'], opts['date']) @@ -188,19 +250,20 @@ def node2txt(repo, node, ver): if ver == "0": return "%s\n" % hgnode.hex(node) else: - util.Abort("unknown signature version") + raise util.Abort(_("unknown signature version")) cmdtable = { "sign": (sign, - [('l', 'local', None, "make the signature local"), - ('f', 'force', None, "sign even if the sigfile is modified"), - ('', 'no-commit', None, "do not commit the sigfile after signing"), - ('m', 'message', "", "commit message"), - ('d', 'date', "", "date code"), - ('u', 'user', "", "user"), - ('k', 'key', "", "the key id to sign with")], - "hg sign [OPTION]... REVISIONS"), - "sigcheck": (check, [], 'hg sigcheck REVISION') + [('l', 'local', None, _("make the signature local")), + ('f', 'force', None, _("sign even if the sigfile is modified")), + ('', 'no-commit', None, _("do not commit the sigfile after signing")), + ('m', 'message', "", _("commit message")), + ('d', 'date', "", _("date code")), + ('u', 'user', "", _("user")), + ('k', 'key', "", _("the key id to sign with"))], + _("hg sign [OPTION]... [REVISION]...")), + "sigcheck": (check, [], _('hg sigcheck REVISION')), + "sigs": (sigs, [], _('hg sigs')), } diff --git a/contrib/patchbomb b/hgext/patchbomb.py old mode 100755 new mode 100644 rename from contrib/patchbomb rename to hgext/patchbomb.py --- a/contrib/patchbomb +++ b/hgext/patchbomb.py @@ -1,7 +1,5 @@ -#!/usr/bin/python -# -# Interactive script for sending a collection of Mercurial changesets -# as a series of patch emails. +# Command for sending a collection of Mercurial changesets as a series +# of patch emails. # # The series is started off with a "[PATCH 0 of N]" introduction, # which describes the series as a whole. @@ -29,6 +27,10 @@ # firing it up "for real", in which case it will use your pager to # display each of the messages that it would send. # +# The "-m" (mbox) option will create an mbox file instead of sending +# the messages directly. This can be reviewed e.g. with "mutt -R -f mbox", +# and finally sent with "formail -s sendmail -bm -t < mbox". +# # To configure a default mail host, add a section like this to your # hgrc file: # @@ -49,10 +51,11 @@ from email.MIMEMultipart import MIMEMultipart from email.MIMEText import MIMEText +from email.Utils import parseaddr from mercurial import commands -from mercurial import fancyopts from mercurial import hg from mercurial import ui +from mercurial.i18n import gettext as _ import os import popen2 import smtplib @@ -89,6 +92,17 @@ def diffstat(patch): except: pass def patchbomb(ui, repo, *revs, **opts): + '''send changesets as a series of patch emails + + The series starts with a "[PATCH 0 of N]" introduction, which + describes the series as a whole. + + Each patch email has a Subject line of "[PATCH M of N] ...", using + the first line of the changeset description as the subject text. + The message contains two or three body parts. First, the rest of + the changeset description. Next, (optionally) if the diffstat + program is installed, the result of running diffstat on the patch. + Finally, the patch itself, as generated by "hg export".''' def prompt(prompt, default = None, rest = ': ', empty_ok = False): if default: prompt += ' [%s]' % default prompt += rest @@ -97,7 +111,7 @@ def patchbomb(ui, repo, *revs, **opts): if r: return r if default is not None: return default if empty_ok: return r - ui.warn('Please enter a valid value.\n') + ui.warn(_('Please enter a valid value.\n')) def confirm(s): if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'): @@ -109,7 +123,7 @@ def patchbomb(ui, repo, *revs, **opts): if summary: ui.write(summary, '\n') ui.write(s, '\n') - confirm('Does the diffstat above look okay') + confirm(_('Does the diffstat above look okay')) return s def makepatch(patch, idx, total): @@ -162,20 +176,20 @@ def patchbomb(ui, repo, *revs, **opts): self.container.append(''.join(self.lines).split('\n')) self.lines = [] - commands.export(ui, repo, *args, **{'output': exportee(patches), + commands.export(ui, repo, *revs, **{'output': exportee(patches), 'switch_parent': False, 'text': None}) jumbo = [] msgs = [] - ui.write('This patch series consists of %d patches.\n\n' % len(patches)) + ui.write(_('This patch series consists of %d patches.\n\n') % len(patches)) for p, i in zip(patches, range(len(patches))): jumbo.extend(p) msgs.append(makepatch(p, i + 1, len(patches))) - ui.write('\nWrite the introductory message for the patch series.\n\n') + ui.write(_('\nWrite the introductory message for the patch series.\n\n')) sender = (opts['from'] or ui.config('patchbomb', 'from') or prompt('From', ui.username())) @@ -193,7 +207,7 @@ def patchbomb(ui, repo, *revs, **opts): to = getaddrs('to', 'To') cc = getaddrs('cc', 'Cc', '') - ui.write('Finish with ^D or a dot on a line by itself.\n\n') + ui.write(_('Finish with ^D or a dot on a line by itself.\n\n')) body = [] @@ -208,12 +222,12 @@ def patchbomb(ui, repo, *revs, **opts): ui.write('\n') if opts['diffstat']: - d = cdiffstat('Final summary:\n', jumbo) + d = cdiffstat(_('Final summary:\n'), jumbo) if d: msg.attach(MIMEText(d)) msgs.insert(0, msg) - if not opts['test']: + if not opts['test'] and not opts['mbox']: s = smtplib.SMTP() s.connect(host = ui.config('smtp', 'host', 'mail'), port = int(ui.config('smtp', 'port', 25))) @@ -227,6 +241,7 @@ def patchbomb(ui, repo, *revs, **opts): s.login(username, password) parent = None tz = time.strftime('%z') + sender_addr = parseaddr(sender)[1] for m in msgs: try: m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) @@ -241,36 +256,36 @@ def patchbomb(ui, repo, *revs, **opts): m['From'] = sender m['To'] = ', '.join(to) if cc: m['Cc'] = ', '.join(cc) - ui.status('Sending ', m['Subject'], ' ...\n') if opts['test']: + ui.status('Displaying ', m['Subject'], ' ...\n') fp = os.popen(os.getenv('PAGER', 'more'), 'w') fp.write(m.as_string(0)) fp.write('\n') fp.close() + elif opts['mbox']: + ui.status('Writing ', m['Subject'], ' ...\n') + fp = open(opts['mbox'], m.has_key('In-Reply-To') and 'ab+' or 'wb+') + date = time.asctime(time.localtime(start_time)) + fp.write('From %s %s\n' % (sender_addr, date)) + fp.write(m.as_string(0)) + fp.write('\n\n') + fp.close() else: + ui.status('Sending ', m['Subject'], ' ...\n') s.sendmail(sender, to + cc, m.as_string(0)) - if not opts['test']: + if not opts['test'] and not opts['mbox']: s.close() -if __name__ == '__main__': - optspec = [('c', 'cc', [], 'email addresses of copy recipients'), - ('d', 'diffstat', None, 'add diffstat output to messages'), - ('f', 'from', '', 'email address of sender'), - ('', 'plain', None, 'omit hg patch header'), - ('n', 'test', None, 'print messages that would be sent'), - ('s', 'subject', '', 'subject of introductory message'), - ('t', 'to', [], 'email addresses of recipients')] - options = {} - try: - args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts + optspec, - options) - except fancyopts.getopt.GetoptError, inst: - u = ui.ui() - u.warn('error: %s' % inst) - sys.exit(1) - - u = ui.ui(options["verbose"], options["debug"], options["quiet"], - not options["noninteractive"]) - repo = hg.repository(ui = u) - - patchbomb(u, repo, *args, **options) +cmdtable = { + 'email': + (patchbomb, + [('c', 'cc', [], 'email addresses of copy recipients'), + ('d', 'diffstat', None, 'add diffstat output to messages'), + ('f', 'from', '', 'email address of sender'), + ('', 'plain', None, 'omit hg patch header'), + ('n', 'test', None, 'print messages that would be sent'), + ('m', 'mbox', '', 'write messages to mbox file instead of sending them'), + ('s', 'subject', '', 'subject of introductory message'), + ('t', 'to', [], 'email addresses of recipients')], + "hg email [OPTION]... [REV]...") + } diff --git a/hgmerge b/hgmerge --- a/hgmerge +++ b/hgmerge @@ -17,127 +17,151 @@ fi # find decent versions of our utilities, insisting on the GNU versions where we # need to +MERGE=merge DIFF3=gdiff3 DIFF=gdiff PATCH=gpatch +type $MERGE >/dev/null 2>&1 || MERGE= type $DIFF3 >/dev/null 2>&1 || DIFF3=diff3 type $DIFF >/dev/null 2>&1 || DIFF=diff -type $PATCH >/dev/null 2>&1 || PATCH=patch +type $PATCH >/dev/null 2>&1 || PATCH=patch $DIFF3 --version >/dev/null 2>&1 || DIFF3= -# Back up our file -cp "$LOCAL" "$LOCAL.orig" +# find optional visual utilities +FILEMERGE='/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge' +KDIFF3=kdiff3 +TKDIFF=tkdiff + +type $FILEMERGE >/dev/null 2>&1 || FILEMERGE= +type $KDIFF3 >/dev/null 2>&1 || KDIFF3= +type $TKDIFF >/dev/null 2>&1 || TKDIFF= + +# random part of names +RAND="$RANDOM.$RANDOM.$RANDOM.$$" + +# temporary directory for diff+patch merge +HGTMP="${TMPDIR-/tmp}/hgmerge.$RAND" + +# backup file +BACKUP="$LOCAL.orig.$RAND" + +# file used to test for file change +CHGTEST="$LOCAL.chg.$RAND" + +# put all your required cleanup here +cleanup() { + rm -f "$BACKUP" "$CHGTEST" + rm -rf "$HGTMP" +} + +# functions concerning program exit +success() { + cleanup + exit 0 +} + +failure() { + echo "merge failed" 1>&2 + mv "$BACKUP" "$LOCAL" + cleanup + exit 1 +} + +# Clean up when interrupted +trap "failure" 1 2 3 6 15 # HUP INT QUIT ABRT TERM + +# Back up our file (and try hard to keep the mtime unchanged) +mv "$LOCAL" "$BACKUP" +cp "$BACKUP" "$LOCAL" # Attempt to do a non-interactive merge -if type merge > /dev/null 2>&1; then - merge "$LOCAL" "$BASE" "$OTHER" 2> /dev/null && exit 0 - cp "$LOCAL.orig" "$LOCAL" +if [ -n "$MERGE" ]; then + $MERGE "$LOCAL" "$BASE" "$OTHER" 2> /dev/null && success + cp "$BACKUP" "$LOCAL" elif [ -n "$DIFF3" ]; then - echo $DIFF3 -m "$LOCAL.orig" "$BASE" "$OTHER" - $DIFF3 -m "$LOCAL.orig" "$BASE" "$OTHER" > "$LOCAL" && exit 0 + echo $DIFF3 -m "$BACKUP" "$BASE" "$OTHER" + $DIFF3 -m "$BACKUP" "$BASE" "$OTHER" > "$LOCAL" && success if [ $? -eq 2 ]; then echo "$DIFF3 failed! Exiting." 1>&2 - cp "$LOCAL.orig" "$LOCAL" - exit 1 + cp "$BACKUP" "$LOCAL" + failure fi - cp "$LOCAL.orig" "$LOCAL" + cp "$BACKUP" "$LOCAL" fi # on MacOS X try FileMerge.app, shipped with Apple's developer tools -# TODO: make proper temp files. foo.orig and foo.link are dangerous -FILEMERGE='/Developer/Applications/Utilities/FileMerge.app/Contents/MacOS/FileMerge' -if type "$FILEMERGE" > /dev/null 2>&1; then - cp "$LOCAL.orig" "$LOCAL" - ln "$LOCAL" "$LOCAL.link" +if [ -n "$FILEMERGE" ]; then + cp "$BACKUP" "$LOCAL" + cp "$BACKUP" "$CHGTEST" # filemerge prefers the right by default - if ! "$FILEMERGE" -left "$OTHER" -right "$LOCAL" -ancestor "$BASE" -merge "$LOCAL" + $FILEMERGE -left "$OTHER" -right "$LOCAL" -ancestor "$BASE" -merge "$LOCAL" + [ $? -ne 0 ] && echo "FileMerge failed to launch" && failure + if test "$LOCAL" -nt "$CHGTEST" then - echo "FileMerge failed to launch" - exit 1 - fi - if ! test "$LOCAL" -ef "$LOCAL.link" - then - rm "$LOCAL.orig" "$LOCAL.link" - exit 0 + success else - rm "$LOCAL.link" - echo "$LOCAL is unchanged. Was the merge successful?" + echo "$LOCAL seems unchanged. Was the merge successful?" select answer in yes no do - if test "$answer" == "yes" - then - rm "$LOCAL.orig" - exit 0 - else - exit 1 - fi + test "$answer" == "yes" && success || failure done - exit 1 fi + failure fi if [ -n "$DISPLAY" ]; then # try using kdiff3, which is fairly nice - if type kdiff3 > /dev/null 2>&1; then - kdiff3 --auto "$BASE" "$LOCAL" "$OTHER" -o "$LOCAL" || exit 1 - exit 0 + if [ -n "$KDIFF3" ]; then + $KDIFF3 --auto "$BASE" "$LOCAL" "$OTHER" -o "$LOCAL" || failure + success fi # try using tkdiff, which is a bit less sophisticated - if type tkdiff > /dev/null 2>&1; then - tkdiff "$LOCAL" "$OTHER" -a "$BASE" -o "$LOCAL" || exit 1 - exit 0 + if [ -n "$TKDIFF" ]; then + $TKDIFF "$LOCAL" "$OTHER" -a "$BASE" -o "$LOCAL" || failure + success fi fi # Attempt to do a merge with $EDITOR -if type merge > /dev/null 2>&1; then +if [ -n "$MERGE" ]; then echo "conflicts detected in $LOCAL" - merge "$LOCAL" "$BASE" "$OTHER" 2>/dev/null || $EDITOR "$LOCAL" - exit 0 + $MERGE "$LOCAL" "$BASE" "$OTHER" 2>/dev/null || $EDITOR "$LOCAL" + success fi if [ -n "$DIFF3" ]; then echo "conflicts detected in $LOCAL" - $DIFF3 -m "$LOCAL.orig" "$BASE" "$OTHER" > "$LOCAL" || { + $DIFF3 -m "$BACKUP" "$BASE" "$OTHER" > "$LOCAL" || { case $? in 1) $EDITOR "$LOCAL" ;; 2) echo "$DIFF3 failed! Exiting." 1>&2 - cp "$LOCAL.orig" "$LOCAL" - exit 1 ;; + cp "$BACKUP" "$LOCAL" + failure ;; esac - exit 0 + success } fi -HGTMP="" -cleanup_exit() { - rm -rf "$HGTMP" -} - # attempt to manually merge with diff and patch if [ -n "$DIFF" -a -n "$PATCH" ]; then - # Remove temporary files even if we get interrupted - trap "cleanup_exit" 0 # normal exit - trap "exit 1" 1 2 3 6 15 # HUP INT QUIT ABRT TERM - HGTMP="${TMPDIR-/tmp}/hgmerge.$RANDOM.$RANDOM.$RANDOM.$$" (umask 077 && mkdir "$HGTMP") || { - echo "Could not create temporary directory! Exiting." 1>&2 - exit 1 + echo "Could not create temporary directory $HGTMP" 1>&2 + failure } $DIFF -u "$BASE" "$OTHER" > "$HGTMP/diff" || : if $PATCH "$LOCAL" < "$HGTMP/diff"; then - exit 0 + success else # If rejects are empty after using the editor, merge was ok - $EDITOR "$LOCAL" "$LOCAL.rej" && test -s "$LOCAL.rej" || exit 0 + $EDITOR "$LOCAL" "$LOCAL.rej" && test -s "$LOCAL.rej" || success fi - exit 1 + failure fi echo "hgmerge: unable to find merge, tkdiff, kdiff3, or diff+patch!" -exit 1 +failure diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -115,8 +115,8 @@ def walkchangerevs(ui, repo, pats, opts) yield rev minrev, maxrev = min(revs), max(revs) - for file in files: - filelog = repo.file(file) + for file_ in files: + filelog = repo.file(file_) # A zero count may be a directory or deleted file, so # try to find matching entries on the slow path. if filelog.count() == 0: @@ -127,7 +127,7 @@ def walkchangerevs(ui, repo, pats, opts) if rev < minrev: break fncache.setdefault(rev, []) - fncache[rev].append(file) + fncache[rev].append(file_) wanted[rev] = 1 if slowpath: # The slow path checks files modified in every changeset. @@ -261,7 +261,7 @@ def make_file(repo, r, pat, node=None, mode) def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always, - changes=None, text=False): + changes=None, text=False, opts={}): if not changes: changes = repo.changes(node1, node2, files, match=match) modified, added, removed, deleted, unknown = changes @@ -296,8 +296,8 @@ def dodiff(fp, ui, repo, node1, node2, f date1 = util.datestr(change[2]) diffopts = ui.diffopts() - showfunc = diffopts['showfunc'] - ignorews = diffopts['ignorews'] + showfunc = opts.get('show_function') or diffopts['showfunc'] + ignorews = opts.get('ignore_all_space') or diffopts['ignorews'] for f in modified: to = None if f in mmap: @@ -405,6 +405,8 @@ def help_(ui, cmd=None, with_version=Fal # description doc = i[0].__doc__ + if not doc: + doc = _("(No help text available)") if ui.quiet: doc = doc.splitlines(0)[0] ui.write("%s\n" % doc.rstrip()) @@ -445,10 +447,10 @@ def help_(ui, cmd=None, with_version=Fal f = f.lstrip("^") if not ui.debugflag and f.startswith("debug"): continue - d = "" - if e[0].__doc__: - d = e[0].__doc__.splitlines(0)[0].rstrip() - h[f] = d + doc = e[0].__doc__ + if not doc: + doc = _("(No help text available)") + h[f] = doc.splitlines(0)[0].rstrip() cmds[f] = c.lstrip("^") fns = h.keys() @@ -516,6 +518,9 @@ def addremove(ui, repo, *pats, **opts): New files are ignored if they match any of the patterns in .hgignore. As with add, these changes take effect at the next commit. """ + return addremove_lock(ui, repo, pats, opts) + +def addremove_lock(ui, repo, pats, opts, wlock=None): add, remove = [], [] for src, abs, rel, exact in walk(repo, pats, opts): if src == 'f' and repo.dirstate.state(abs) == '?': @@ -526,8 +531,8 @@ def addremove(ui, repo, *pats, **opts): remove.append(abs) if ui.verbose or not exact: ui.status(_('removing %s\n') % ((pats and rel) or abs)) - repo.add(add) - repo.remove(remove) + repo.add(add, wlock=wlock) + repo.remove(remove, wlock=wlock) def annotate(ui, repo, *pats, **opts): """show changeset information per file line @@ -616,7 +621,7 @@ def bundle(ui, repo, fname, dest="defaul dest = ui.expandpath(dest, repo.root) other = hg.repository(ui, dest) o = repo.findoutgoing(other) - cg = repo.changegroup(o) + cg = repo.changegroup(o, 'bundle') try: f.write("HG10") @@ -719,8 +724,8 @@ def clone(ui, source, dest=None, **opts) # can end up with extra data in the cloned revlogs that's # not pointed to by changesets, thus causing verify to # fail - l1 = lock.lock(os.path.join(source, ".hg", "lock")) - except OSError: + l1 = other.lock() + except lock.LockException: copy = False if copy: @@ -812,14 +817,19 @@ def docopy(ui, repo, pats, opts): reasons = {'?': _('is not managed'), 'a': _('has been marked for add'), 'r': _('has been marked for remove')} - reason = reasons.get(repo.dirstate.state(abs)) + state = repo.dirstate.state(abs) + reason = reasons.get(state) if reason: + if state == 'a': + origsrc = repo.dirstate.copied(abs) + if origsrc is not None: + return origsrc if exact: ui.warn(_('%s: not copying - file %s\n') % (rel, reason)) else: - return True - - def copy(abssrc, relsrc, target, exact): + return abs + + def copy(origsrc, abssrc, relsrc, target, exact): abstarget = util.canonpath(repo.root, cwd, target) reltarget = util.pathto(cwd, abstarget) prevsrc = targets.get(abstarget) @@ -858,7 +868,7 @@ def docopy(ui, repo, pats, opts): if ui.verbose or not exact: ui.status(_('copying %s to %s\n') % (relsrc, reltarget)) targets[abstarget] = abssrc - repo.copy(abssrc, abstarget) + repo.copy(origsrc, abstarget) copied.append((abssrc, relsrc, exact)) def targetpathfn(pat, dest, srcs): @@ -932,8 +942,9 @@ def docopy(ui, repo, pats, opts): for pat in pats: srcs = [] for tag, abssrc, relsrc, exact in walk(repo, [pat], opts): - if okaytocopy(abssrc, relsrc, exact): - srcs.append((abssrc, relsrc, exact)) + origsrc = okaytocopy(abssrc, relsrc, exact) + if origsrc: + srcs.append((origsrc, abssrc, relsrc, exact)) if not srcs: continue copylist.append((tfn(pat, dest, srcs), srcs)) @@ -941,8 +952,8 @@ def docopy(ui, repo, pats, opts): raise util.Abort(_('no files to copy')) for targetpath, srcs in copylist: - for abssrc, relsrc, exact in srcs: - copy(abssrc, relsrc, targetpath(abssrc), exact) + for origsrc, abssrc, relsrc, exact in srcs: + copy(origsrc, abssrc, relsrc, targetpath(abssrc), exact) if errors: ui.warn(_('(consider using --after)\n')) @@ -974,6 +985,18 @@ def debugancestor(ui, index, rev1, rev2) a = r.ancestor(r.lookup(rev1), r.lookup(rev2)) ui.write("%d:%s\n" % (r.rev(a), hex(a))) +def debugrebuildstate(ui, repo, rev=None): + """rebuild the dirstate as it would look like for the given revision""" + if not rev: + rev = repo.changelog.tip() + else: + rev = repo.lookup(rev) + change = repo.changelog.read(rev) + n = change[0] + files = repo.manifest.readflags(n) + wlock = repo.wlock() + repo.dirstate.rebuild(rev, files.iteritems()) + def debugcheckstate(ui, repo): """validate the correctness of the current dirstate""" parent1, parent2 = repo.dirstate.parents() @@ -1134,7 +1157,7 @@ def diff(ui, repo, *pats, **opts): fns, matchfn, anypats = matchpats(repo, pats, opts) dodiff(sys.stdout, ui, repo, node1, node2, fns, match=matchfn, - text=opts['text']) + text=opts['text'], opts=opts) def doexport(ui, repo, changeset, seqno, total, revwidth, opts): node = repo.lookup(changeset) @@ -1278,6 +1301,7 @@ def grep(ui, repo, pattern, *pats, **opt s = linestate(line, lnum, cstart, cend) m[s] = s + # FIXME: prev isn't used, why ? prev = {} ucache = {} def display(fn, rev, states, prevstates): @@ -1587,7 +1611,19 @@ def log(ui, repo, *pats, **opts): self.write(*args) def __getattr__(self, key): return getattr(self.ui, key) + changeiter, getchange, matchfn = walkchangerevs(ui, repo, pats, opts) + + if opts['limit']: + try: + limit = int(opts['limit']) + except ValueError: + raise util.Abort(_('limit must be a positive integer')) + if limit <= 0: raise util.Abort(_('limit must be positive')) + else: + limit = sys.maxint + count = 0 + for st, rev, fns in changeiter: if st == 'window': du = dui(ui) @@ -1601,7 +1637,6 @@ def log(ui, repo, *pats, **opts): if opts['only_merges'] and len(parents) != 2: continue - br = None if opts['keyword']: changes = getchange(rev) miss = 0 @@ -1614,7 +1649,8 @@ def log(ui, repo, *pats, **opts): if miss: continue - if opts['branch']: + br = None + if opts['branches']: br = repo.branchlookup([repo.changelog.node(rev)]) show_changeset(du, repo, rev, brinfo=br) @@ -1623,8 +1659,11 @@ def log(ui, repo, *pats, **opts): dodiff(du, du, repo, prev, changenode, match=matchfn) du.write("\n\n") elif st == 'iter': - for args in du.hunk[rev]: - ui.write(*args) + if count == limit: break + if du.hunk[rev]: + count += 1 + for args in du.hunk[rev]: + ui.write(*args) def manifest(ui, repo, rev=None): """output the latest or given revision of the project manifest @@ -1675,7 +1714,7 @@ def outgoing(ui, repo, dest="default-pus dodiff(ui, ui, repo, prev, n) ui.write("\n") -def parents(ui, repo, rev=None): +def parents(ui, repo, rev=None, branches=None): """show the parents of the working dir or revision Print the working directory's parent revisions. @@ -1685,9 +1724,12 @@ def parents(ui, repo, rev=None): else: p = repo.dirstate.parents() + br = None + if branches is not None: + br = repo.branchlookup(p) for n in p: if n != nullid: - show_changeset(ui, repo, changenode=n) + show_changeset(ui, repo, changenode=n, brinfo=br) def paths(ui, search=None): """show definition of symbolic path names @@ -1990,7 +2032,7 @@ def serve(ui, repo, **opts): arg, roots = getarg() nodes = map(bin, roots.split(" ")) - cg = repo.changegroup(nodes) + cg = repo.changegroup(nodes, 'serve') while 1: d = cg.read(4096) if not d: @@ -2013,6 +2055,16 @@ def serve(ui, repo, **opts): if opts[o]: ui.setconfig("web", o, opts[o]) + if opts['daemon'] and not opts['daemon_pipefds']: + rfd, wfd = os.pipe() + args = sys.argv[:] + args.append('--daemon-pipefds=%d,%d' % (rfd, wfd)) + pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0), + args[0], args) + os.close(wfd) + os.read(rfd, 1) + os._exit(0) + try: httpd = hgweb.create_server(repo) except socket.error, inst: @@ -2031,6 +2083,25 @@ def serve(ui, repo, **opts): ui.status(_('listening at http://%s:%d/\n') % (addr, port)) else: ui.status(_('listening at http://%s/\n') % addr) + + if opts['pid_file']: + fp = open(opts['pid_file'], 'w') + fp.write(str(os.getpid())) + fp.close() + + if opts['daemon_pipefds']: + rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')] + os.close(rfd) + os.write(wfd, 'y') + os.close(wfd) + sys.stdout.flush() + sys.stderr.flush() + fd = os.open(util.nulldev, os.O_RDWR) + if fd != 0: os.dup2(fd, 0) + if fd != 1: os.dup2(fd, 1) + if fd != 2: os.dup2(fd, 2) + if fd not in (0, 1, 2): os.close(fd) + httpd.serve_forever() def status(ui, repo, *pats, **opts): @@ -2107,8 +2178,12 @@ def tag(ui, repo, name, rev_=None, **opt if name.find(c) >= 0: raise util.Abort(_("%s cannot be used in a tag name") % repr(c)) + repo.hook('pretag', throw=True, node=r, tag=name, + local=int(not not opts['local'])) + if opts['local']: repo.opener("localtags", "a").write("%s %s\n" % (r, name)) + repo.hook('tag', node=r, tag=name, local=1) return for x in repo.changes(): @@ -2124,6 +2199,7 @@ def tag(ui, repo, name, rev_=None, **opt _("Added tag %s for changeset %s") % (name, r)) try: repo.commit([".hgtags"], message, opts['user'], opts['date']) + repo.hook('tag', node=r, tag=name, local=0) except ValueError, inst: raise util.Abort(str(inst)) @@ -2144,13 +2220,18 @@ def tags(ui, repo): r = " ?:?" ui.write("%-30s %s\n" % (t, r)) -def tip(ui, repo): +def tip(ui, repo, **opts): """show the tip revision Show the tip revision. """ n = repo.changelog.tip() - show_changeset(ui, repo, changenode=n) + br = None + if opts['branches']: + br = repo.branchlookup([n]) + show_changeset(ui, repo, changenode=n, brinfo=br) + if opts['patch']: + dodiff(ui, ui, repo, repo.changelog.parents(n)[0], n) def unbundle(ui, repo, fname, **opts): """apply a changegroup file @@ -2308,6 +2389,10 @@ table = { _('forcibly copy over an existing managed file'))], _('hg copy [OPTION]... [SOURCE]... DEST')), "debugancestor": (debugancestor, [], _('debugancestor INDEX REV1 REV2')), + "debugrebuildstate": + (debugrebuildstate, + [('r', 'rev', "", _("revision to rebuild to"))], + _('debugrebuildstate [-r REV] [REV]')), "debugcheckstate": (debugcheckstate, [], _('debugcheckstate')), "debugconfig": (debugconfig, [], _('debugconfig')), "debugsetparents": (debugsetparents, [], _('debugsetparents REV1 [REV2]')), @@ -2326,7 +2411,12 @@ table = { [('r', 'rev', [], _('revision')), ('a', 'text', None, _('treat all files as text')), ('I', 'include', [], _('include names matching the given patterns')), - ('X', 'exclude', [], _('exclude names matching the given patterns'))], + ('p', 'show-function', None, + _('show which function each change is in')), + ('w', 'ignore-all-space', None, + _('ignore white space when comparing lines')), + ('X', 'exclude', [], + _('exclude names matching the given patterns'))], _('hg diff [-a] [-I] [-X] [-r REV1 [-r REV2]] [FILE]...')), "^export": (export, @@ -2354,7 +2444,7 @@ table = { _('hg grep [OPTION]... PATTERN [FILE]...')), "heads": (heads, - [('b', 'branches', None, _('find branch info')), + [('b', 'branches', None, _('show branches')), ('r', 'rev', '', _('show only heads which are descendants of rev'))], _('hg heads [-b] [-r ]')), "help": (help_, [], _('hg help [COMMAND]')), @@ -2388,8 +2478,9 @@ table = { (log, [('I', 'include', [], _('include names matching the given patterns')), ('X', 'exclude', [], _('exclude names matching the given patterns')), - ('b', 'branch', None, _('show branches')), + ('b', 'branches', None, _('show branches')), ('k', 'keyword', [], _('search for a keyword')), + ('l', 'limit', '', _('limit number of changes displayed')), ('r', 'rev', [], _('show the specified revision or range')), ('M', 'no-merges', None, _('do not show merges')), ('m', 'only-merges', None, _('show only merges')), @@ -2401,7 +2492,10 @@ table = { ('p', 'patch', None, _('show patch')), ('n', 'newest-first', None, _('show newest record first'))], _('hg outgoing [-p] [-n] [-M] [DEST]')), - "^parents": (parents, [], _('hg parents [REV]')), + "^parents": + (parents, + [('b', 'branches', None, _('show branches'))], + _('hg parents [-b] [REV]')), "paths": (paths, [], _('hg paths [NAME]')), "^pull": (pull, @@ -2452,11 +2546,14 @@ table = { "^serve": (serve, [('A', 'accesslog', '', _('name of access log file to write to')), + ('d', 'daemon', None, _('run server in background')), + ('', 'daemon-pipefds', '', _('used internally by daemon mode')), ('E', 'errorlog', '', _('name of error log file to write to')), ('p', 'port', 0, _('port to use (default: 8000)')), ('a', 'address', '', _('address to use')), ('n', 'name', '', _('name to show in web pages (default: working dir)')), + ('', 'pid-file', '', _('name of file to write process ID to')), ('', 'stdio', None, _('for remote clients')), ('t', 'templates', '', _('web templates to use')), ('', 'style', '', _('template style to use')), @@ -2484,7 +2581,11 @@ table = { ('r', 'rev', '', _('revision to tag'))], _('hg tag [-r REV] [OPTION]... NAME')), "tags": (tags, [], _('hg tags')), - "tip": (tip, [], _('hg tip')), + "tip": + (tip, + [('b', 'branches', None, _('show branches')), + ('p', 'patch', None, _('show patch'))], + _('hg [-b] [-p] tip')), "unbundle": (unbundle, [('u', 'update', None, @@ -2524,17 +2625,20 @@ norepo = ("clone init version help debug def find(cmd): """Return (aliases, command table entry) for command string.""" choice = None + count = 0 for e in table.keys(): aliases = e.lstrip("^").split("|") if cmd in aliases: return aliases, table[e] for a in aliases: if a.startswith(cmd): - if choice: - raise AmbiguousCommand(cmd) - else: - choice = aliases, table[e] - break + count += 1 + choice = aliases, table[e] + break + + if count > 1: + raise AmbiguousCommand(cmd) + if choice: return choice diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -197,6 +197,19 @@ class dirstate(object): def clear(self): self.map = {} + self.copies = {} + self.markdirty() + + def rebuild(self, parent, files): + self.clear() + umask = os.umask(0) + os.umask(umask) + for f, mode in files: + if mode: + self.map[f] = ('n', ~umask, -1, 0) + else: + self.map[f] = ('n', ~umask & 0666, -1, 0) + self.pl = (parent, nullid) self.markdirty() def write(self): @@ -270,11 +283,11 @@ class dirstate(object): elif not dc: dc = self.filterfiles(files) - def statmatch(file, stat): - file = util.pconvert(file) - if file not in dc and self.ignore(file): + def statmatch(file_, stat): + file_ = util.pconvert(file_) + if file_ not in dc and self.ignore(file_): return False - return match(file) + return match(file_) return self.walkhelper(files=files, statmatch=statmatch, dc=dc) @@ -350,9 +363,9 @@ class dirstate(object): continue if stat.S_ISDIR(st.st_mode): cmp1 = (lambda x, y: cmp(x[1], y[1])) - sorted = [ x for x in findfiles(f) ] - sorted.sort(cmp1) - for e in sorted: + sorted_ = [ x for x in findfiles(f) ] + sorted_.sort(cmp1) + for e in sorted_: yield e else: ff = util.normpath(ff) @@ -380,7 +393,7 @@ class dirstate(object): for src, fn, st in self.statwalk(files, match): try: - type, mode, size, time = self[fn] + type_, mode, size, time = self[fn] except KeyError: unknown.append(fn) continue @@ -399,22 +412,23 @@ class dirstate(object): nonexistent = False # XXX: what to do with file no longer present in the fs # who are not removed in the dirstate ? - if nonexistent and type in "nm": + if nonexistent and type_ in "nm": deleted.append(fn) continue # check the common case first - if type == 'n': + if type_ == 'n': if not st: st = os.stat(fn) - if size != st.st_size or (mode ^ st.st_mode) & 0100: + if size >= 0 and (size != st.st_size + or (mode ^ st.st_mode) & 0100): modified.append(fn) elif time != st.st_mtime: lookup.append(fn) - elif type == 'm': + elif type_ == 'm': modified.append(fn) - elif type == 'a': + elif type_ == 'a': added.append(fn) - elif type == 'r': + elif type_ == 'r': removed.append(fn) return (lookup, modified, added, removed, deleted, unknown) diff --git a/mercurial/hgweb.py b/mercurial/hgweb.py --- a/mercurial/hgweb.py +++ b/mercurial/hgweb.py @@ -298,19 +298,25 @@ class hgweb(object): def changelog(self, pos): def changenav(**map): - def seq(factor=1): - yield 1 * factor - yield 3 * factor - #yield 5 * factor + def seq(factor, maxchanges=None): + if maxchanges: + yield maxchanges + if maxchanges >= 20 and maxchanges <= 40: + yield 50 + else: + yield 1 * factor + yield 3 * factor for f in seq(factor * 10): yield f l = [] - for f in seq(): - if f < self.maxchanges / 2: + last = 0 + for f in seq(1, self.maxchanges): + if f < self.maxchanges or f <= last: continue if f > count: break + last = f r = "%d" % f if pos + f < count: l.append(("+" + r, pos + f)) @@ -958,7 +964,7 @@ class hgweb(object): nodes = map(bin, req.form['roots'][0].split(" ")) z = zlib.compressobj() - f = self.repo.changegroup(nodes) + f = self.repo.changegroup(nodes, 'serve') while 1: chunk = f.read(4096) if not chunk: diff --git a/mercurial/httprepo.py b/mercurial/httprepo.py --- a/mercurial/httprepo.py +++ b/mercurial/httprepo.py @@ -119,7 +119,7 @@ class httprepository(remoterepository): self.ui.warn(_("unexpected response:\n") + d[:400] + "\n...\n") raise - def changegroup(self, nodes): + def changegroup(self, nodes, kind): n = " ".join(map(hex, nodes)) f = self.do_cmd("changegroup", roots=n) bytes = 0 diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -48,30 +48,36 @@ class localrepository(object): except IOError: pass - def hook(self, name, **args): + def hook(self, name, throw=False, **args): def runhook(name, cmd): self.ui.note(_("running hook %s: %s\n") % (name, cmd)) old = {} for k, v in args.items(): k = k.upper() + old['HG_' + k] = os.environ.get(k, None) old[k] = os.environ.get(k, None) - os.environ[k] = v + os.environ['HG_' + k] = str(v) + os.environ[k] = str(v) - # Hooks run in the repository root - olddir = os.getcwd() - os.chdir(self.root) - r = os.system(cmd) - os.chdir(olddir) + try: + # Hooks run in the repository root + olddir = os.getcwd() + os.chdir(self.root) + r = os.system(cmd) + finally: + for k, v in old.items(): + if v is not None: + os.environ[k] = v + else: + del os.environ[k] - for k, v in old.items(): - if v != None: - os.environ[k] = v - else: - del os.environ[k] + os.chdir(olddir) if r: - self.ui.warn(_("abort: %s hook failed with status %d!\n") % - (name, r)) + desc, r = util.explain_exit(r) + if throw: + raise util.Abort(_('%s hook %s') % (name, desc)) + self.ui.warn(_('error: %s hook %s\n') % (name, desc)) return False return True @@ -225,7 +231,7 @@ class localrepository(object): self.join("journal"), after) def recover(self): - lock = self.lock() + l = self.lock() if os.path.exists(self.join("journal")): self.ui.status(_("rolling back interrupted transaction\n")) transaction.rollback(self.opener, self.join("journal")) @@ -236,9 +242,10 @@ class localrepository(object): self.ui.warn(_("no interrupted transaction available\n")) return False - def undo(self): - wlock = self.wlock() - lock = self.lock() + def undo(self, wlock=None): + if not wlock: + wlock = self.wlock() + l = self.lock() if os.path.exists(self.join("undo")): self.ui.status(_("rolling back last transaction\n")) transaction.rollback(self.opener, self.join("undo")) @@ -247,27 +254,46 @@ class localrepository(object): else: self.ui.warn(_("no undo information available\n")) - def lock(self, wait=1): + def do_lock(self, lockname, wait, releasefn=None, acquirefn=None): try: - return lock.lock(self.join("lock"), 0) - except lock.LockHeld, inst: - if wait: - self.ui.warn(_("waiting for lock held by %s\n") % inst.args[0]) - return lock.lock(self.join("lock"), wait) - raise inst - - def wlock(self, wait=1): - try: - wlock = lock.lock(self.join("wlock"), 0, self.dirstate.write) + l = lock.lock(self.join(lockname), 0, releasefn) except lock.LockHeld, inst: if not wait: raise inst self.ui.warn(_("waiting for lock held by %s\n") % inst.args[0]) - wlock = lock.lock(self.join("wlock"), wait, self.dirstate.write) - self.dirstate.read() - return wlock + l = lock.lock(self.join(lockname), wait, releasefn) + if acquirefn: + acquirefn() + return l + + def lock(self, wait=1): + return self.do_lock("lock", wait) + + def wlock(self, wait=1): + return self.do_lock("wlock", wait, + self.dirstate.write, + self.dirstate.read) - def rawcommit(self, files, text, user, date, p1=None, p2=None): + def checkfilemerge(self, filename, text, filelog, manifest1, manifest2): + "determine whether a new filenode is needed" + fp1 = manifest1.get(filename, nullid) + fp2 = manifest2.get(filename, nullid) + + if fp2 != nullid: + # is one parent an ancestor of the other? + fpa = filelog.ancestor(fp1, fp2) + if fpa == fp1: + fp1, fp2 = fp2, nullid + elif fpa == fp2: + fp2 = nullid + + # is the file unmodified from the parent? report existing entry + if fp2 == nullid and text == filelog.read(fp1): + return (fp1, None, None) + + return (None, fp1, fp2) + + def rawcommit(self, files, text, user, date, p1=None, p2=None, wlock=None): orig_parent = self.dirstate.parents()[0] or nullid p1 = p1 or self.dirstate.parents()[0] or nullid p2 = p2 or self.dirstate.parents()[1] or nullid @@ -283,8 +309,9 @@ class localrepository(object): else: update_dirstate = 0 - wlock = self.wlock() - lock = self.lock() + if not wlock: + wlock = self.wlock() + l = self.lock() tr = self.transaction() mm = m1.copy() mfm = mf1.copy() @@ -296,27 +323,10 @@ class localrepository(object): r = self.file(f) mfm[f] = tm - fp1 = m1.get(f, nullid) - fp2 = m2.get(f, nullid) - - # is the same revision on two branches of a merge? - if fp2 == fp1: - fp2 = nullid - - if fp2 != nullid: - # is one parent an ancestor of the other? - fpa = r.ancestor(fp1, fp2) - if fpa == fp1: - fp1, fp2 = fp2, nullid - elif fpa == fp2: - fp2 = nullid - - # is the file unmodified from the parent? - if t == r.read(fp1): - # record the proper existing parent in manifest - # no need to add a revision - mm[f] = fp1 - continue + (entry, fp1, fp2) = self.checkfilemerge(f, t, r, m1, m2) + if entry: + mm[f] = entry + continue mm[f] = r.add(t, {}, tr, linkrev, fp1, fp2) changed.append(f) @@ -340,7 +350,7 @@ class localrepository(object): self.dirstate.setparents(n, nullid) def commit(self, files=None, text="", user=None, date=None, - match=util.always, force=False): + match=util.always, force=False, wlock=None): commit = [] remove = [] changed = [] @@ -370,11 +380,15 @@ class localrepository(object): self.ui.status(_("nothing changed\n")) return None - if not self.hook("precommit"): - return None + xp1 = hex(p1) + if p2 == nullid: xp2 = '' + else: xp2 = hex(p2) - wlock = self.wlock() - lock = self.lock() + self.hook("precommit", throw=True, parent1=xp1, parent2=xp2) + + if not wlock: + wlock = self.wlock() + l = self.lock() tr = self.transaction() # check in files @@ -400,22 +414,9 @@ class localrepository(object): self.ui.debug(_(" %s: copy %s:%s\n") % (f, cp, meta["copyrev"])) fp1, fp2 = nullid, nullid else: - fp1 = m1.get(f, nullid) - fp2 = m2.get(f, nullid) - - if fp2 != nullid: - # is one parent an ancestor of the other? - fpa = r.ancestor(fp1, fp2) - if fpa == fp1: - fp1, fp2 = fp2, nullid - elif fpa == fp2: - fp2 = nullid - - # is the file unmodified from the parent? - if not meta and t == r.read(fp1) and fp2 == nullid: - # record the proper existing parent in manifest - # no need to add a revision - new[f] = fp1 + entry, fp1, fp2 = self.checkfilemerge(f, t, r, m1, m2) + if entry: + new[f] = entry continue new[f] = r.add(t, meta, tr, linkrev, fp1, fp2) @@ -437,29 +438,34 @@ class localrepository(object): new.sort() if not text: - edittext = "" + edittext = [""] if p2 != nullid: - edittext += "HG: branch merge\n" - edittext += "\n" + "HG: manifest hash %s\n" % hex(mn) - edittext += "".join(["HG: changed %s\n" % f for f in changed]) - edittext += "".join(["HG: removed %s\n" % f for f in remove]) + edittext.append("HG: branch merge") + edittext.extend(["HG: changed %s" % f for f in changed]) + edittext.extend(["HG: removed %s" % f for f in remove]) if not changed and not remove: - edittext += "HG: no files changed\n" - edittext = self.ui.edit(edittext) + edittext.append("HG: no files changed") + edittext.append("") + # run editor in the repository root + olddir = os.getcwd() + os.chdir(self.root) + edittext = self.ui.edit("\n".join(edittext)) + os.chdir(olddir) if not edittext.rstrip(): return None text = edittext user = user or self.ui.username() n = self.changelog.add(mn, changed + remove, text, tr, p1, p2, user, date) + self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1, + parent2=xp2) tr.close() self.dirstate.setparents(n) self.dirstate.update(new, "n") self.dirstate.forget(remove) - if not self.hook("commit", node=hex(n)): - return None + self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2) return n def walk(self, node=None, files=[], match=util.always): @@ -476,7 +482,8 @@ class localrepository(object): for src, fn in self.dirstate.walk(files, match): yield src, fn - def changes(self, node1=None, node2=None, files=[], match=util.always): + def changes(self, node1=None, node2=None, files=[], match=util.always, + wlock=None): """return changes between two nodes or node and working directory If node1 is None, use the first dirstate parent instead. @@ -498,10 +505,11 @@ class localrepository(object): # are we comparing the working directory? if not node2: - try: - wlock = self.wlock(wait=0) - except lock.LockHeld: - wlock = None + if not wlock: + try: + wlock = self.wlock(wait=0) + except lock.LockException: + wlock = None lookup, modified, added, removed, deleted, unknown = ( self.dirstate.changes(files, match)) @@ -550,8 +558,9 @@ class localrepository(object): l.sort() return (modified, added, removed, deleted, unknown) - def add(self, list): - wlock = self.wlock() + def add(self, list, wlock=None): + if not wlock: + wlock = self.wlock() for f in list: p = self.wjoin(f) if not os.path.exists(p): @@ -564,15 +573,16 @@ class localrepository(object): else: self.dirstate.update([f], "a") - def forget(self, list): - wlock = self.wlock() + def forget(self, list, wlock=None): + if not wlock: + wlock = self.wlock() for f in list: if self.dirstate.state(f) not in 'ai': self.ui.warn(_("%s not added!\n") % f) else: self.dirstate.forget([f]) - def remove(self, list, unlink=False): + def remove(self, list, unlink=False, wlock=None): if unlink: for f in list: try: @@ -580,25 +590,26 @@ class localrepository(object): except OSError, inst: if inst.errno != errno.ENOENT: raise - wlock = self.wlock() + if not wlock: + wlock = self.wlock() for f in list: p = self.wjoin(f) if os.path.exists(p): self.ui.warn(_("%s still exists!\n") % f) elif self.dirstate.state(f) == 'a': - self.ui.warn(_("%s never committed!\n") % f) self.dirstate.forget([f]) elif f not in self.dirstate: self.ui.warn(_("%s not tracked!\n") % f) else: self.dirstate.update([f], "r") - def undelete(self, list): + def undelete(self, list, wlock=None): p = self.dirstate.parents()[0] mn = self.changelog.read(p)[0] mf = self.manifest.readflags(mn) m = self.manifest.read(mn) - wlock = self.wlock() + if not wlock: + wlock = self.wlock() for f in list: if self.dirstate.state(f) not in "r": self.ui.warn("%s not removed!\n" % f) @@ -608,14 +619,15 @@ class localrepository(object): util.set_exec(self.wjoin(f), mf[f]) self.dirstate.update([f], "n") - def copy(self, source, dest): + def copy(self, source, dest, wlock=None): p = self.wjoin(dest) if not os.path.exists(p): self.ui.warn(_("%s does not exist!\n") % dest) elif not os.path.isfile(p): self.ui.warn(_("copy failed: %s is not a file\n") % dest) else: - wlock = self.wlock() + if not wlock: + wlock = self.wlock() if self.dirstate.state(dest) == '?': self.dirstate.update([dest], "a") self.dirstate.copy(source, dest) @@ -919,7 +931,7 @@ class localrepository(object): return subset def pull(self, remote, heads=None): - lock = self.lock() + l = self.lock() # if we have an empty repo, fetch everything if self.changelog.tip() == nullid: @@ -933,13 +945,13 @@ class localrepository(object): return 1 if heads is None: - cg = remote.changegroup(fetch) + cg = remote.changegroup(fetch, 'pull') else: - cg = remote.changegroupsubset(fetch, heads) + cg = remote.changegroupsubset(fetch, heads, 'pull') return self.addchangegroup(cg) def push(self, remote, force=False): - lock = remote.lock() + l = remote.lock() base = {} heads = remote.heads() @@ -960,10 +972,10 @@ class localrepository(object): " use push -f to force)\n")) return 1 - cg = self.changegroup(update) + cg = self.changegroup(update, 'push') return remote.addchangegroup(cg) - def changegroupsubset(self, bases, heads): + def changegroupsubset(self, bases, heads, source): """This function generates a changegroup consisting of all the nodes that are descendents of any of the bases, and ancestors of any of the heads. @@ -975,6 +987,8 @@ class localrepository(object): Another wrinkle is doing the reverse, figuring out which changeset in the changegroup a particular filenode or manifestnode belongs to.""" + self.hook('preoutgoing', throw=True, source=source) + # Set up some initial variables # Make it easy to refer to self.changelog cl = self.changelog @@ -1227,14 +1241,19 @@ class localrepository(object): # Signal that no more groups are left. yield struct.pack(">l", 0) + self.hook('outgoing', node=hex(msng_cl_lst[0]), source=source) + return util.chunkbuffer(gengroup()) - def changegroup(self, basenodes): + def changegroup(self, basenodes, source): """Generate a changegroup of all nodes that we have that a recipient doesn't. This is much easier than the previous function as we can assume that the recipient has any changenode we aren't sending them.""" + + self.hook('preoutgoing', throw=True, source=source) + cl = self.changelog nodes = cl.nodesbetween(basenodes, None)[0] revset = dict.fromkeys([cl.rev(n) for n in nodes]) @@ -1286,6 +1305,7 @@ class localrepository(object): yield chnk yield struct.pack(">l", 0) + self.hook('outgoing', node=hex(nodes[0]), source=source) return util.chunkbuffer(gengroup()) @@ -1321,6 +1341,9 @@ class localrepository(object): if not source: return + + self.hook('prechangegroup', throw=True) + changesets = files = revisions = 0 tr = self.transaction() @@ -1363,21 +1386,19 @@ class localrepository(object): " with %d changes to %d files%s\n") % (changesets, revisions, files, heads)) + self.hook('pretxnchangegroup', throw=True, + node=hex(self.changelog.node(cor+1))) + tr.close() if changesets > 0: - if not self.hook("changegroup", - node=hex(self.changelog.node(cor+1))): - self.ui.warn(_("abort: changegroup hook returned failure!\n")) - return 1 + self.hook("changegroup", node=hex(self.changelog.node(cor+1))) for i in range(cor + 1, cnr + 1): - self.hook("commit", node=hex(self.changelog.node(i))) - - return + self.hook("incoming", node=hex(self.changelog.node(i))) def update(self, node, allow=False, force=False, choose=None, - moddirstate=True, forcemerge=False): + moddirstate=True, forcemerge=False, wlock=None): pl = self.dirstate.parents() if not force and pl[1] != nullid: self.ui.warn(_("aborting: outstanding uncommitted merges\n")) @@ -1399,6 +1420,13 @@ class localrepository(object): modified, added, removed, deleted, unknown = self.changes() + # is this a jump, or a merge? i.e. is there a linear path + # from p1 to p2? + linear_path = (pa == p1 or pa == p2) + + if allow and linear_path: + raise util.Abort(_("there is nothing to merge, " + "just use 'hg update'")) if allow and not forcemerge: if modified or added or removed: raise util.Abort(_("outstanding uncommited changes")) @@ -1411,10 +1439,6 @@ class localrepository(object): raise util.Abort(_("'%s' already exists in the working" " dir and differs from remote") % f) - # is this a jump, or a merge? i.e. is there a linear path - # from p1 to p2? - linear_path = (pa == p1 or pa == p2) - # resolve the manifest to determine which files # we care about merging self.ui.note(_("resolving manifests\n")) @@ -1436,7 +1460,7 @@ class localrepository(object): mw[f] = "" mfw[f] = util.is_exec(self.wjoin(f), mfw.get(f, False)) - if moddirstate: + if moddirstate and not wlock: wlock = self.wlock() for f in deleted + removed: diff --git a/mercurial/lock.py b/mercurial/lock.py --- a/mercurial/lock.py +++ b/mercurial/lock.py @@ -5,10 +5,14 @@ # This software may be used and distributed according to the terms # of the GNU General Public License, incorporated herein by reference. -import os, time +import errno, os, time import util -class LockHeld(Exception): +class LockException(Exception): + pass +class LockHeld(LockException): + pass +class LockUnavailable(LockException): pass class lock(object): @@ -38,8 +42,11 @@ class lock(object): try: util.makelock(str(pid), self.f) self.held = 1 - except (OSError, IOError): - raise LockHeld(util.readlock(self.f)) + except (OSError, IOError), why: + if why.errno == errno.EEXIST: + raise LockHeld(util.readlock(self.f)) + else: + raise LockUnavailable(why) def release(self): if self.held: diff --git a/mercurial/mdiff.py b/mercurial/mdiff.py --- a/mercurial/mdiff.py +++ b/mercurial/mdiff.py @@ -18,16 +18,22 @@ def unidiff(a, ad, b, bd, fn, r=None, te if not text and (util.binary(a) or util.binary(b)): l = ['Binary file %s has changed\n' % fn] - elif a == None: + elif not a: b = b.splitlines(1) - l1 = "--- %s\t%s\n" % ("/dev/null", epoch) + if a is None: + l1 = "--- %s\t%s\n" % ("/dev/null", epoch) + else: + l1 = "--- %s\t%s\n" % ("a/" + fn, ad) l2 = "+++ %s\t%s\n" % ("b/" + fn, bd) l3 = "@@ -0,0 +1,%d @@\n" % len(b) l = [l1, l2, l3] + ["+" + e for e in b] - elif b == None: + elif not b: a = a.splitlines(1) l1 = "--- %s\t%s\n" % ("a/" + fn, ad) - l2 = "+++ %s\t%s\n" % ("/dev/null", epoch) + if b is None: + l2 = "+++ %s\t%s\n" % ("/dev/null", epoch) + else: + l2 = "+++ %s\t%s\n" % ("b/" + fn, bd) l3 = "@@ -1,%d +0,0 @@\n" % len(a) l = [l1, l2, l3] + ["-" + e for e in a] else: diff --git a/mercurial/mpatch.c b/mercurial/mpatch.c --- a/mercurial/mpatch.c +++ b/mercurial/mpatch.c @@ -43,6 +43,7 @@ static uint32_t ntohl(uint32_t x) #endif static char mpatch_doc[] = "Efficient binary patching."; +static PyObject *mpatch_Error; struct frag { int start, end, len; @@ -65,8 +66,11 @@ static struct flist *lalloc(int size) a = NULL; } else a->head = a->tail = a->base; + return a; } - return a; + if (!PyErr_Occurred()) + PyErr_NoMemory(); + return NULL; } static void lfree(struct flist *a) @@ -215,6 +219,9 @@ static struct flist *decode(char *bin, i /* assume worst case size, we won't have many of these lists */ l = lalloc(len / 12); + if (!l) + return NULL; + lt = l->tail; while (bin < end) { @@ -227,6 +234,13 @@ static struct flist *decode(char *bin, i lt++; } + if (bin != end) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, "patch cannot be decoded"); + lfree(l); + return NULL; + } + l->tail = lt; return l; } @@ -238,6 +252,12 @@ static int calcsize(int len, struct flis struct frag *f = l->head; while (f != l->tail) { + if (f->start < last || f->end > len) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, + "invalid patch"); + return -1; + } outlen += f->start - last; last = f->end; outlen += f->len; @@ -248,13 +268,19 @@ static int calcsize(int len, struct flis return outlen; } -static void apply(char *buf, char *orig, int len, struct flist *l) +static int apply(char *buf, char *orig, int len, struct flist *l) { struct frag *f = l->head; int last = 0; char *p = buf; while (f != l->tail) { + if (f->start < last || f->end > len) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, + "invalid patch"); + return 0; + } memcpy(p, orig + last, f->start - last); p += f->start - last; memcpy(p, f->data, f->len); @@ -263,6 +289,7 @@ static void apply(char *buf, char *orig, f++; } memcpy(p, orig + last, len - last); + return 1; } /* recursively generate a patch of all bins between start and end */ @@ -304,16 +331,25 @@ patches(PyObject *self, PyObject *args) patch = fold(bins, 0, len); if (!patch) - return PyErr_NoMemory(); + return NULL; outlen = calcsize(PyString_Size(text), patch); + if (outlen < 0) { + result = NULL; + goto cleanup; + } result = PyString_FromStringAndSize(NULL, outlen); - if (result) { - in = PyString_AsString(text); - out = PyString_AsString(result); - apply(out, in, PyString_Size(text), patch); + if (!result) { + result = NULL; + goto cleanup; } - + in = PyString_AsString(text); + out = PyString_AsString(result); + if (!apply(out, in, PyString_Size(text), patch)) { + Py_DECREF(result); + result = NULL; + } +cleanup: lfree(patch); return result; } @@ -327,5 +363,6 @@ PyMODINIT_FUNC initmpatch(void) { Py_InitModule3("mpatch", methods, mpatch_doc); + mpatch_Error = PyErr_NewException("mpatch.mpatchError", NULL, NULL); } diff --git a/mercurial/revlog.py b/mercurial/revlog.py --- a/mercurial/revlog.py +++ b/mercurial/revlog.py @@ -624,12 +624,10 @@ class revlog(object): # we store negative distances because heap returns smallest member h = [(-dist[node], node)] seen = {} - earliest = self.count() while h: d, n = heapq.heappop(h) if n not in seen: seen[n] = 1 - r = self.rev(n) yield (-d, n) for p in self.parents(n): heapq.heappush(h, (-dist[p], p)) @@ -690,11 +688,6 @@ class revlog(object): p = self.parents(self.node(revs[0]))[0] revs.insert(0, self.rev(p)) - # helper to reconstruct intermediate versions - def construct(text, base, rev): - bins = [self.chunk(r) for r in xrange(base + 1, rev + 1)] - return mdiff.patches(text, bins) - # build deltas for d in xrange(0, len(revs) - 1): a, b = revs[d], revs[d + 1] @@ -738,10 +731,10 @@ class revlog(object): base = prev = -1 start = end = measure = 0 if r: - start = self.start(self.base(t)) + base = self.base(t) + start = self.start(base) end = self.end(t) - measure = self.length(self.base(t)) - base = self.base(t) + measure = self.length(base) prev = self.tip() transaction.add(self.datafile, end) @@ -793,14 +786,15 @@ class revlog(object): raise RevlogError(_("consistency error adding group")) measure = len(text) else: - e = (end, len(cdelta), self.base(t), link, p1, p2, node) + e = (end, len(cdelta), base, link, p1, p2, node) self.index.append(e) self.nodemap[node] = r dfh.write(cdelta) ifh.write(struct.pack(indexformat, *e)) t, r, chain, prev = r, r + 1, node, node - start = self.start(self.base(t)) + base = self.base(t) + start = self.start(base) end = self.end(t) dfh.close() @@ -828,6 +822,7 @@ class revlog(object): # then reset internal state in memory to forget those revisions self.cache = None + self.chunkcache = None for p in self.index[rev:]: del self.nodemap[p[6]] del self.index[rev:] diff --git a/mercurial/sshrepo.py b/mercurial/sshrepo.py --- a/mercurial/sshrepo.py +++ b/mercurial/sshrepo.py @@ -110,7 +110,7 @@ class sshrepository(remoterepository): except: raise hg.RepoError(_("unexpected response '%s'") % (d[:400] + "...")) - def changegroup(self, nodes): + def changegroup(self, nodes, kind): n = " ".join(map(hex, nodes)) f = self.do_cmd("changegroup", roots=n) return self.pipei diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -81,7 +81,7 @@ try: author='Matt Mackall', author_email='mpm@selenic.com', url='http://selenic.com/mercurial', - description='scalable distributed SCM', + description='Scalable distributed SCM', license='GNU GPL', packages=['mercurial', 'hgext'], ext_modules=[Extension('mercurial.mpatch', ['mercurial/mpatch.c']), @@ -92,6 +92,10 @@ try: glob.glob('templates/*.tmpl'))], cmdclass=cmdclass, scripts=['hg', 'hgmerge'], + options=dict(bdist_mpkg=dict(zipdist=True, + license='COPYING', + readme='contrib/macosx/Readme.html', + welcome='contrib/macosx/Welcome.html')), **py2exe_opts) finally: mercurial.version.forget_version() diff --git a/templates/changelog-gitweb.tmpl b/templates/changelog-gitweb.tmpl --- a/templates/changelog-gitweb.tmpl +++ b/templates/changelog-gitweb.tmpl @@ -27,4 +27,8 @@ #entries%changelogentry# + + #footer# diff --git a/templates/changelogentry-gitweb.tmpl b/templates/changelogentry-gitweb.tmpl --- a/templates/changelogentry-gitweb.tmpl +++ b/templates/changelogentry-gitweb.tmpl @@ -5,7 +5,7 @@ -#author|obfuscate# [#date|rfc822date#]
+#author|obfuscate# [#date|rfc822date#] rev #rev#
#desc|escape|addbreaks# diff --git a/templates/fileannotate-gitweb.tmpl b/templates/fileannotate-gitweb.tmpl --- a/templates/fileannotate-gitweb.tmpl +++ b/templates/fileannotate-gitweb.tmpl @@ -10,7 +10,7 @@
#file|escape#
@@ -19,6 +19,7 @@ changeset #rev#: #node|short# +#rename%filerename# #parent%fileannotateparent# #child%fileannotatechild# diff --git a/templates/fileannotate-raw.tmpl b/templates/fileannotate-raw.tmpl new file mode 100644 --- /dev/null +++ b/templates/fileannotate-raw.tmpl @@ -0,0 +1,5 @@ +#header# +#annotate%annotateline# +#footer# + + diff --git a/templates/fileannotate.tmpl b/templates/fileannotate.tmpl --- a/templates/fileannotate.tmpl +++ b/templates/fileannotate.tmpl @@ -10,6 +10,7 @@ manifest file revisions +raw

Annotate #file|escape#

diff --git a/templates/filelog-gitweb.tmpl b/templates/filelog-gitweb.tmpl --- a/templates/filelog-gitweb.tmpl +++ b/templates/filelog-gitweb.tmpl @@ -1,12 +1,12 @@ #header# -#repo|escape#: Manifest +#repo|escape#: File revisions