# HG changeset patch # User Vadim Gelfer # Date 1138956751 28800 # Node ID 5e73dfe26ac19d30450baaeb634acb6b8983df77 # Parent dd5085897010768f810b28ec63b4a9212adcf25c# Parent 60c3a55c61b81f34f00046a99fc93c4f426ea050 merge with crew. diff -r dd5085897010 -r 5e73dfe26ac1 contrib/bash_completion --- a/contrib/bash_completion Fri Feb 03 00:50:28 2006 -0800 +++ b/contrib/bash_completion Fri Feb 03 00:52:31 2006 -0800 @@ -2,36 +2,36 @@ _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 @@ _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 @@ 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 @@ 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 @@ # 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 @@ 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 @@ 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 @@ 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 -r dd5085897010 -r 5e73dfe26ac1 contrib/patchbomb --- a/contrib/patchbomb Fri Feb 03 00:50:28 2006 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,276 +0,0 @@ -#!/usr/bin/python -# -# Interactive script 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. -# -# 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: -# -# The remainder of the changeset description. -# -# [Optional] If the diffstat program is installed, the result of -# running diffstat on the patch. -# -# The patch itself, as generated by "hg export". -# -# Each message refers to all of its predecessors using the In-Reply-To -# and References headers, so they will show up as a sequence in -# threaded mail and news readers, and in mail archives. -# -# For each changeset, you will be prompted with a diffstat summary and -# the changeset summary, so you can be sure you are sending the right -# changes. -# -# It is best to run this script with the "-n" (test only) flag before -# firing it up "for real", in which case it will use your pager to -# display each of the messages that it would send. -# -# To configure a default mail host, add a section like this to your -# hgrc file: -# -# [smtp] -# host = my_mail_host -# port = 1025 -# tls = yes # or omit if not needed -# username = user # if SMTP authentication required -# password = password # if SMTP authentication required - PLAINTEXT -# -# To configure other defaults, add a section like this to your hgrc -# file: -# -# [patchbomb] -# from = My Name -# to = recipient1, recipient2, ... -# cc = cc1, cc2, ... - -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from mercurial import commands -from mercurial import fancyopts -from mercurial import hg -from mercurial import ui -import os -import popen2 -import smtplib -import socket -import sys -import tempfile -import time - -try: - # readline gives raw_input editing capabilities, but is not - # present on windows - import readline -except ImportError: pass - -def diffstat(patch): - fd, name = tempfile.mkstemp() - try: - p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) - try: - for line in patch: print >> p.tochild, line - p.tochild.close() - if p.wait(): return - fp = os.fdopen(fd, 'r') - stat = [] - for line in fp: stat.append(line.lstrip()) - last = stat.pop() - stat.insert(0, last) - stat = ''.join(stat) - if stat.startswith('0 files'): raise ValueError - return stat - except: raise - finally: - try: os.unlink(name) - except: pass - -def patchbomb(ui, repo, *revs, **opts): - def prompt(prompt, default = None, rest = ': ', empty_ok = False): - if default: prompt += ' [%s]' % default - prompt += rest - while True: - r = raw_input(prompt) - if r: return r - if default is not None: return default - if empty_ok: return r - ui.warn('Please enter a valid value.\n') - - def confirm(s): - if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'): - raise ValueError - - def cdiffstat(summary, patch): - s = diffstat(patch) - if s: - if summary: - ui.write(summary, '\n') - ui.write(s, '\n') - confirm('Does the diffstat above look okay') - return s - - def makepatch(patch, idx, total): - desc = [] - node = None - body = '' - for line in patch: - if line.startswith('#'): - if line.startswith('# Node ID'): node = line.split()[-1] - continue - if line.startswith('diff -r'): break - desc.append(line) - if not node: raise ValueError - - #body = ('\n'.join(desc[1:]).strip() or - # 'Patch subject is complete summary.') - #body += '\n\n\n' - - if opts['plain']: - while patch and patch[0].startswith('# '): patch.pop(0) - if patch: patch.pop(0) - while patch and not patch[0].strip(): patch.pop(0) - if opts['diffstat']: - body += cdiffstat('\n'.join(desc), patch) + '\n\n' - body += '\n'.join(patch) - msg = MIMEText(body) - subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip()) - if subj.endswith('.'): subj = subj[:-1] - msg['Subject'] = subj - msg['X-Mercurial-Node'] = node - return msg - - start_time = int(time.time()) - - def genmsgid(id): - return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn()) - - patches = [] - - class exportee: - def __init__(self, container): - self.lines = [] - self.container = container - self.name = 'email' - - def write(self, data): - self.lines.append(data) - - def close(self): - self.container.append(''.join(self.lines).split('\n')) - self.lines = [] - - commands.export(ui, repo, *args, **{'output': exportee(patches), - 'switch_parent': False, - 'text': None}) - - jumbo = [] - msgs = [] - - 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') - - sender = (opts['from'] or ui.config('patchbomb', 'from') or - prompt('From', ui.username())) - - msg = MIMEMultipart() - msg['Subject'] = '[PATCH 0 of %d] %s' % ( - len(patches), - opts['subject'] or - prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches))) - - def getaddrs(opt, prpt, default = None): - addrs = opts[opt] or (ui.config('patchbomb', opt) or - prompt(prpt, default = default)).split(',') - return [a.strip() for a in addrs if a.strip()] - to = getaddrs('to', 'To') - cc = getaddrs('cc', 'Cc', '') - - ui.write('Finish with ^D or a dot on a line by itself.\n\n') - - body = [] - - while True: - try: l = raw_input() - except EOFError: break - if l == '.': break - body.append(l) - - msg.attach(MIMEText('\n'.join(body) + '\n')) - - ui.write('\n') - - if opts['diffstat']: - d = cdiffstat('Final summary:\n', jumbo) - if d: msg.attach(MIMEText(d)) - - msgs.insert(0, msg) - - if not opts['test']: - s = smtplib.SMTP() - s.connect(host = ui.config('smtp', 'host', 'mail'), - port = int(ui.config('smtp', 'port', 25))) - if ui.configbool('smtp', 'tls'): - s.ehlo() - s.starttls() - s.ehlo() - username = ui.config('smtp', 'username') - password = ui.config('smtp', 'password') - if username and password: - s.login(username, password) - parent = None - tz = time.strftime('%z') - for m in msgs: - try: - m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) - except TypeError: - m['Message-Id'] = genmsgid('patchbomb') - if parent: - m['In-Reply-To'] = parent - else: - parent = m['Message-Id'] - m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz - start_time += 1 - m['From'] = sender - m['To'] = ', '.join(to) - if cc: m['Cc'] = ', '.join(cc) - ui.status('Sending ', m['Subject'], ' ...\n') - if opts['test']: - fp = os.popen(os.getenv('PAGER', 'more'), 'w') - fp.write(m.as_string(0)) - fp.write('\n') - fp.close() - else: - s.sendmail(sender, to + cc, m.as_string(0)) - if not opts['test']: - 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) diff -r dd5085897010 -r 5e73dfe26ac1 hgext/gpg.py --- a/hgext/gpg.py Fri Feb 03 00:50:28 2006 -0800 +++ b/hgext/gpg.py Fri Feb 03 00:52:31 2006 -0800 @@ -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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 -r dd5085897010 -r 5e73dfe26ac1 hgext/patchbomb.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/patchbomb.py Fri Feb 03 00:52:31 2006 -0800 @@ -0,0 +1,275 @@ +# 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. +# +# 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: +# +# The remainder of the changeset description. +# +# [Optional] If the diffstat program is installed, the result of +# running diffstat on the patch. +# +# The patch itself, as generated by "hg export". +# +# Each message refers to all of its predecessors using the In-Reply-To +# and References headers, so they will show up as a sequence in +# threaded mail and news readers, and in mail archives. +# +# For each changeset, you will be prompted with a diffstat summary and +# the changeset summary, so you can be sure you are sending the right +# changes. +# +# It is best to run this script with the "-n" (test only) flag before +# firing it up "for real", in which case it will use your pager to +# display each of the messages that it would send. +# +# To configure a default mail host, add a section like this to your +# hgrc file: +# +# [smtp] +# host = my_mail_host +# port = 1025 +# tls = yes # or omit if not needed +# username = user # if SMTP authentication required +# password = password # if SMTP authentication required - PLAINTEXT +# +# To configure other defaults, add a section like this to your hgrc +# file: +# +# [patchbomb] +# from = My Name +# to = recipient1, recipient2, ... +# cc = cc1, cc2, ... + +from email.MIMEMultipart import MIMEMultipart +from email.MIMEText import MIMEText +from mercurial import commands +from mercurial import hg +from mercurial import ui +from mercurial.i18n import gettext as _ +import os +import popen2 +import smtplib +import socket +import sys +import tempfile +import time + +try: + # readline gives raw_input editing capabilities, but is not + # present on windows + import readline +except ImportError: pass + +def diffstat(patch): + fd, name = tempfile.mkstemp() + try: + p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name) + try: + for line in patch: print >> p.tochild, line + p.tochild.close() + if p.wait(): return + fp = os.fdopen(fd, 'r') + stat = [] + for line in fp: stat.append(line.lstrip()) + last = stat.pop() + stat.insert(0, last) + stat = ''.join(stat) + if stat.startswith('0 files'): raise ValueError + return stat + except: raise + finally: + try: os.unlink(name) + 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 + while True: + r = raw_input(prompt) + if r: return r + if default is not None: return default + if empty_ok: return r + ui.warn(_('Please enter a valid value.\n')) + + def confirm(s): + if not prompt(s, default = 'y', rest = '? ').lower().startswith('y'): + raise ValueError + + def cdiffstat(summary, patch): + s = diffstat(patch) + if s: + if summary: + ui.write(summary, '\n') + ui.write(s, '\n') + confirm(_('Does the diffstat above look okay')) + return s + + def makepatch(patch, idx, total): + desc = [] + node = None + body = '' + for line in patch: + if line.startswith('#'): + if line.startswith('# Node ID'): node = line.split()[-1] + continue + if line.startswith('diff -r'): break + desc.append(line) + if not node: raise ValueError + + #body = ('\n'.join(desc[1:]).strip() or + # 'Patch subject is complete summary.') + #body += '\n\n\n' + + if opts['plain']: + while patch and patch[0].startswith('# '): patch.pop(0) + if patch: patch.pop(0) + while patch and not patch[0].strip(): patch.pop(0) + if opts['diffstat']: + body += cdiffstat('\n'.join(desc), patch) + '\n\n' + body += '\n'.join(patch) + msg = MIMEText(body) + subj = '[PATCH %d of %d] %s' % (idx, total, desc[0].strip()) + if subj.endswith('.'): subj = subj[:-1] + msg['Subject'] = subj + msg['X-Mercurial-Node'] = node + return msg + + start_time = int(time.time()) + + def genmsgid(id): + return '<%s.%s@%s>' % (id[:20], start_time, socket.getfqdn()) + + patches = [] + + class exportee: + def __init__(self, container): + self.lines = [] + self.container = container + self.name = 'email' + + def write(self, data): + self.lines.append(data) + + def close(self): + self.container.append(''.join(self.lines).split('\n')) + self.lines = [] + + 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)) + + 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')) + + sender = (opts['from'] or ui.config('patchbomb', 'from') or + prompt('From', ui.username())) + + msg = MIMEMultipart() + msg['Subject'] = '[PATCH 0 of %d] %s' % ( + len(patches), + opts['subject'] or + prompt('Subject:', rest = ' [PATCH 0 of %d] ' % len(patches))) + + def getaddrs(opt, prpt, default = None): + addrs = opts[opt] or (ui.config('patchbomb', opt) or + prompt(prpt, default = default)).split(',') + return [a.strip() for a in addrs if a.strip()] + to = getaddrs('to', 'To') + cc = getaddrs('cc', 'Cc', '') + + ui.write(_('Finish with ^D or a dot on a line by itself.\n\n')) + + body = [] + + while True: + try: l = raw_input() + except EOFError: break + if l == '.': break + body.append(l) + + msg.attach(MIMEText('\n'.join(body) + '\n')) + + ui.write('\n') + + if opts['diffstat']: + d = cdiffstat(_('Final summary:\n'), jumbo) + if d: msg.attach(MIMEText(d)) + + msgs.insert(0, msg) + + if not opts['test']: + s = smtplib.SMTP() + s.connect(host = ui.config('smtp', 'host', 'mail'), + port = int(ui.config('smtp', 'port', 25))) + if ui.configbool('smtp', 'tls'): + s.ehlo() + s.starttls() + s.ehlo() + username = ui.config('smtp', 'username') + password = ui.config('smtp', 'password') + if username and password: + s.login(username, password) + parent = None + tz = time.strftime('%z') + for m in msgs: + try: + m['Message-Id'] = genmsgid(m['X-Mercurial-Node']) + except TypeError: + m['Message-Id'] = genmsgid('patchbomb') + if parent: + m['In-Reply-To'] = parent + else: + parent = m['Message-Id'] + m['Date'] = time.strftime('%a, %e %b %Y %T ', time.localtime(start_time)) + tz + start_time += 1 + m['From'] = sender + m['To'] = ', '.join(to) + if cc: m['Cc'] = ', '.join(cc) + ui.status('Sending ', m['Subject'], ' ...\n') + if opts['test']: + fp = os.popen(os.getenv('PAGER', 'more'), 'w') + fp.write(m.as_string(0)) + fp.write('\n') + fp.close() + else: + s.sendmail(sender, to + cc, m.as_string(0)) + if not opts['test']: + s.close() + +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'), + ('s', 'subject', '', 'subject of introductory message'), + ('t', 'to', [], 'email addresses of recipients')], + "hg email [OPTION]... [REV]...") + } diff -r dd5085897010 -r 5e73dfe26ac1 mercurial/localrepo.py --- a/mercurial/localrepo.py Fri Feb 03 00:50:28 2006 -0800 +++ b/mercurial/localrepo.py Fri Feb 03 00:52:31 2006 -0800 @@ -1399,6 +1399,13 @@ 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 +1418,6 @@ 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")) diff -r dd5085897010 -r 5e73dfe26ac1 tests/test-up-local-change --- a/tests/test-up-local-change Fri Feb 03 00:50:28 2006 -0800 +++ b/tests/test-up-local-change Fri Feb 03 00:52:31 2006 -0800 @@ -24,11 +24,34 @@ cd ../r2 hg -q pull ../r1 hg status +hg parents hg --debug up +hg parents +hg --debug up 0 +hg parents hg --debug up -m || echo failed -hg --debug up -f -m +hg parents +hg --debug up hg parents hg -v history hg diff | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" +# create a second head +cd ../r1 +hg up 0 +echo b2 > b +echo a3 > a +hg addremove +hg commit -m "3" -d "0 0" + +cd ../r2 +hg -q pull ../r1 +hg status +hg parents +hg --debug up || echo failed +hg --debug up -m || echo failed +hg --debug up -f -m +hg parents +hg diff | sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \ + -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" diff -r dd5085897010 -r 5e73dfe26ac1 tests/test-up-local-change.out --- a/tests/test-up-local-change.out Fri Feb 03 00:50:28 2006 -0800 +++ b/tests/test-up-local-change.out Fri Feb 03 00:52:31 2006 -0800 @@ -7,6 +7,11 @@ +abc adding b M a +changeset: 0:c19d34741b0a +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 1 + resolving manifests force None allow None moddirstate True linear True ancestor a0c8bcbbb45c local a0c8bcbbb45c remote 1165e8bd193e @@ -16,11 +21,38 @@ merging a resolving a file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2 -abort: outstanding uncommited changes -failed +changeset: 1:1e71731e6fbb +tag: tip +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 2 + resolving manifests - force None allow 1 moddirstate True linear True - ancestor 1165e8bd193e local 1165e8bd193e remote 1165e8bd193e + force None allow None moddirstate True linear True + ancestor a0c8bcbbb45c local 1165e8bd193e remote a0c8bcbbb45c +remote deleted b +removing b +changeset: 0:c19d34741b0a +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 1 + +abort: there is nothing to merge, just use 'hg update' +failed +changeset: 0:c19d34741b0a +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 1 + +resolving manifests + force None allow None moddirstate True linear True + ancestor a0c8bcbbb45c local a0c8bcbbb45c remote 1165e8bd193e + a versions differ, resolve +remote created b +getting b +merging a +resolving a +file a: my b789fdd96dc2 other d730145abbf9 ancestor b789fdd96dc2 changeset: 1:1e71731e6fbb tag: tip user: test @@ -50,3 +82,52 @@ @@ -1,1 +1,1 @@ a2 -a2 +abc +adding b +M a +changeset: 1:1e71731e6fbb +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 2 + +resolving manifests + force None allow None moddirstate True linear False + ancestor a0c8bcbbb45c local 1165e8bd193e remote 4096f2872392 + a versions differ, resolve + b versions differ, resolve +this update spans a branch affecting the following files: + a (resolve) + b (resolve) +aborting update spanning branches! +(use update -m to merge across branches or -C to lose changes) +failed +abort: outstanding uncommited changes +failed +resolving manifests + force None allow 1 moddirstate True linear False + ancestor a0c8bcbbb45c local 1165e8bd193e remote 4096f2872392 + a versions differ, resolve + b versions differ, resolve +merging a +resolving a +file a: my d730145abbf9 other 13e0d5f949fa ancestor b789fdd96dc2 +merging b +resolving b +file b: my 1e88685f5dde other 61de8c7723ca ancestor 000000000000 +changeset: 1:1e71731e6fbb +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 2 + +changeset: 2:83c51d0caff4 +tag: tip +parent: 0:c19d34741b0a +user: test +date: Thu Jan 1 00:00:00 1970 +0000 +summary: 3 + +diff -r 1e71731e6fbb a +--- a/a ++++ b/a +@@ -1,1 +1,1 @@ a2 +-a2 ++abc