merge with crew
authorBenoit Boissinot <benoit.boissinot@ens-lyon.org>
Wed, 24 May 2006 09:27:16 +0200
changeset 2346 925610b2d90a
parent 2345 4f7745fc9823 (current diff)
parent 2344 ae12e5a2c4a3 (diff)
child 2347 5b178298b7f4
merge with crew
mercurial/localrepo.py
--- a/.hgignore	Wed May 24 01:01:39 2006 +0200
+++ b/.hgignore	Wed May 24 09:27:16 2006 +0200
@@ -4,6 +4,7 @@
 *.orig
 *.rej
 *~
+*.o
 *.so
 *.pyc
 *.swp
@@ -12,6 +13,7 @@
 tests/annotated
 tests/*.err
 build
+contrib/hgsh/hgsh
 dist
 doc/*.[0-9]
 doc/*.[0-9].gendoc.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/hgsh/Makefile	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,13 @@
+CC := gcc
+CFLAGS := -g -O2 -Wall -Werror
+
+prefix ?= /usr/bin
+
+hgsh: hgsh.o
+	$(CC) -o $@ $<
+
+install: hgsh
+	install -m755 hgsh $(prefix)
+
+clean:
+	rm -f *.o hgsh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/hgsh/hgsh.c	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,372 @@
+/*
+ * hgsh.c - restricted login shell for mercurial
+ *
+ * Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+ *
+ * This software may be used and distributed according to the terms of the
+ * GNU General Public License, incorporated herein by reference.
+ *
+ * this program is login shell for dedicated mercurial user account. it
+ * only allows few actions:
+ *
+ * 1. run hg in server mode on specific repository. no other hg commands
+ * are allowed. we try to verify that repo to be accessed exists under
+ * given top-level directory.
+ *
+ * 2. (optional) forward ssh connection from firewall/gateway machine to
+ * "real" mercurial host, to let users outside intranet pull and push
+ * changes through firewall.
+ *
+ * 3. (optional) run normal shell, to allow to "su" to mercurial user, use
+ * "sudo" to run programs as that user, or run cron jobs as that user.
+ *
+ * only tested on linux yet. patches for non-linux systems welcome.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* for asprintf */
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+/*
+ * user config.
+ *
+ * if you see a hostname below, just use first part of hostname. example,
+ * if you have host named foo.bar.com, use "foo".
+ */
+
+/*
+ * HG_GATEWAY: hostname of gateway/firewall machine that people outside your
+ * intranet ssh into if they need to ssh to other machines. if you do not
+ * have such machine, set to NULL.
+ */
+#ifndef HG_GATEWAY
+#define HG_GATEWAY     "gateway"
+#endif
+
+/*
+ * HG_HOST: hostname of mercurial server. if any machine is allowed, set to
+ * NULL.
+ */
+#ifndef HG_HOST
+#define HG_HOST         "mercurial"
+#endif
+
+/*
+ * HG_USER: username to log in from HG_GATEWAY to HG_HOST. if gateway and
+ * host username are same, set to NULL.
+ */
+#ifndef HG_USER
+#define HG_USER         "hg"
+#endif
+
+/*
+ * HG_ROOT: root of tree full of mercurial repos. if you do not want to
+ * validate location of repo when someone is try to access, set to NULL.
+ */
+#ifndef HG_ROOT
+#define HG_ROOT         "/home/hg/repos"
+#endif
+
+/*
+ * HG: path to the mercurial executable to run.
+ */
+#ifndef HG
+#define HG              "/home/hg/bin/hg"
+#endif
+
+/*
+ * HG_SHELL: shell to use for actions like "sudo" and "su" access to
+ * mercurial user, and cron jobs. if you want to make these things
+ * impossible, set to NULL.
+ */
+#ifndef HG_SHELL
+#define HG_SHELL        NULL
+// #define HG_SHELL        "/bin/bash"
+#endif
+
+/*
+ * HG_HELP: some way for users to get support if they have problem. if they
+ * should not get helpful message, set to NULL.
+ */
+#ifndef HG_HELP
+#define HG_HELP         "please contact support@example.com for help."
+#endif
+
+/*
+ * SSH: path to ssh executable to run, if forwarding from HG_GATEWAY to
+ * HG_HOST. if you want to use rsh instead (why?), you need to modify
+ * arguments it is called with. see forward_through_gateway.
+ */
+#ifndef SSH
+#define SSH             "/usr/bin/ssh"
+#endif
+
+/*
+ * tell whether to print command that is to be executed. useful for
+ * debugging. should not interfere with mercurial operation, since
+ * mercurial only cares about stdin and stdout, and this prints to stderr.
+ */
+static const int debug = 0;
+
+static void print_cmdline(int argc, char **argv)
+{
+    FILE *fp = stderr;
+    int i;
+
+    fputs("command: ", fp);
+
+    for (i = 0; i < argc; i++) {
+        char *spc = strpbrk(argv[i], " \t\r\n");
+        if (spc) {
+            fputc('\'', fp);
+        }
+        fputs(argv[i], fp);
+        if (spc) {
+            fputc('\'', fp);
+        }
+        if (i < argc - 1) {
+            fputc(' ', fp);
+        }
+    }
+    fputc('\n', fp);
+    fflush(fp);
+}
+
+static void usage(const char *reason, int exitcode)
+{
+    char *hg_help = HG_HELP;
+
+    if (reason) {
+        fprintf(stderr, "*** Error: %s.\n", reason);
+    }
+    fprintf(stderr, "*** This program has been invoked incorrectly.\n");
+    if (hg_help) {
+        fprintf(stderr, "*** %s\n", hg_help);
+    }
+    exit(exitcode ? exitcode : EX_USAGE);
+}
+
+/*
+ * run on gateway host to make another ssh connection, to "real" mercurial
+ * server. it sends its command line unmodified to far end.
+ *
+ * never called if HG_GATEWAY is NULL.
+ */
+static void forward_through_gateway(int argc, char **argv)
+{
+    char *ssh = SSH;
+    char *hg_host = HG_HOST;
+    char *hg_user = HG_USER;
+    char **nargv = alloca((10 + argc) * sizeof(char *));
+    int i = 0, j;
+
+    nargv[i++] = ssh;
+    nargv[i++] = "-q";
+    nargv[i++] = "-T";
+    nargv[i++] = "-x";
+    if (hg_user) {
+        nargv[i++] = "-l";
+        nargv[i++] = hg_user;
+    }
+    nargv[i++] = hg_host;
+
+    /*
+     * sshd called us with added "-c", because it thinks we are a shell.
+     * drop it if we find it.
+     */
+    j = 1;
+    if (j < argc && strcmp(argv[j], "-c") == 0) {
+        j++;
+    }
+
+    for (; j < argc; i++, j++) {
+        nargv[i] = argv[j];
+    }
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(ssh, nargv);
+    perror(ssh);
+    exit(EX_UNAVAILABLE);
+}
+
+/*
+ * run shell. let administrator "su" to mercurial user's account to do
+ * administrative works.
+ *
+ * never called if HG_SHELL is NULL.
+ */
+static void run_shell(int argc, char **argv)
+{
+    char *hg_shell = HG_SHELL;
+    char **nargv;
+    char *c;
+    int i;
+
+    nargv = alloca((argc + 3) * sizeof(char *));
+    c = strrchr(hg_shell, '/');
+
+    /* tell "real" shell it is login shell, if needed. */
+
+    if (argv[0][0] == '-' && c) {
+        nargv[0] = strdup(c);
+        if (nargv[0] == NULL) {
+            perror("malloc");
+            exit(EX_OSERR);
+        }
+        nargv[0][0] = '-';
+    } else {
+        nargv[0] = hg_shell;
+    }
+
+    for (i = 1; i < argc; i++) {
+        nargv[i] = argv[i];
+    }
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(hg_shell, nargv);
+    perror(hg_shell);
+    exit(EX_OSFILE);
+}
+
+/*
+ * paranoid wrapper, runs hg executable in server mode.
+ */
+static void serve_data(int argc, char **argv)
+{
+    char *hg_root = HG_ROOT;
+    char *repo, *abspath;
+    char *nargv[6];
+    struct stat st;
+    size_t repolen;
+    int i;
+
+    /*
+     * check argv for looking okay. we should be invoked with argv
+     * resembling like this:
+     *
+     *   hgsh
+     *   -c
+     *   hg -R some/path serve --stdio
+     *
+     * the "-c" is added by sshd, because it thinks we are login shell.
+     */
+
+    if (argc != 3) {
+        goto badargs;
+    }
+
+    if (strcmp(argv[1], "-c") != 0) {
+        goto badargs;
+    }
+
+    if (sscanf(argv[2], "hg -R %as serve --stdio", &repo) != 1) {
+        goto badargs;
+    }
+
+    repolen = repo ? strlen(repo) : 0;
+
+    if (repolen == 0) {
+        goto badargs;
+    }
+
+    if (hg_root) {
+        if (asprintf(&abspath, "%s/%s/.hg/data", hg_root, repo) == -1) {
+            goto badargs;
+        }
+
+        /*
+         * attempt to stop break out from inside the repository tree. could
+         * do something more clever here, because e.g. we could traverse a
+         * symlink that looks safe, but really breaks us out of tree.
+         */
+
+        if (strstr(abspath, "/../") != NULL) {
+            goto badargs;
+        }
+
+        /* verify that we really are looking at valid repo. */
+
+        if (stat(abspath, &st) == -1) {
+            perror(repo);
+            exit(EX_DATAERR);
+        }
+
+        if (chdir(hg_root) == -1) {
+            perror(hg_root);
+            exit(EX_SOFTWARE);
+        }
+    }
+
+    i = 0;
+    nargv[i++] = HG;
+    nargv[i++] = "-R";
+    nargv[i++] = repo;
+    nargv[i++] = "serve";
+    nargv[i++] = "--stdio";
+    nargv[i] = NULL;
+
+    if (debug) {
+        print_cmdline(i, nargv);
+    }
+
+    execv(HG, nargv);
+    perror(HG);
+    exit(EX_UNAVAILABLE);
+
+badargs:
+    /* print useless error message. */
+
+    usage("invalid arguments", EX_DATAERR);
+}
+
+int main(int argc, char **argv)
+{
+    char host[1024];
+    char *c;
+
+    if (gethostname(host, sizeof(host)) == -1) {
+        perror("gethostname");
+        exit(EX_OSERR);
+    }
+
+    if ((c = strchr(host, '.')) != NULL) {
+        *c = '\0';
+    }
+
+    if (getenv("SSH_CLIENT")) {
+        char *hg_gateway = HG_GATEWAY;
+        char *hg_host = HG_HOST;
+
+        if (hg_gateway && strcmp(host, hg_gateway) == 0) {
+            forward_through_gateway(argc, argv);
+        }
+
+        if (hg_host && strcmp(host, hg_host) != 0) {
+            usage("invoked on unexpected host", EX_USAGE);
+        }
+
+        serve_data(argc, argv);
+    } else if (HG_SHELL) {
+        run_shell(argc, argv);
+    } else {
+        usage("invalid arguments", EX_DATAERR);
+    }
+
+    return 0;
+}
--- a/contrib/win32/ReadMe.html	Wed May 24 01:01:39 2006 +0200
+++ b/contrib/win32/ReadMe.html	Wed May 24 09:27:16 2006 +0200
@@ -90,8 +90,14 @@
       other Mercurial commands should work fine for you.</p>
 
     <h1>Configuration notes</h1>
-    <p>The default editor is 'vi'. You can set the EDITOR environment variable
-    (or HGEDITOR) to specify your preference. </p>
+    <p>The default editor for commit messages is 'vi'. You can set the EDITOR
+    (or HGEDITOR) environment variable to specify your preference or set it in
+    mercurial.ini:</p>
+    <pre>
+[ui]
+editor = whatever
+</pre>
+
 
     <h1>Reporting problems</h1>
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/acl.py	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,124 @@
+# acl.py - changeset access control for mercurial
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+#
+# this hook allows to allow or deny access to parts of a repo when
+# taking incoming changesets.
+#
+# authorization is against local user name on system where hook is
+# run, not committer of original changeset (since that is easy to
+# spoof).
+#
+# acl hook is best to use if you use hgsh to set up restricted shells
+# for authenticated users to only push to / pull from.  not safe if
+# user has interactive shell access, because they can disable hook.
+# also not safe if remote users share one local account, because then
+# no way to tell remote users apart.
+#
+# to use, configure acl extension in hgrc like this:
+#
+#   [extensions]
+#   hgext.acl =
+#
+#   [hooks]
+#   pretxnchangegroup.acl = python:hgext.acl.hook
+#
+#   [acl]
+#   sources = serve        # check if source of incoming changes in this list
+#                          # ("serve" == ssh or http, "push", "pull", "bundle")
+#
+# allow and deny lists have subtree pattern (default syntax is glob)
+# on left, user names on right. deny list checked before allow list.
+#
+#   [acl.allow]
+#   # if acl.allow not present, all users allowed by default
+#   # empty acl.allow = no users allowed
+#   docs/** = doc_writer
+#   .hgtags = release_engineer
+#
+#   [acl.deny]
+#   # if acl.deny not present, no users denied by default
+#   # empty acl.deny = all users allowed
+#   glob pattern = user4, user5
+#   ** = user6
+
+from mercurial.demandload import *
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+demandload(globals(), 'getpass mercurial:util')
+
+class checker(object):
+    '''acl checker.'''
+
+    def buildmatch(self, key):
+        '''return tuple of (match function, list enabled).'''
+        if not self.ui.has_config(key):
+            self.ui.debug(_('acl: %s not enabled\n') % key)
+            return None, False
+
+        thisuser = self.getuser()
+        pats = [pat for pat, user in self.ui.configitems(key)
+                if user == thisuser]
+        self.ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
+                      (key, len(pats), thisuser))
+        if pats:
+            match = util.matcher(self.repo.root, names=pats)[1]
+        else:
+            match = util.never
+        return match, True
+
+    def getuser(self):
+        '''return name of authenticated user.'''
+        return self.user
+
+    def __init__(self, ui, repo):
+        self.ui = ui
+        self.repo = repo
+        self.user = getpass.getuser()
+        cfg = self.ui.config('acl', 'config')
+        if cfg:
+            self.ui.readconfig(cfg)
+        self.allow, self.allowable = self.buildmatch('acl.allow')
+        self.deny, self.deniable = self.buildmatch('acl.deny')
+
+    def skipsource(self, source):
+        '''true if incoming changes from this source should be skipped.'''
+        ok_sources = self.ui.config('acl', 'sources', 'serve').split()
+        return source not in ok_sources
+
+    def check(self, node):
+        '''return if access allowed, raise exception if not.'''
+        files = self.repo.changelog.read(node)[3]
+        if self.deniable:
+            for f in files:
+                if self.deny(f):
+                    self.ui.debug(_('acl: user %s denied on %s\n') %
+                                  (self.getuser(), f))
+                    raise util.Abort(_('acl: access denied for changeset %s') %
+                                     short(node))
+        if self.allowable:
+            for f in files:
+                if not self.allow(f):
+                    self.ui.debug(_('acl: user %s not allowed on %s\n') %
+                                  (self.getuser(), f))
+                    raise util.Abort(_('acl: access denied for changeset %s') %
+                                     short(node))
+        self.ui.debug(_('acl: allowing changeset %s\n') % short(node))
+
+def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
+    if hooktype != 'pretxnchangegroup':
+        raise util.Abort(_('config error - hook type "%s" cannot stop '
+                           'incoming changesets') % hooktype)
+
+    c = checker(ui, repo)
+    if c.skipsource(source):
+        ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
+        return
+
+    start = repo.changelog.rev(bin(node))
+    end = repo.changelog.count()
+    for rev in xrange(start, end):
+        c.check(repo.changelog.node(rev))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/extdiff.py	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,150 @@
+# extdiff.py - external diff program support for mercurial
+#
+# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+#
+# allow to use external programs to compare revisions, or revision
+# with working dir. program is called with two arguments: paths to
+# directories containing snapshots of files to compare.
+#
+# to enable:
+#
+#   [extensions]
+#   hgext.extdiff =
+#
+# also allows to configure new diff commands, so you do not need to
+# type "hg extdiff -p kdiff3" always.
+#
+#   [extdiff]
+#   # add new command called vdiff, runs kdiff3
+#   cmd.vdiff = kdiff3
+#   # add new command called meld, runs meld (no need to name twice)
+#   cmd.meld =
+#
+# you can use -I/-X and list of file or directory names like normal
+# "hg diff" command. extdiff makes snapshots of only needed files, so
+# compare program will be fast.
+
+from mercurial.demandload import demandload
+from mercurial.i18n import gettext as _
+from mercurial.node import *
+demandload(globals(), 'mercurial:commands,util os shutil tempfile')
+
+def dodiff(ui, repo, diffcmd, pats, opts):
+    def snapshot_node(files, node):
+        '''snapshot files as of some revision'''
+        changes = repo.changelog.read(node)
+        mf = repo.manifest.read(changes[0])
+        dirname = '%s.%s' % (os.path.basename(repo.root), short(node))
+        base = os.path.join(tmproot, dirname)
+        os.mkdir(base)
+        if not ui.quiet:
+            ui.write_err(_('making snapshot of %d files from rev %s\n') %
+                         (len(files), short(node)))
+        for fn in files:
+            wfn = util.pconvert(fn)
+            ui.note('  %s\n' % wfn)
+            dest = os.path.join(base, wfn)
+            destdir = os.path.dirname(dest)
+            if not os.path.isdir(destdir):
+                os.makedirs(destdir)
+            repo.wwrite(wfn, repo.file(fn).read(mf[fn]), open(dest, 'w'))
+        return dirname
+
+    def snapshot_wdir(files):
+        '''snapshot files from working directory.
+        if not using snapshot, -I/-X does not work and recursive diff
+        in tools like kdiff3 and meld displays too many files.'''
+        dirname = os.path.basename(repo.root)
+        base = os.path.join(tmproot, dirname)
+        os.mkdir(base)
+        if not ui.quiet:
+            ui.write_err(_('making snapshot of %d files from working dir\n') %
+                         (len(files)))
+        for fn in files:
+            wfn = util.pconvert(fn)
+            ui.note('  %s\n' % wfn)
+            dest = os.path.join(base, wfn)
+            destdir = os.path.dirname(dest)
+            if not os.path.isdir(destdir):
+                os.makedirs(destdir)
+            fp = open(dest, 'w')
+            for chunk in util.filechunkiter(repo.wopener(wfn)):
+                fp.write(chunk)
+        return dirname
+
+    node1, node2 = commands.revpair(ui, repo, opts['rev'])
+    files, matchfn, anypats = commands.matchpats(repo, pats, opts)
+    modified, added, removed, deleted, unknown = repo.changes(
+        node1, node2, files, match=matchfn)
+    if not (modified or added or removed):
+        return 0
+
+    tmproot = tempfile.mkdtemp(prefix='extdiff.')
+    try:
+        dir1 = snapshot_node(modified + removed, node1)
+        if node2:
+            dir2 = snapshot_node(modified + added, node2)
+        else:
+            dir2 = snapshot_wdir(modified + added)
+        util.system('%s %s "%s" "%s"' %
+                    (diffcmd, ' '.join(opts['option']), dir1, dir2),
+                    cwd=tmproot)
+        return 1
+    finally:
+        ui.note(_('cleaning up temp directory\n'))
+        shutil.rmtree(tmproot)
+
+def extdiff(ui, repo, *pats, **opts):
+    '''use external program to diff repository (or selected files)
+
+    Show differences between revisions for the specified files, using
+    an external program.  The default program used is "diff -Npru".
+    To select a different program, use the -p option.  The program
+    will be passed the names of two directories to compare.  To pass
+    additional options to the program, use the -o option.  These will
+    be passed before the names of the directories to compare.
+
+    When two revision arguments are given, then changes are
+    shown between those revisions. If only one revision is
+    specified then that revision is compared to the working
+    directory, and, when no revisions are specified, the
+    working directory files are compared to its parent.'''
+    return dodiff(ui, repo, opts['program'] or 'diff -Npru', pats, opts)
+
+cmdtable = {
+    "extdiff":
+    (extdiff,
+     [('p', 'program', '', _('comparison program to run')),
+      ('o', 'option', [], _('pass option to comparison program')),
+      ('r', 'rev', [], _('revision')),
+      ('I', 'include', [], _('include names matching the given patterns')),
+      ('X', 'exclude', [], _('exclude names matching the given patterns'))],
+     _('hg extdiff [OPT]... [FILE]...')),
+    }
+
+def uisetup(ui):
+    for cmd, path in ui.configitems('extdiff'):
+        if not cmd.startswith('cmd.'): continue
+        cmd = cmd[4:]
+        if not path: path = cmd
+        def save(cmd, path):
+            '''use closure to save diff command to use'''
+            def mydiff(ui, repo, *pats, **opts):
+                return dodiff(ui, repo, path, pats, opts)
+            mydiff.__doc__ = '''use %s to diff repository (or selected files)
+
+            Show differences between revisions for the specified
+            files, using the %s program.
+
+            When two revision arguments are given, then changes are
+            shown between those revisions. If only one revision is
+            specified then that revision is compared to the working
+            directory, and, when no revisions are specified, the
+            working directory files are compared to its parent.''' % (cmd, cmd)
+            return mydiff
+        cmdtable[cmd] = (save(cmd, path),
+                         cmdtable['extdiff'][1][1:],
+                         _('hg %s [OPT]... [FILE]...') % cmd)
--- a/hgext/notify.py	Wed May 24 01:01:39 2006 +0200
+++ b/hgext/notify.py	Wed May 24 09:27:16 2006 +0200
@@ -99,7 +99,9 @@
 
     def __init__(self, ui, repo, hooktype):
         self.ui = ui
-        self.ui.readconfig(self.ui.config('notify', 'config'))
+        cfg = self.ui.config('notify', 'config')
+        if cfg:
+            self.ui.readconfig(cfg)
         self.repo = repo
         self.stripcount = int(self.ui.config('notify', 'strip', 0))
         self.root = self.strip(self.repo.root)
@@ -123,7 +125,7 @@
 
         path = util.pconvert(path)
         count = self.stripcount
-        while path and count >= 0:
+        while count > 0:
             c = path.find('/')
             if c == -1:
                 break
@@ -225,6 +227,8 @@
             if not msgtext.endswith('\n'):
                 self.ui.write('\n')
         else:
+            self.ui.status(_('notify: sending %d subscribers %d changes\n') %
+                             (len(self.subs), count))
             mail = self.ui.sendmail()
             mail.sendmail(templater.email(msg['From']), self.subs, msgtext)
 
@@ -250,7 +254,12 @@
     if used as changegroup hook, send one email for all changesets in
     changegroup. else send one email per changeset.'''
     n = notifier(ui, repo, hooktype)
-    if not n.subs or n.skipsource(source):
+    if not n.subs:
+        ui.debug(_('notify: no subscribers to this repo\n'))
+        return
+    if n.skipsource(source):
+        ui.debug(_('notify: changes have source "%s" - skipping\n') %
+                  source)
         return
     node = bin(node)
     if hooktype == 'changegroup':
--- a/mercurial/commands.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/commands.py	Wed May 24 09:27:16 2006 +0200
@@ -179,39 +179,61 @@
 
 revrangesep = ':'
 
-def revrange(ui, repo, revs, revlog=None):
-    """Yield revision as strings from a list of revision specifications."""
-    if revlog is None:
-        revlog = repo.changelog
-    revcount = revlog.count()
-    def fix(val, defval):
-        if not val:
-            return defval
+def revfix(repo, val, defval):
+    '''turn user-level id of changeset into rev number.
+    user-level id can be tag, changeset, rev number, or negative rev
+    number relative to number of revs (-1 is tip, etc).'''
+    if not val:
+        return defval
+    try:
+        num = int(val)
+        if str(num) != val:
+            raise ValueError
+        if num < 0:
+            num += repo.changelog.count()
+        if num < 0:
+            num = 0
+        elif num >= repo.changelog.count():
+            raise ValueError
+    except ValueError:
         try:
-            num = int(val)
-            if str(num) != val:
-                raise ValueError
-            if num < 0:
-                num += revcount
-            if num < 0:
-                num = 0
-            elif num >= revcount:
-                raise ValueError
-        except ValueError:
-            try:
-                num = repo.changelog.rev(repo.lookup(val))
-            except KeyError:
-                try:
-                    num = revlog.rev(revlog.lookup(val))
-                except KeyError:
-                    raise util.Abort(_('invalid revision identifier %s'), val)
-        return num
+            num = repo.changelog.rev(repo.lookup(val))
+        except KeyError:
+            raise util.Abort(_('invalid revision identifier %s'), val)
+    return num
+
+def revpair(ui, repo, revs):
+    '''return pair of nodes, given list of revisions. second item can
+    be None, meaning use working dir.'''
+    if not revs:
+        return repo.dirstate.parents()[0], None
+    end = None
+    if len(revs) == 1:
+        start = revs[0]
+        if revrangesep in start:
+            start, end = start.split(revrangesep, 1)
+            start = revfix(repo, start, 0)
+            end = revfix(repo, end, repo.changelog.count() - 1)
+        else:
+            start = revfix(repo, start, None)
+    elif len(revs) == 2:
+        if revrangesep in revs[0] or revrangesep in revs[1]:
+            raise util.Abort(_('too many revisions specified'))
+        start = revfix(repo, revs[0], None)
+        end = revfix(repo, revs[1], None)
+    else:
+        raise util.Abort(_('too many revisions specified'))
+    if end is not None: end = repo.lookup(str(end))
+    return repo.lookup(str(start)), end
+
+def revrange(ui, repo, revs):
+    """Yield revision as strings from a list of revision specifications."""
     seen = {}
     for spec in revs:
         if spec.find(revrangesep) >= 0:
             start, end = spec.split(revrangesep, 1)
-            start = fix(start, 0)
-            end = fix(end, revcount - 1)
+            start = revfix(repo, start, 0)
+            end = revfix(repo, end, repo.changelog.count() - 1)
             step = start > end and -1 or 1
             for rev in xrange(start, end+step, step):
                 if rev in seen:
@@ -219,7 +241,7 @@
                 seen[rev] = 1
                 yield str(rev)
         else:
-            rev = fix(spec, None)
+            rev = revfix(repo, spec, None)
             if rev in seen:
                 continue
             seen[rev] = 1
@@ -1361,15 +1383,7 @@
     it detects as binary. With -a, diff will generate a diff anyway,
     probably with undesirable results.
     """
-    node1, node2 = None, None
-    revs = [repo.lookup(x) for x in opts['rev']]
-
-    if len(revs) > 0:
-        node1 = revs[0]
-    if len(revs) > 1:
-        node2 = revs[1]
-    if len(revs) > 2:
-        raise util.Abort(_("too many revisions to diff"))
+    node1, node2 = revpair(ui, repo, opts['rev'])
 
     fns, matchfn, anypats = matchpats(repo, pats, opts)
 
@@ -3285,12 +3299,13 @@
             external.append(mod)
         except Exception, inst:
             u.warn(_("*** failed to import extension %s: %s\n") % (x[0], inst))
-            if u.traceback:
-                traceback.print_exc()
+            if u.print_exc():
                 return 1
-            continue
 
     for x in external:
+        uisetup = getattr(x, 'uisetup', None)
+        if uisetup:
+            uisetup(u)
         cmdtable = getattr(x, 'cmdtable', {})
         for t in cmdtable:
             if t in table:
@@ -3381,8 +3396,7 @@
             # enter the debugger when we hit an exception
             if options['debugger']:
                 pdb.post_mortem(sys.exc_info()[2])
-            if u.traceback:
-                traceback.print_exc()
+            u.print_exc()
             raise
     except ParseError, inst:
         if inst.args[0]:
--- a/mercurial/hgweb/__init__.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/hgweb/__init__.py	Wed May 24 09:27:16 2006 +0200
@@ -519,7 +519,8 @@
         mnode = hex(mn)
         mf = man.read(mn)
         rev = man.rev(mn)
-        node = self.repo.changelog.node(rev)
+        changerev = man.linkrev(mn)
+        node = self.repo.changelog.node(changerev)
         mff = man.readflags(mn)
 
         files = {}
--- a/mercurial/httprepo.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/httprepo.py	Wed May 24 09:27:16 2006 +0200
@@ -22,6 +22,9 @@
         if authinfo != (None, None):
             return authinfo
 
+        if not ui.interactive:
+            raise util.Abort(_('http authorization required'))
+
         self.ui.write(_("http authorization required\n"))
         self.ui.status(_("realm: %s\n") % realm)
         user = self.ui.prompt(_("user:"), default=None)
@@ -30,37 +33,95 @@
         self.add_password(realm, authuri, user, passwd)
         return (user, passwd)
 
+def netlocsplit(netloc):
+    '''split [user[:passwd]@]host[:port] into 4-tuple.'''
+
+    a = netloc.find('@')
+    if a == -1:
+        user, passwd = None, None
+    else:
+        userpass, netloc = netloc[:a], netloc[a+1:]
+        c = userpass.find(':')
+        if c == -1:
+            user, passwd = urllib.unquote(userpass), None
+        else:
+            user = urllib.unquote(userpass[:c])
+            passwd = urllib.unquote(userpass[c+1:])
+    c = netloc.find(':')
+    if c == -1:
+        host, port = netloc, None
+    else:
+        host, port = netloc[:c], netloc[c+1:]
+    return host, port, user, passwd
+
+def netlocunsplit(host, port, user=None, passwd=None):
+    '''turn host, port, user, passwd into [user[:passwd]@]host[:port].'''
+    if port:
+        hostport = host + ':' + port
+    else:
+        hostport = host
+    if user:
+        if passwd:
+            userpass = urllib.quote(user) + ':' + urllib.quote(passwd)
+        else:
+            userpass = urllib.quote(user)
+        return userpass + '@' + hostport
+    return hostport
+
 class httprepository(remoterepository):
     def __init__(self, ui, path):
-        # fix missing / after hostname
-        s = urlparse.urlsplit(path)
-        partial = s[2]
-        if not partial: partial = "/"
-        self.url = urlparse.urlunsplit((s[0], s[1], partial, '', ''))
+        scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
+        if query or frag:
+            raise util.Abort(_('unsupported URL component: "%s"') %
+                             (query or frag))
+        if not urlpath: urlpath = '/'
+        host, port, user, passwd = netlocsplit(netloc)
+
+        # urllib cannot handle URLs with embedded user or passwd
+        self.url = urlparse.urlunsplit((scheme, netlocunsplit(host, port),
+                                        urlpath, '', ''))
         self.ui = ui
-        no_list = [ "localhost", "127.0.0.1" ]
-        host = ui.config("http_proxy", "host")
-        if host is None:
-            host = os.environ.get("http_proxy")
-        if host and host.startswith('http://'):
-            host = host[7:]
-        user = ui.config("http_proxy", "user")
-        passwd = ui.config("http_proxy", "passwd")
-        no = ui.config("http_proxy", "no")
-        if no is None:
-            no = os.environ.get("no_proxy")
-        if no:
-            no_list = no_list + no.split(",")
+
+        proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
+        proxyauthinfo = None
+        handler = urllib2.BaseHandler()
+
+        if proxyurl:
+            # proxy can be proper url or host[:port]
+            if not (proxyurl.startswith('http:') or
+                    proxyurl.startswith('https:')):
+                proxyurl = 'http://' + proxyurl + '/'
+            snpqf = urlparse.urlsplit(proxyurl)
+            proxyscheme, proxynetloc, proxypath, proxyquery, proxyfrag = snpqf
+            hpup = netlocsplit(proxynetloc)
+
+            proxyhost, proxyport, proxyuser, proxypasswd = hpup
+            if not proxyuser:
+                proxyuser = ui.config("http_proxy", "user")
+                proxypasswd = ui.config("http_proxy", "passwd")
 
-        no_proxy = 0
-        for h in no_list:
-            if (path.startswith("http://" + h + "/") or
-                path.startswith("http://" + h + ":") or
-                path == "http://" + h):
-                no_proxy = 1
+            # see if we should use a proxy for this url
+            no_list = [ "localhost", "127.0.0.1" ]
+            no_list.extend([p.strip().lower() for
+                            p in ui.config("http_proxy", "no", '').split(',')
+                            if p.strip()])
+            no_list.extend([p.strip().lower() for
+                            p in os.getenv("no_proxy", '').split(',')
+                            if p.strip()])
+            # "http_proxy.always" config is for running tests on localhost
+            if (not ui.configbool("http_proxy", "always") and
+                host.lower() in no_list):
+                ui.debug(_('disabling proxy for %s\n') % host)
+            else:
+                proxyurl = urlparse.urlunsplit((
+                    proxyscheme, netlocunsplit(proxyhost, proxyport,
+                                               proxyuser, proxypasswd or ''),
+                    proxypath, proxyquery, proxyfrag))
+                handler = urllib2.ProxyHandler({scheme: proxyurl})
+                ui.debug(_('proxying through %s\n') % proxyurl)
 
-        # Note: urllib2 takes proxy values from the environment and those will
-        # take precedence
+        # urllib2 takes proxy values from the environment and those
+        # will take precedence if found, so drop them
         for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
             try:
                 if os.environ.has_key(env):
@@ -68,24 +129,15 @@
             except OSError:
                 pass
 
-        proxy_handler = urllib2.BaseHandler()
-        if host and not no_proxy:
-            proxy_handler = urllib2.ProxyHandler({"http" : "http://" + host})
+        passmgr = passwordmgr(ui)
+        if user:
+            ui.debug(_('will use user %s for http auth\n') % user)
+            passmgr.add_password(None, host, user, passwd or '')
 
-        proxyauthinfo = None
-        if user and passwd:
-            passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
-            passmgr.add_password(None, host, user, passwd)
-            proxyauthinfo = urllib2.ProxyBasicAuthHandler(passmgr)
-
-        if ui.interactive:
-            passmgr = passwordmgr(ui)
-            opener = urllib2.build_opener(
-                proxy_handler, proxyauthinfo,
-                urllib2.HTTPBasicAuthHandler(passmgr),
-                urllib2.HTTPDigestAuthHandler(passmgr))
-        else:
-            opener = urllib2.build_opener(proxy_handler, proxyauthinfo)
+        opener = urllib2.build_opener(
+            handler,
+            urllib2.HTTPBasicAuthHandler(passmgr),
+            urllib2.HTTPDigestAuthHandler(passmgr))
 
         # 1.0 here is the _protocol_ version
         opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
@@ -106,7 +158,9 @@
         try:
             resp = urllib2.urlopen(cu)
         except httplib.HTTPException, inst:
-            raise IOError(None, _('http error while sending %s command') % cmd)
+            self.ui.debug(_('http error while sending %s command\n') % cmd)
+            self.ui.print_exc()
+            raise IOError(None, inst)
         proto = resp.headers['content-type']
 
         # accept old "text/plain" and "application/hg-changegroup" for now
--- a/mercurial/localrepo.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/localrepo.py	Wed May 24 09:27:16 2006 +0200
@@ -12,7 +12,7 @@
 from demandload import *
 demandload(globals(), "appendfile changegroup")
 demandload(globals(), "re lock transaction tempfile stat mdiff errno ui")
-demandload(globals(), "revlog traceback")
+demandload(globals(), "revlog")
 
 class localrepository(object):
     def __del__(self):
@@ -125,8 +125,7 @@
                                    '%s\n') % (hname, exc))
                 if throw:
                     raise
-                if self.ui.traceback:
-                    traceback.print_exc()
+                self.ui.print_exc()
                 return True
             if r:
                 if throw:
@@ -900,6 +899,21 @@
         return r
 
     def findincoming(self, remote, base=None, heads=None, force=False):
+        """Return list of roots of the subsets of missing nodes from remote
+
+        If base dict is specified, assume that these nodes and their parents
+        exist on the remote side and that no child of a node of base exists
+        in both remote and self.
+        Furthermore base will be updated to include the nodes that exists
+        in self and remote but no children exists in self and remote.
+        If a list of heads is specified, return only nodes which are heads
+        or ancestors of these heads.
+
+        All the ancestors of base are in self and in remote.
+        All the descendants of the list returned are missing in self.
+        (and so we know that the rest of the nodes are missing in remote, see
+        outgoing)
+        """
         m = self.changelog.nodemap
         search = []
         fetch = {}
@@ -912,6 +926,7 @@
             heads = remote.heads()
 
         if self.changelog.tip() == nullid:
+            base[nullid] = 1
             if heads != [nullid]:
                 return [nullid]
             return []
@@ -930,7 +945,7 @@
         if not unknown:
             return []
 
-        rep = {}
+        req = dict.fromkeys(unknown)
         reqcnt = 0
 
         # search through remote branches
@@ -947,12 +962,12 @@
 
                 self.ui.debug(_("examining %s:%s\n")
                               % (short(n[0]), short(n[1])))
-                if n[0] == nullid:
-                    break
-                if n in seenbranch:
+                if n[0] == nullid: # found the end of the branch
+                    pass
+                elif n in seenbranch:
                     self.ui.debug(_("branch already found\n"))
                     continue
-                if n[1] and n[1] in m: # do we know the base?
+                elif n[1] and n[1] in m: # do we know the base?
                     self.ui.debug(_("found incomplete branch %s:%s\n")
                                   % (short(n[0]), short(n[1])))
                     search.append(n) # schedule branch range for scanning
@@ -963,14 +978,14 @@
                             self.ui.debug(_("found new changeset %s\n") %
                                           short(n[1]))
                             fetch[n[1]] = 1 # earliest unknown
-                            base[n[2]] = 1 # latest known
-                            continue
+                        for p in n[2:4]:
+                            if p in m:
+                                base[p] = 1 # latest known
 
-                    for a in n[2:4]:
-                        if a not in rep:
-                            r.append(a)
-                            rep[a] = 1
-
+                    for p in n[2:4]:
+                        if p not in req and p not in m:
+                            r.append(p)
+                            req[p] = 1
                 seen[n[0]] = 1
 
             if r:
@@ -981,12 +996,7 @@
                     for b in remote.branches(r[p:p+10]):
                         self.ui.debug(_("received %s:%s\n") %
                                       (short(b[0]), short(b[1])))
-                        if b[0] in m:
-                            self.ui.debug(_("found base node %s\n")
-                                          % short(b[0]))
-                            base[b[0]] = 1
-                        elif b[0] not in seen:
-                            unknown.append(b)
+                        unknown.append(b)
 
         # do binary search on the branches we found
         while search:
--- a/mercurial/packagescan.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/packagescan.py	Wed May 24 09:27:16 2006 +0200
@@ -1,5 +1,6 @@
 # packagescan.py - Helper module for identifing used modules.
 # Used for the py2exe distutil.
+# This module must be the first mercurial module imported in setup.py
 #
 # Copyright 2005 Volker Kleinfeld <Volker.Kleinfeld@gmx.de>
 #
@@ -8,25 +9,58 @@
 import glob
 import os
 import sys
-import demandload
 import ihooks
+import types
+import string
+
+# Install this module as fake demandload module
+sys.modules['mercurial.demandload'] = sys.modules[__name__]
 
-requiredmodules = {} # Will contain the modules imported by demandload
+# Requiredmodules contains the modules imported by demandload.
+# Please note that demandload can be invoked before the 
+# mercurial.packagescan.scan method is invoked in case a mercurial
+# module is imported.
+requiredmodules = {} 
 def demandload(scope, modules):
-    """ fake demandload function that collects the required modules """
+    """ fake demandload function that collects the required modules 
+        foo            import foo
+        foo bar        import foo, bar
+        foo.bar        import foo.bar
+        foo:bar        from foo import bar
+        foo:bar,quux   from foo import bar, quux
+        foo.bar:quux   from foo.bar import quux"""
+
     for m in modules.split():
         mod = None
         try:
-            module, submodules = m.split(':')
-            submodules = submodules.split(',')
+            module, fromlist = m.split(':')
+            fromlist = fromlist.split(',')
         except:
             module = m
-            submodules = []
-        mod = __import__(module, scope, scope, submodules)
-        scope[module] = mod
-        requiredmodules[mod.__name__] = 1
+            fromlist = []
+        mod = __import__(module, scope, scope, fromlist)
+        if fromlist == []:
+            # mod is only the top package, but we need all packages
+            comp = module.split('.')
+            i = 1
+            mn = comp[0]
+            while True:
+                # mn and mod.__name__ might not be the same
+                scope[mn] = mod
+                requiredmodules[mod.__name__] = 1
+                if len(comp) == i: break
+                mod = getattr(mod,comp[i]) 
+                mn = string.join(comp[:i+1],'.')
+                i += 1
+        else:
+            # mod is the last package in the component list
+            requiredmodules[mod.__name__] = 1
+            for f in fromlist:
+                scope[f] = getattr(mod,f)
+                if type(scope[f]) == types.ModuleType:
+                    requiredmodules[scope[f].__name__] = 1
 
-def getmodules(libpath,packagename):
+def scan(libpath,packagename):
     """ helper for finding all required modules of package <packagename> """
     # Use the package in the build directory
     libpath = os.path.abspath(libpath)
@@ -45,8 +79,6 @@
     pymodulefiles = glob.glob('*.py')
     extmodulefiles = glob.glob('*.pyd')
     os.chdir(cwd)
-    # Install a fake demandload module
-    sys.modules['mercurial.demandload'] = sys.modules['mercurial.packagescan']
     # Import all python modules and by that run the fake demandload
     for m in pymodulefiles:
         if m == '__init__.py': continue
@@ -62,8 +94,9 @@
         fullname = packagename+'.'+mname
         __import__(fullname,tmp,tmp)
         requiredmodules[fullname] = 1
-    includes = requiredmodules.keys()
-    return includes
+
+def getmodules():
+    return requiredmodules.keys()
 
 def importfrom(filename):
     """
--- a/mercurial/ui.py	Wed May 24 01:01:39 2006 +0200
+++ b/mercurial/ui.py	Wed May 24 09:27:16 2006 +0200
@@ -9,7 +9,7 @@
 from i18n import gettext as _
 from demandload import *
 demandload(globals(), "errno getpass os re smtplib socket sys tempfile")
-demandload(globals(), "templater util")
+demandload(globals(), "templater traceback util")
 
 class ui(object):
     def __init__(self, verbose=False, debug=False, quiet=False,
@@ -109,6 +109,10 @@
         else:
             return self.parentui.configbool(section, name, default)
 
+    def has_config(self, section):
+        '''tell whether section exists in config.'''
+        return self.cdata.has_section(section)
+
     def configitems(self, section):
         items = {}
         if self.parentui is not None:
@@ -179,7 +183,8 @@
         and stop searching if one of these is set.
         Abort if found username is an empty string to force specifying
         the commit user elsewhere, e.g. with line option or repo hgrc.
-        If not found, use $LOGNAME or $USERNAME +"@full.hostname".
+        If not found, use ($LOGNAME or $USER or $LNAME or
+        $USERNAME) +"@full.hostname".
         """
         user = os.environ.get("HGUSER")
         if user is None:
@@ -187,11 +192,10 @@
         if user is None:
             user = os.environ.get("EMAIL")
         if user is None:
-            user = os.environ.get("LOGNAME") or os.environ.get("USERNAME")
-            if user:
-                user = "%s@%s" % (user, socket.getfqdn())
-        if not user:
-            raise util.Abort(_("Please specify a username."))
+            try:
+                user = '%s@%s' % (getpass.getuser(), socket.getfqdn())
+            except KeyError:
+                raise util.Abort(_("Please specify a username."))
         return user
 
     def shortuser(self, user):
@@ -335,3 +339,11 @@
         else:
             mail = sendmail(self, method)
         return mail
+
+    def print_exc(self):
+        '''print exception traceback if traceback printing enabled.
+        only to call in exception handler. returns true if traceback
+        printed.'''
+        if self.traceback:
+            traceback.print_exc()
+        return self.traceback
--- a/setup.py	Wed May 24 01:01:39 2006 +0200
+++ b/setup.py	Wed May 24 09:27:16 2006 +0200
@@ -13,6 +13,8 @@
 from distutils.core import setup, Extension
 from distutils.command.install_data import install_data
 
+# mercurial.packagescan must be the first mercurial module imported
+import mercurial.packagescan
 import mercurial.version
 
 # py2exe needs to be installed to work
@@ -36,7 +38,6 @@
     # Due to the use of demandload py2exe is not finding the modules.
     # packagescan.getmodules creates a list of modules included in
     # the mercurial package plus depdent modules.
-    import mercurial.packagescan
     from py2exe.build_exe import py2exe as build_exe
 
     class py2exe_for_demandload(build_exe):
@@ -54,12 +55,10 @@
                 self.includes = []
             else:
                 self.includes = self.includes.split(',')
-            self.includes += mercurial.packagescan.getmodules(self.build_lib,
-                                                              'mercurial')
-            self.includes += mercurial.packagescan.getmodules(self.build_lib,
-                                                              'mercurial/hgweb')
-            self.includes += mercurial.packagescan.getmodules(self.build_lib,
-                                                              'hgext')
+            mercurial.packagescan.scan(self.build_lib,'mercurial')
+            mercurial.packagescan.scan(self.build_lib,'mercurial/hgweb')
+            mercurial.packagescan.scan(self.build_lib,'hgext')
+            self.includes += mercurial.packagescan.getmodules()
             build_exe.finalize_options(self)
 except ImportError:
     py2exe_for_demandload = None
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-empty-group	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+#  A          B    
+#  
+#  3  4       3    
+#  |\/|       |\   
+#  |/\|       | \  
+#  1  2       1  2 
+#  \ /        \ /  
+#   0          0
+#
+# if the result of the merge of 1 and 2
+# is the same in 3 and 4, no new manifest
+# will be created and the manifest group
+# will be empty during the pull
+#
+# (plus we test a failure where outgoing
+# wrongly reported the number of csets)
+#
+
+hg init a
+cd a
+touch init
+hg ci -A -m 0 -d "1000000 0"
+touch x y
+hg ci -A -m 1 -d "1000000 0"
+hg update 0
+touch x y
+hg ci -A -m 2 -d "1000000 0"
+hg merge 1
+hg ci -A -m m1 -d "1000000 0"
+#hg log
+#hg debugindex .hg/00manifest.i
+hg update -C 1
+hg merge 2
+hg ci -A -m m2 -d "1000000 0"
+#hg log
+#hg debugindex .hg/00manifest.i
+
+cd ..
+hg clone -r 3 a b
+hg clone -r 4 a c
+hg -R a outgoing b
+hg -R a outgoing c
+hg -R b outgoing c
+hg -R c outgoing b
+
+hg -R b pull a
+hg -R c pull a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-empty-group.out	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,72 @@
+adding init
+adding x
+adding y
+0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+adding x
+adding y
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+(branch merge, don't forget to commit)
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 4 changesets with 3 changes to 3 files
+3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 4 changesets with 3 changes to 3 files
+3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+searching for changes
+changeset:   4:fdb3c546e859
+tag:         tip
+parent:      1:1f703b3fcbc6
+parent:      2:de997049e034
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     m2
+
+searching for changes
+changeset:   3:f40f830c0024
+parent:      2:de997049e034
+parent:      1:1f703b3fcbc6
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     m1
+
+searching for changes
+changeset:   3:f40f830c0024
+tag:         tip
+parent:      2:de997049e034
+parent:      1:1f703b3fcbc6
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     m1
+
+searching for changes
+changeset:   3:fdb3c546e859
+tag:         tip
+parent:      1:1f703b3fcbc6
+parent:      2:de997049e034
+user:        test
+date:        Mon Jan 12 13:46:40 1970 +0000
+summary:     m2
+
+pulling from a
+searching for changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 0 changes to 0 files (+1 heads)
+(run 'hg heads' to see heads, 'hg merge' to merge)
+pulling from a
+searching for changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 0 changes to 0 files (+1 heads)
+(run 'hg heads' to see heads, 'hg merge' to merge)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-http-proxy	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+hg init a
+cd a
+echo a > a
+hg ci -Ama -d '1123456789 0'
+hg serve -p 20059 -d --pid-file=hg.pid
+
+cd ..
+("$TESTDIR/tinyproxy.py" 20060 localhost >/dev/null 2>&1 </dev/null &
+echo $! > proxy.pid)
+sleep 2
+
+echo %% url for proxy
+http_proxy=http://localhost:20060/ hg --config http_proxy.always=True clone http://localhost:20059/ b
+
+echo %% host:port for proxy
+http_proxy=localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ c
+
+echo %% proxy url with user name and password
+http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://localhost:20059/ d
+
+echo %% url with user name and password
+http_proxy=http://user:passwd@localhost:20060 hg clone --config http_proxy.always=True http://user:passwd@localhost:20059/ e
+
+echo %% bad host:port for proxy
+http_proxy=localhost:20061 hg clone --config http_proxy.always=True http://localhost:20059/ f
+
+kill $(cat proxy.pid a/hg.pid)
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-http-proxy.out	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,31 @@
+adding a
+%% url for proxy
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+%% host:port for proxy
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+%% proxy url with user name and password
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+%% url with user name and password
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+%% bad host:port for proxy
+abort: error: Connection refused
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/tinyproxy.py	Wed May 24 09:27:16 2006 +0200
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+
+__doc__ = """Tiny HTTP Proxy.
+
+This module implements GET, HEAD, POST, PUT and DELETE methods
+on BaseHTTPServer, and behaves as an HTTP proxy.  The CONNECT
+method is also implemented experimentally, but has not been
+tested yet.
+
+Any help will be greatly appreciated.           SUZUKI Hisao
+"""
+
+__version__ = "0.2.1"
+
+import BaseHTTPServer, select, socket, SocketServer, urlparse
+
+class ProxyHandler (BaseHTTPServer.BaseHTTPRequestHandler):
+    __base = BaseHTTPServer.BaseHTTPRequestHandler
+    __base_handle = __base.handle
+
+    server_version = "TinyHTTPProxy/" + __version__
+    rbufsize = 0                        # self.rfile Be unbuffered
+
+    def handle(self):
+        (ip, port) =  self.client_address
+        if hasattr(self, 'allowed_clients') and ip not in self.allowed_clients:
+            self.raw_requestline = self.rfile.readline()
+            if self.parse_request(): self.send_error(403)
+        else:
+            self.__base_handle()
+
+    def _connect_to(self, netloc, soc):
+        i = netloc.find(':')
+        if i >= 0:
+            host_port = netloc[:i], int(netloc[i+1:])
+        else:
+            host_port = netloc, 80
+        print "\t" "connect to %s:%d" % host_port
+        try: soc.connect(host_port)
+        except socket.error, arg:
+            try: msg = arg[1]
+            except: msg = arg
+            self.send_error(404, msg)
+            return 0
+        return 1
+
+    def do_CONNECT(self):
+        soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            if self._connect_to(self.path, soc):
+                self.log_request(200)
+                self.wfile.write(self.protocol_version +
+                                 " 200 Connection established\r\n")
+                self.wfile.write("Proxy-agent: %s\r\n" % self.version_string())
+                self.wfile.write("\r\n")
+                self._read_write(soc, 300)
+        finally:
+            print "\t" "bye"
+            soc.close()
+            self.connection.close()
+
+    def do_GET(self):
+        (scm, netloc, path, params, query, fragment) = urlparse.urlparse(
+            self.path, 'http')
+        if scm != 'http' or fragment or not netloc:
+            self.send_error(400, "bad url %s" % self.path)
+            return
+        soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            if self._connect_to(netloc, soc):
+                self.log_request()
+                soc.send("%s %s %s\r\n" % (
+                    self.command,
+                    urlparse.urlunparse(('', '', path, params, query, '')),
+                    self.request_version))
+                self.headers['Connection'] = 'close'
+                del self.headers['Proxy-Connection']
+                for key_val in self.headers.items():
+                    soc.send("%s: %s\r\n" % key_val)
+                soc.send("\r\n")
+                self._read_write(soc)
+        finally:
+            print "\t" "bye"
+            soc.close()
+            self.connection.close()
+
+    def _read_write(self, soc, max_idling=20):
+        iw = [self.connection, soc]
+        ow = []
+        count = 0
+        while 1:
+            count += 1
+            (ins, _, exs) = select.select(iw, ow, iw, 3)
+            if exs: break
+            if ins:
+                for i in ins:
+                    if i is soc:
+                        out = self.connection
+                    else:
+                        out = soc
+                    data = i.recv(8192)
+                    if data:
+                        out.send(data)
+                        count = 0
+            else:
+                print "\t" "idle", count
+            if count == max_idling: break
+
+    do_HEAD = do_GET
+    do_POST = do_GET
+    do_PUT  = do_GET
+    do_DELETE=do_GET
+
+class ThreadingHTTPServer (SocketServer.ThreadingMixIn,
+                           BaseHTTPServer.HTTPServer): pass
+
+if __name__ == '__main__':
+    from sys import argv
+    if argv[1:] and argv[1] in ('-h', '--help'):
+        print argv[0], "[port [allowed_client_name ...]]"
+    else:
+        if argv[2:]:
+            allowed = []
+            for name in argv[2:]:
+                client = socket.gethostbyname(name)
+                allowed.append(client)
+                print "Accept: %s (%s)" % (client, name)
+            ProxyHandler.allowed_clients = allowed
+            del argv[2:]
+        else:
+            print "Any clients will be served..."
+        BaseHTTPServer.test(ProxyHandler, ThreadingHTTPServer)